reformat remaining files

This commit is contained in:
Martin Michelsen
2025-12-21 21:06:29 -08:00
parent e5a03b7e9b
commit a0a7231d67
40 changed files with 2117 additions and 3190 deletions
+14 -28
View File
@@ -11,8 +11,6 @@
using namespace std; using namespace std;
// TODO: fix style in this file, especially in psobb functions
RandomGenerator::RandomGenerator(uint32_t seed) : initial_seed(seed) {} RandomGenerator::RandomGenerator(uint32_t seed) : initial_seed(seed) {}
DisabledRandomGenerator::DisabledRandomGenerator() : RandomGenerator(0) {} DisabledRandomGenerator::DisabledRandomGenerator() : RandomGenerator(0) {}
@@ -77,8 +75,7 @@ void PSOLFGEncryption::encrypt_both_endian(void* le_vdata, void* be_vdata, size_
} }
} }
PSOV2Encryption::PSOV2Encryption(uint32_t seed) PSOV2Encryption::PSOV2Encryption(uint32_t seed) : PSOLFGEncryption(seed, STREAM_LENGTH + 1, STREAM_LENGTH) {
: PSOLFGEncryption(seed, STREAM_LENGTH + 1, STREAM_LENGTH) {
uint32_t a = 1, b = this->initial_seed; uint32_t a = 1, b = this->initial_seed;
this->stream[0x37] = b; this->stream[0x37] = b;
for (uint16_t virtual_index = 0x15; virtual_index <= 0x36 * 0x15; virtual_index += 0x15) { for (uint16_t virtual_index = 0x15; virtual_index <= 0x36 * 0x15; virtual_index += 0x15) {
@@ -106,8 +103,7 @@ PSOEncryption::Type PSOV2Encryption::type() const {
return Type::V2; return Type::V2;
} }
PSOV3Encryption::PSOV3Encryption(uint32_t seed) PSOV3Encryption::PSOV3Encryption(uint32_t seed) : PSOLFGEncryption(seed, STREAM_LENGTH, STREAM_LENGTH) {
: PSOLFGEncryption(seed, STREAM_LENGTH, STREAM_LENGTH) {
uint32_t x, y, basekey, source1, source2, source3; uint32_t x, y, basekey, source1, source2, source3;
basekey = 0; basekey = 0;
@@ -154,9 +150,7 @@ PSOEncryption::Type PSOV3Encryption::type() const {
return Type::V3; return Type::V3;
} }
PSOBBEncryption::PSOBBEncryption( PSOBBEncryption::PSOBBEncryption(const KeyFile& key, const void* original_seed, size_t seed_size) : state(key) {
const KeyFile& key, const void* original_seed, size_t seed_size)
: state(key) {
this->apply_seed(original_seed, seed_size); this->apply_seed(original_seed, seed_size);
} }
@@ -361,11 +355,10 @@ void PSOBBEncryption::tfs1_scramble(uint32_t* out1, uint32_t* out2) const {
} }
void PSOBBEncryption::apply_seed(const void* original_seed, size_t seed_size) { void PSOBBEncryption::apply_seed(const void* original_seed, size_t seed_size) {
// Note: This part is done in the 03 command handler in the BB client, and // Note: This part is done in the 03 command handler in the BB client, and isn't actually part of the encryption
// isn't actually part of the encryption library. (Why did they do this?) // library. (Why did they do this?)
string seed; string seed;
const uint8_t* original_seed_data = reinterpret_cast<const uint8_t*>( const uint8_t* original_seed_data = reinterpret_cast<const uint8_t*>(original_seed);
original_seed);
for (size_t x = 0; x < seed_size; x += 3) { for (size_t x = 0; x < seed_size; x += 3) {
seed.push_back(original_seed_data[x] ^ 0x19); seed.push_back(original_seed_data[x] ^ 0x19);
seed.push_back(original_seed_data[x + 1] ^ 0x16); seed.push_back(original_seed_data[x + 1] ^ 0x16);
@@ -614,12 +607,8 @@ void PSOBBEncryption::apply_seed(const void* original_seed, size_t seed_size) {
} }
PSOV2OrV3DetectorEncryption::PSOV2OrV3DetectorEncryption( PSOV2OrV3DetectorEncryption::PSOV2OrV3DetectorEncryption(
uint32_t key, uint32_t key, const std::unordered_set<uint32_t>& v2_matches, const std::unordered_set<uint32_t>& v3_matches)
const std::unordered_set<uint32_t>& v2_matches, : key(key), v2_matches(v2_matches), v3_matches(v3_matches) {}
const std::unordered_set<uint32_t>& v3_matches)
: key(key),
v2_matches(v2_matches),
v3_matches(v3_matches) {}
void PSOV2OrV3DetectorEncryption::encrypt(void* data, size_t size) { void PSOV2OrV3DetectorEncryption::encrypt(void* data, size_t size) {
if (!this->active_crypt) { if (!this->active_crypt) {
@@ -641,7 +630,8 @@ void PSOV2OrV3DetectorEncryption::encrypt(void* data, size_t size) {
bool v3_match = this->v3_matches.count(decrypted_v3); bool v3_match = this->v3_matches.count(decrypted_v3);
if (!v2_match && !v3_match) { if (!v2_match && !v3_match) {
throw runtime_error(std::format( throw runtime_error(std::format(
"unable to determine crypt version (input={:08X}, v2={:08X}, v3={:08X})", encrypted, decrypted_v2, decrypted_v3)); "unable to determine crypt version (input={:08X}, v2={:08X}, v3={:08X})",
encrypted, decrypted_v2, decrypted_v3));
} else if (v2_match && v3_match) { } else if (v2_match && v3_match) {
throw runtime_error(std::format("ambiguous crypt version (v2={:08X}, v3={:08X})", decrypted_v2, decrypted_v3)); throw runtime_error(std::format("ambiguous crypt version (v2={:08X}, v3={:08X})", decrypted_v2, decrypted_v3));
} else if (v2_match) { } else if (v2_match) {
@@ -665,8 +655,7 @@ PSOEncryption::Type PSOV2OrV3DetectorEncryption::type() const {
PSOV2OrV3ImitatorEncryption::PSOV2OrV3ImitatorEncryption( PSOV2OrV3ImitatorEncryption::PSOV2OrV3ImitatorEncryption(
uint32_t key, std::shared_ptr<PSOV2OrV3DetectorEncryption> detector_crypt) uint32_t key, std::shared_ptr<PSOV2OrV3DetectorEncryption> detector_crypt)
: key(key), : key(key), detector_crypt(detector_crypt) {}
detector_crypt(detector_crypt) {}
void PSOV2OrV3ImitatorEncryption::encrypt(void* data, size_t size) { void PSOV2OrV3ImitatorEncryption::encrypt(void* data, size_t size) {
if (!this->active_crypt) { if (!this->active_crypt) {
@@ -761,9 +750,8 @@ shared_ptr<PSOBBEncryption> PSOBBMultiKeyImitatorEncryption::ensure_crypt() {
if (!key.get()) { if (!key.get()) {
throw logic_error("server crypt cannot be initialized because client crypt is not ready"); throw logic_error("server crypt cannot be initialized because client crypt is not ready");
} }
// Hack: JSD1 uses the client seed for both ends of the connection and // Hack: JSD1 uses the client seed for both ends of the connection and ignores the server seed (though each end has
// ignores the server seed (though each end has its own state after that). // its own state after that). To handle this, we use the other crypt's seed if the type is JSD1.
// To handle this, we use the other crypt's seed if the type is JSD1.
if ((key->subtype == PSOBBEncryption::Subtype::JSD1) && this->jsd1_use_detector_seed) { if ((key->subtype == PSOBBEncryption::Subtype::JSD1) && this->jsd1_use_detector_seed) {
const auto& detector_seed = this->detector_crypt->get_seed(); const auto& detector_seed = this->detector_crypt->get_seed();
this->active_crypt = make_shared<PSOBBEncryption>(*key, detector_seed.data(), detector_seed.size()); this->active_crypt = make_shared<PSOBBEncryption>(*key, detector_seed.data(), detector_seed.size());
@@ -837,9 +825,7 @@ uint32_t encrypt_challenge_time(uint16_t value) {
uint16_t decrypt_challenge_time(uint32_t value) { uint16_t decrypt_challenge_time(uint32_t value) {
uint16_t mask = (value >> 0x10); uint16_t mask = (value >> 0x10);
uint8_t mask_one_bits = count_one_bits(mask); uint8_t mask_one_bits = count_one_bits(mask);
return ((mask_one_bits < 4) || (mask_one_bits > 12)) return ((mask_one_bits < 4) || (mask_one_bits > 12)) ? 0xFFFF : ((mask ^ value) & 0xFFFF);
? 0xFFFF
: ((mask ^ value) & 0xFFFF);
} }
string decrypt_v2_registry_value(const void* data, size_t size) { string decrypt_v2_registry_value(const void* data, size_t size) {
+9 -15
View File
@@ -159,8 +159,7 @@ public:
}; };
struct KeyFile { struct KeyFile {
// initial_keys are actually a stream of uint32_ts, but we treat them as // initial_keys are actually a stream of uint32_ts, but we treat them as bytes for code simplicity
// bytes for code simplicity
union InitialKeys { union InitialKeys {
uint8_t jsd1_stream_offset; uint8_t jsd1_stream_offset;
parray<uint8_t, 0x48> as8; parray<uint8_t, 0x48> as8;
@@ -176,11 +175,9 @@ public:
} __packed_ws__(PrivateKeys, 0x1000); } __packed_ws__(PrivateKeys, 0x1000);
InitialKeys initial_keys; InitialKeys initial_keys;
PrivateKeys private_keys; PrivateKeys private_keys;
// This field only really needs to be one byte, but annoyingly, some // This field only really needs to be one byte, but annoyingly, some compilers pad this structure to a longer
// compilers pad this structure to a longer alignment, presumably because // alignment, presumably because the unions above contain structures with 32-bit alignment. To prevent this
// the unions above contain structures with 32-bit alignment. To prevent // structure's size from not matching the .nsk files' sizes, we use an unnecessarily large size for this field.
// this structure's size from not matching the .nsk files' sizes, we use
// an unnecessarily large size for this field.
le_uint64_t subtype; le_uint64_t subtype;
} __packed_ws__(KeyFile, 0x1050); } __packed_ws__(KeyFile, 0x1050);
@@ -198,8 +195,8 @@ protected:
void apply_seed(const void* original_seed, size_t seed_size); void apply_seed(const void* original_seed, size_t seed_size);
}; };
// The following classes provide support for automatically detecting which type // The following classes provide support for automatically detecting which type of encryption a client is using based
// of encryption a client is using based on their initial response to the server // on their initial response to the server
class PSOV2OrV3DetectorEncryption : public PSOEncryption { class PSOV2OrV3DetectorEncryption : public PSOEncryption {
public: public:
@@ -234,9 +231,8 @@ protected:
std::shared_ptr<PSOEncryption> active_crypt; std::shared_ptr<PSOEncryption> active_crypt;
}; };
// The following classes provide support for multiple PSOBB private keys, and // The following classes provide support for multiple PSOBB private keys, and the ability to automatically detect which
// the ability to automatically detect which key the client is using based on // key the client is using based on the first 8 bytes they send
// the first 8 bytes they send
class PSOBBMultiKeyDetectorEncryption : public PSOEncryption { class PSOBBMultiKeyDetectorEncryption : public PSOEncryption {
public: public:
@@ -397,9 +393,7 @@ DecryptedPR2 decrypt_pr2_data(const std::string& data) {
throw std::runtime_error("not enough data for PR2 header"); throw std::runtime_error("not enough data for PR2 header");
} }
phosg::StringReader r(data); phosg::StringReader r(data);
DecryptedPR2 ret = { DecryptedPR2 ret = {.compressed_data = data.substr(8), .decompressed_size = r.get<U32T<BE>>()};
.compressed_data = data.substr(8),
.decompressed_size = r.get<U32T<BE>>()};
PSOV2Encryption crypt(r.get<U32T<BE>>()); PSOV2Encryption crypt(r.get<U32T<BE>>());
if (BE) { if (BE) {
crypt.encrypt_big_endian(ret.compressed_data.data(), ret.compressed_data.size()); crypt.encrypt_big_endian(ret.compressed_data.data(), ret.compressed_data.size());
+3 -9
View File
@@ -189,25 +189,19 @@ void PSOCommandHeader::set_flag(Version version, uint32_t flag) {
void check_size_v(size_t size, size_t min_size, size_t max_size) { void check_size_v(size_t size, size_t min_size, size_t max_size) {
if (size < min_size) { if (size < min_size) {
throw std::runtime_error(std::format( throw std::runtime_error(std::format(
"command too small (expected at least 0x{:X} bytes, received 0x{:X} bytes)", "command too small (expected at least 0x{:X} bytes, received 0x{:X} bytes)", min_size, size));
min_size, size));
} }
if (max_size < min_size) { if (max_size < min_size) {
max_size = min_size; max_size = min_size;
} }
if (size > max_size) { if (size > max_size) {
throw std::runtime_error(std::format( throw std::runtime_error(std::format(
"command too large (expected at most 0x{:X} bytes, received 0x{:X} bytes)", "command too large (expected at most 0x{:X} bytes, received 0x{:X} bytes)", max_size, size));
max_size, size));
} }
} }
std::string prepend_command_header( std::string prepend_command_header(
Version version, Version version, bool encryption_enabled, uint16_t cmd, uint32_t flag, const std::string& data) {
bool encryption_enabled,
uint16_t cmd,
uint32_t flag,
const std::string& data) {
phosg::StringWriter ret; phosg::StringWriter ret;
switch (version) { switch (version) {
case Version::DC_NTE: case Version::DC_NTE:
+5 -16
View File
@@ -46,23 +46,16 @@ union PSOCommandHeader {
PSOCommandHeader(); PSOCommandHeader();
} __packed_ws__(PSOCommandHeader, 8); } __packed_ws__(PSOCommandHeader, 8);
// This function is used in a lot of places to check received command sizes and // This function is used in a lot of places to check received command sizes and cast them to the appropriate type
// cast them to the appropriate type
template <typename RetT, typename PtrT> template <typename RetT, typename PtrT>
RetT& check_size_generic( RetT& check_size_generic(PtrT data, size_t size, size_t min_size, size_t max_size) {
PtrT data,
size_t size,
size_t min_size,
size_t max_size) {
if (size < min_size) { if (size < min_size) {
throw std::runtime_error(std::format( throw std::runtime_error(std::format(
"command too small (expected at least 0x{:X} bytes, received 0x{:X} bytes)", "command too small (expected at least 0x{:X} bytes, received 0x{:X} bytes)", min_size, size));
min_size, size));
} }
if (size > max_size) { if (size > max_size) {
throw std::runtime_error(std::format( throw std::runtime_error(std::format(
"command too large (expected at most 0x{:X} bytes, received 0x{:X} bytes)", "command too large (expected at most 0x{:X} bytes, received 0x{:X} bytes)", max_size, size));
max_size, size));
} }
return *reinterpret_cast<RetT*>(data); return *reinterpret_cast<RetT*>(data);
} }
@@ -128,8 +121,4 @@ T* check_size_vec_t(std::string& data, size_t count, bool allow_extra = false) {
void check_size_v(size_t size, size_t min_size, size_t max_size = 0); void check_size_v(size_t size, size_t min_size, size_t max_size = 0);
std::string prepend_command_header( std::string prepend_command_header(
Version version, Version version, bool encryption_enabled, uint16_t cmd, uint32_t flag, const std::string& data);
bool encryption_enabled,
uint16_t cmd,
uint32_t flag,
const std::string& data);
+79 -121
View File
@@ -48,16 +48,12 @@ static void forward_command(shared_ptr<Client> c, bool to_server, const Channel:
} }
} }
// Command handlers. These are called to preprocess or react to specific // Command handlers. These are called to preprocess or react to specific commands in either direction. The functions
// commands in either direction. The functions have abbreviated names in order // have abbreviated names in order to make the massive table more readable. The functions' names are, in general,
// to make the massive table more readable. The functions' names are, in // <SC>_[VERSIONS]_<COMMAND-NUMBERS>, where <SC> denotes who sent the command, VERSIONS denotes which versions this
// general, <SC>_[VERSIONS]_<COMMAND-NUMBERS>, where <SC> denotes who sent the // handler is for (with shortcuts - so v123 refers to all non-BB versions, for example, and DGX refers to all console
// command, VERSIONS denotes which versions this handler is for (with shortcuts // versions), and COMMAND-NUMBERS are the hexadecimal value in the command header field that this handler is called
// - so v123 refers to all non-BB versions, for example, and DGX refers to all // for. If VERSIONS is omitted, the command handler is for all versions (for example, the 97 handler is like this).
// console versions), and COMMAND-NUMBERS are the hexadecimal value in the
// command header field that this handler is called for. If VERSIONS is omitted,
// the command handler is for all versions (for example, the 97 handler is like
// this).
static asio::awaitable<HandlerResult> default_handler(shared_ptr<Client>, Channel::Message&) { static asio::awaitable<HandlerResult> default_handler(shared_ptr<Client>, Channel::Message&) {
co_return HandlerResult::FORWARD; co_return HandlerResult::FORWARD;
@@ -88,8 +84,8 @@ static asio::awaitable<HandlerResult> S_1D(shared_ptr<Client> c, Channel::Messag
} }
static asio::awaitable<HandlerResult> S_97(shared_ptr<Client> c, Channel::Message&) { static asio::awaitable<HandlerResult> S_97(shared_ptr<Client> c, Channel::Message&) {
// We always assume a 97 has already been received by the client - we should // We always assume a 97 has already been received by the client - we should have sent 97 01 before sending the
// have sent 97 01 before sending the client to the proxy server. // client to the proxy server.
c->proxy_session->server_channel->send(0xB1, 0x00); c->proxy_session->server_channel->send(0xB1, 0x00);
co_return HandlerResult::SUPPRESS; co_return HandlerResult::SUPPRESS;
} }
@@ -208,9 +204,8 @@ static void send_9E_XB_to_server(std::shared_ptr<Client> c) {
} }
static asio::awaitable<HandlerResult> S_G_9A(shared_ptr<Client> c, Channel::Message&) { static asio::awaitable<HandlerResult> S_G_9A(shared_ptr<Client> c, Channel::Message&) {
// TODO: Either delete this handler or finish implementing it (flag=00/02 // TODO: Either delete this handler or finish implementing it (flag=00/02 should do the below, 01 should send 9C,
// should do the below, 01 should send 9C, anything else should end the // anything else should end the session)
// session)
C_LoginExtended_GC_9E cmd; C_LoginExtended_GC_9E cmd;
if (c->proxy_session->remote_guild_card_number < 0) { if (c->proxy_session->remote_guild_card_number < 0) {
cmd.player_tag = 0xFFFF0000; cmd.player_tag = 0xFFFF0000;
@@ -232,8 +227,7 @@ static asio::awaitable<HandlerResult> S_G_9A(shared_ptr<Client> c, Channel::Mess
cmd.login_character_name.encode(c->login_character_name, c->language()); cmd.login_character_name.encode(c->login_character_name, c->language());
cmd.client_config = c->proxy_session->remote_client_config_data; cmd.client_config = c->proxy_session->remote_client_config_data;
// If there's a guild card number, a shorter 9E is sent that ends // If there's a guild card number, a shorter 9E is sent that ends right after the client config data
// right after the client config data
c->proxy_session->server_channel->send( c->proxy_session->server_channel->send(
0x9E, 0x01, &cmd, 0x9E, 0x01, &cmd,
cmd.is_extended ? sizeof(C_LoginExtended_GC_9E) : sizeof(C_Login_PC_GC_9E)); cmd.is_extended ? sizeof(C_LoginExtended_GC_9E) : sizeof(C_Login_PC_GC_9E));
@@ -246,8 +240,7 @@ static asio::awaitable<HandlerResult> S_V123U_02_17(shared_ptr<Client> c, Channe
throw invalid_argument("patch server sent 17 server init"); throw invalid_argument("patch server sent 17 server init");
} }
// Most servers don't include after_message or have a shorter // Most servers don't include after_message or have a shorter after_message than newserv does, so don't require it
// after_message than newserv does, so don't require it
const auto& cmd = msg.check_size_t<S_ServerInitDefault_DC_PC_V3_02_17_91_9B>(0xFFFF); const auto& cmd = msg.check_size_t<S_ServerInitDefault_DC_PC_V3_02_17_91_9B>(0xFFFF);
// This isn't forwarded to the client, so don't recreate the client's crypts // This isn't forwarded to the client, so don't recreate the client's crypts
@@ -259,9 +252,8 @@ static asio::awaitable<HandlerResult> S_V123U_02_17(shared_ptr<Client> c, Channe
c->proxy_session->server_channel->crypt_out = make_shared<PSOV2Encryption>(cmd.client_key); c->proxy_session->server_channel->crypt_out = make_shared<PSOV2Encryption>(cmd.client_key);
} }
// Respond with an appropriate login command. We don't let the client do this // Respond with an appropriate login command. We don't let the client do this because it believes it already did
// because it believes it already did (when it was in an unlinked session, or // (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).
switch (c->version()) { switch (c->version()) {
case Version::PC_PATCH: case Version::PC_PATCH:
@@ -332,15 +324,12 @@ static asio::awaitable<HandlerResult> S_U_04(shared_ptr<Client> c, Channel::Mess
} }
static asio::awaitable<HandlerResult> S_B_03(shared_ptr<Client> c, Channel::Message& msg) { static asio::awaitable<HandlerResult> S_B_03(shared_ptr<Client> c, Channel::Message& msg) {
// Most servers don't include after_message or have a shorter after_message // Most servers don't include after_message or have a shorter after_message than newserv does, so don't require it
// than newserv does, so don't require it
const auto& cmd = msg.check_size_t<S_ServerInitDefault_BB_03_9B>(0xFFFF); const auto& cmd = msg.check_size_t<S_ServerInitDefault_BB_03_9B>(0xFFFF);
// This isn't forwarded to the client, so only recreate the server's crypts. // This isn't forwarded to the client, so only recreate the server's crypts. Use the same crypt type as the client...
// Use the same crypt type as the client... the server has the luxury of // the server has the luxury of being able to try all the crypts it knows to detect what type the client uses, but
// being able to try all the crypts it knows to detect what type the client // the client can't do this since it sends the first encrypted data on the connection.
// uses, but the client can't do this since it sends the first encrypted
// data on the connection.
if (!c->bb_detector_crypt) { if (!c->bb_detector_crypt) {
throw logic_error("Client proxy session started with missing detector crypt"); throw logic_error("Client proxy session started with missing detector crypt");
} }
@@ -394,44 +383,38 @@ static asio::awaitable<HandlerResult> S_V123_04(shared_ptr<Client> c, Channel::M
co_return HandlerResult::SUPPRESS; co_return HandlerResult::SUPPRESS;
} }
// Some servers send a short 04 command if they don't use all of the 0x20 // Some servers send a short 04 command if they don't use all of the 0x20 bytes available. We should be prepared to
// bytes available. We should be prepared to handle that. // handle that.
auto& cmd = msg.check_size_t<S_UpdateClientConfig_V3_04>( auto& cmd = msg.check_size_t<S_UpdateClientConfig_V3_04>(
offsetof(S_UpdateClientConfig_V3_04, client_config), offsetof(S_UpdateClientConfig_V3_04, client_config), sizeof(S_UpdateClientConfig_V3_04));
sizeof(S_UpdateClientConfig_V3_04));
// If this is a logged-in session, hide the guild card number assigned by the // If this is a logged-in session, hide the guild card number assigned by the remote server so the client doesn't see
// remote server so the client doesn't see it change. If this is a logged-out // it change. If this is a logged-out session, then the client never received a guild card number from newserv
// session, then the client never received a guild card number from newserv
// anyway, so we can let the client see the number from the remote server. // anyway, so we can let the client see the number from the remote server.
bool had_guild_card_number = (c->proxy_session->remote_guild_card_number >= 0); bool had_guild_card_number = (c->proxy_session->remote_guild_card_number >= 0);
if (c->proxy_session->remote_guild_card_number != cmd.guild_card_number) { if (c->proxy_session->remote_guild_card_number != cmd.guild_card_number) {
c->proxy_session->remote_guild_card_number = cmd.guild_card_number; c->proxy_session->remote_guild_card_number = cmd.guild_card_number;
c->log.info_f("Remote guild card number set to {}", c->proxy_session->remote_guild_card_number); c->log.info_f("Remote guild card number set to {}", c->proxy_session->remote_guild_card_number);
string message = std::format( string message = std::format(
"The remote server\nhas assigned your\nGuild Card number:\n$C6{}", "The remote server\nhas assigned your\nGuild Card number:\n$C6{}", c->proxy_session->remote_guild_card_number);
c->proxy_session->remote_guild_card_number);
send_ship_info(c->channel, message); send_ship_info(c->channel, message);
} }
if (c->login) { if (c->login) {
cmd.guild_card_number = c->login->account->account_id; cmd.guild_card_number = c->login->account->account_id;
} }
// It seems the client ignores the length of the 04 command, and always copies // It seems the client ignores the length of the 04 command, and always copies 0x20 bytes to its config data. So if
// 0x20 bytes to its config data. So if the server sends a short 04 command, // the server sends a short 04 command, part of the previous command ends up in the security data (usually part of
// part of the previous command ends up in the security data (usually part of // the copyright string from the server init command), which we simulate here. If there was previously a guild card
// the copyright string from the server init command). We simulate that here. // number, assume we got the lobby server init text instead of the port map init text.
// If there was previously a guild card number, assume we got the lobby server
// init text instead of the port map init text.
memcpy(c->proxy_session->remote_client_config_data.data(), memcpy(c->proxy_session->remote_client_config_data.data(),
had_guild_card_number ? "t Lobby Server. Copyright SEGA E" : "t Port Map. Copyright SEGA Enter", 0x20); had_guild_card_number ? "t Lobby Server. Copyright SEGA E" : "t Port Map. Copyright SEGA Enter", 0x20);
memcpy(c->proxy_session->remote_client_config_data.data(), &cmd.client_config, memcpy(c->proxy_session->remote_client_config_data.data(), &cmd.client_config,
min<size_t>(msg.data.size() - offsetof(S_UpdateClientConfig_V3_04, client_config), min<size_t>(msg.data.size() - offsetof(S_UpdateClientConfig_V3_04, client_config),
c->proxy_session->remote_client_config_data.bytes())); c->proxy_session->remote_client_config_data.bytes()));
// If the guild card number was not set, pretend (to the server) that this is // If the guild card number was not set, pretend (to the server) that this is the first 04 command the client has
// the first 04 command the client has received. The client responds with a 96 // received. The client responds with a 96 (checksum) in that case.
// (checksum) in that case.
if (!had_guild_card_number) { if (!had_guild_card_number) {
le_uint64_t checksum = phosg::random_object<uint64_t>() & 0x0000FFFFFFFFFFFF; le_uint64_t checksum = phosg::random_object<uint64_t>() & 0x0000FFFFFFFFFFFF;
c->proxy_session->server_channel->send(0x96, 0x00, &checksum, sizeof(checksum)); c->proxy_session->server_channel->send(0x96, 0x00, &checksum, sizeof(checksum));
@@ -450,9 +433,8 @@ static asio::awaitable<HandlerResult> S_V123_06(shared_ptr<Client> c, Channel::M
} }
} }
// If the session is Ep3, and Unmask Whispers is on, and there's enough data, // If the session is Ep3, and Unmask Whispers is on, and there's enough data, and the message has private_flags, and
// and the message has private_flags, and the private_flags say that you // the private_flags say that you shouldn't see the message, then change the private_flags
// shouldn't see the message, then change the private_flags
if (is_ep3(c->version()) && if (is_ep3(c->version()) &&
c->check_flag(Client::Flag::PROXY_EP3_UNMASK_WHISPERS) && c->check_flag(Client::Flag::PROXY_EP3_UNMASK_WHISPERS) &&
(msg.data.size() >= 12) && (msg.data.size() >= 12) &&
@@ -523,8 +505,8 @@ constexpr on_message_t S_P_81 = &S_81<SC_SimpleMail_PC_81>;
constexpr on_message_t S_B_81 = &S_81<SC_SimpleMail_BB_81>; constexpr on_message_t S_B_81 = &S_81<SC_SimpleMail_BB_81>;
static asio::awaitable<HandlerResult> S_88(shared_ptr<Client> c, Channel::Message& msg) { static asio::awaitable<HandlerResult> S_88(shared_ptr<Client> c, Channel::Message& msg) {
// If the client isn't in the lobby, suppress the command (Ep3 can crash if // If the client isn't in the lobby, suppress the command (Ep3 can crash if it receives this while loading; other
// it receives this while loading; other versions probably also will crash) // versions probably also will crash)
if (!c->proxy_session->is_in_lobby) { if (!c->proxy_session->is_in_lobby) {
co_return HandlerResult::SUPPRESS; co_return HandlerResult::SUPPRESS;
} }
@@ -544,8 +526,7 @@ static asio::awaitable<HandlerResult> S_88(shared_ptr<Client> c, Channel::Messag
} }
static asio::awaitable<HandlerResult> S_B1(shared_ptr<Client> c, Channel::Message&) { static asio::awaitable<HandlerResult> S_B1(shared_ptr<Client> c, Channel::Message&) {
// Block all time updates from the remote server, so client's time remains // Block all time updates from the remote server, so client's time remains consistent
// consistent
c->proxy_session->server_channel->send(0x99, 0x00); c->proxy_session->server_channel->send(0x99, 0x00);
co_return HandlerResult::SUPPRESS; co_return HandlerResult::SUPPRESS;
} }
@@ -597,7 +578,6 @@ static asio::awaitable<HandlerResult> S_B2(shared_ptr<Client> c, Channel::Messag
using FooterT = RELFileFooterT<BE>; using FooterT = RELFileFooterT<BE>;
// TODO: Support SH-4 disassembly too
bool is_ppc = ::is_ppc(c->version()); bool is_ppc = ::is_ppc(c->version());
bool is_x86 = ::is_x86(c->version()); bool is_x86 = ::is_x86(c->version());
bool is_sh4 = ::is_sh4(c->version()); bool is_sh4 = ::is_sh4(c->version());
@@ -713,8 +693,7 @@ static asio::awaitable<HandlerResult> S_C4(shared_ptr<Client> c, Channel::Messag
bool modified = false; bool modified = false;
if (c->login && c->login->account->account_id != c->proxy_session->remote_guild_card_number) { if (c->login && c->login->account->account_id != c->proxy_session->remote_guild_card_number) {
size_t expected_size = sizeof(CmdT) * msg.flag; size_t expected_size = sizeof(CmdT) * msg.flag;
// Some servers (e.g. Schtserv) send extra data on the end of this command; // Schtserv sends extra data on the end of this command; the client ignores it so we can ignore it too
// the client ignores it so we can ignore it too
auto* entries = &msg.check_size_t<CmdT>(expected_size, 0xFFFF); auto* entries = &msg.check_size_t<CmdT>(expected_size, 0xFFFF);
for (size_t x = 0; x < msg.flag; x++) { for (size_t x = 0; x < msg.flag; x++) {
if (entries[x].guild_card_number == c->proxy_session->remote_guild_card_number) { if (entries[x].guild_card_number == c->proxy_session->remote_guild_card_number) {
@@ -745,15 +724,12 @@ static asio::awaitable<HandlerResult> S_G_E4(shared_ptr<Client> c, Channel::Mess
} }
static asio::awaitable<HandlerResult> S_B_22(shared_ptr<Client> c, Channel::Message& msg) { static asio::awaitable<HandlerResult> S_B_22(shared_ptr<Client> c, Channel::Message& msg) {
// We use this command (which is sent before the init encryption command) to // We use this command (which is sent before the init encryption command) to detect a particular server behavior that
// detect a particular server behavior that we'll have to work around later. // we'll have to work around later. It looks like this command's existence is an anti-proxy measure, since this
// It looks like this command's existence is an anti-proxy measure, since // command is 0x34 bytes in total, and the logic that adds padding bytes when the command size isn't a multiple of 8
// this command is 0x34 bytes in total, and the logic that adds padding bytes // is only active when encryption is enabled. Presumably some simpler proxies would get this wrong.
// when the command size isn't a multiple of 8 is only active when encryption // Editor's note: There's an unsavory message in this command's data field, hence the hash here instead of a direct
// is enabled. Presumably some simpler proxies would get this wrong. // string comparison. I'd love to hear the story behind why they put that string there.
// Editor's note: There's an unsavory message in this command's data field,
// hence the hash here instead of a direct string comparison. I'd love to
// hear the story behind why they put that string there.
if ((msg.data.size() == 0x2C) && (phosg::fnv1a64(msg.data.data(), msg.data.size()) == 0x8AF8314316A27994)) { if ((msg.data.size() == 0x2C) && (phosg::fnv1a64(msg.data.data(), msg.data.size()) == 0x8AF8314316A27994)) {
c->log.info_f("Enabling remote IP CRC patch"); c->log.info_f("Enabling remote IP CRC patch");
c->proxy_session->enable_remote_ip_crc_patch = true; c->proxy_session->enable_remote_ip_crc_patch = true;
@@ -762,12 +738,10 @@ static asio::awaitable<HandlerResult> S_B_22(shared_ptr<Client> c, Channel::Mess
} }
static asio::awaitable<HandlerResult> S_19_U_14(shared_ptr<Client> c, Channel::Message& msg) { static asio::awaitable<HandlerResult> S_19_U_14(shared_ptr<Client> c, Channel::Message& msg) {
// If the command is shorter than 6 bytes, use the previous server command to // If the command is shorter than 6 bytes, use the previous server command to fill it in. This simulates a behavior
// fill it in. This simulates a behavior used by some private servers where a // used by some private servers where a longer previous command is used to fill part of the client's receive buffer
// longer previous command is used to fill part of the client's receive buffer // with meaningful data, then an intentionally undersize 19 command is sent which results in the client using the
// with meaningful data, then an intentionally undersize 19 command is sent // previous command's data as part of the 19 command's contents. They presumably do this in an attempt to prevent
// which results in the client using the previous command's data as part of
// the 19 command's contents. They presumably do this in an attempt to prevent
// people from using proxies. // people from using proxies.
if (msg.data.size() < sizeof(c->proxy_session->prev_server_command_bytes)) { if (msg.data.size() < sizeof(c->proxy_session->prev_server_command_bytes)) {
msg.data.append( msg.data.append(
@@ -792,8 +766,8 @@ static asio::awaitable<HandlerResult> S_19_U_14(shared_ptr<Client> c, Channel::M
auto& cmd = msg.check_size_t<S_ReconnectIPv6_Extension_19>(0xFFFF); auto& cmd = msg.check_size_t<S_ReconnectIPv6_Extension_19>(0xFFFF);
new_ep = make_endpoint_ipv6(cmd.address.data(), cmd.port); new_ep = make_endpoint_ipv6(cmd.address.data(), cmd.port);
} else { } else {
// This weird maximum size is here to properly handle the version-split // This weird maximum size is here to properly handle the version-split command that some servers (including
// command that some servers (including newserv) use on port 9100 // newserv) use on port 9100
auto& cmd = msg.check_size_t<S_Reconnect_19>(0xFFFF); auto& cmd = msg.check_size_t<S_Reconnect_19>(0xFFFF);
new_ep = make_endpoint_ipv4(cmd.address, cmd.port); new_ep = make_endpoint_ipv4(cmd.address, cmd.port);
} }
@@ -824,9 +798,8 @@ static asio::awaitable<HandlerResult> S_19_U_14(shared_ptr<Client> c, Channel::M
} }
static asio::awaitable<HandlerResult> S_V3_1A_D5(shared_ptr<Client> c, Channel::Message&) { static asio::awaitable<HandlerResult> S_V3_1A_D5(shared_ptr<Client> c, Channel::Message&) {
// If the client is a version that sends close confirmations and the client // If the client is a version that sends close confirmations and the client has the no-close-confirmation flag set in
// has the no-close-confirmation flag set in its newserv client config, send a // its newserv client config, send a fake confirmation to the remote server immediately.
// fake confirmation to the remote server immediately.
if (is_v3(c->version()) && c->check_flag(Client::Flag::NO_D6)) { if (is_v3(c->version()) && c->check_flag(Client::Flag::NO_D6)) {
c->proxy_session->server_channel->send(0xD6); c->proxy_session->server_channel->send(0xD6);
} }
@@ -1160,8 +1133,7 @@ static asio::awaitable<HandlerResult> S_6x(shared_ptr<Client> c, Channel::Messag
static asio::awaitable<HandlerResult> C_GXB_61(shared_ptr<Client> c, Channel::Message& msg) { static asio::awaitable<HandlerResult> C_GXB_61(shared_ptr<Client> c, Channel::Message& msg) {
bool modified = false; bool modified = false;
// TODO: We should check if the info board text was actually modified and // TODO: We should check if the info board text was actually modified and return MODIFIED if so.
// return MODIFIED if so.
if (is_v4(c->version())) { if (is_v4(c->version())) {
auto& pd = msg.check_size_t<C_CharacterData_BB_61_98>(0xFFFF); auto& pd = msg.check_size_t<C_CharacterData_BB_61_98>(0xFFFF);
@@ -1171,9 +1143,8 @@ static asio::awaitable<HandlerResult> C_GXB_61(shared_ptr<Client> c, Channel::Me
C_CharacterData_V3_61_98* pd; C_CharacterData_V3_61_98* pd;
if (msg.flag == 4) { // Episode 3 if (msg.flag == 4) { // Episode 3
auto& ep3_pd = msg.check_size_t<C_CharacterData_Ep3_61_98>(); auto& ep3_pd = msg.check_size_t<C_CharacterData_Ep3_61_98>();
// Technically we could decrypt the Ep3 config struct within the player // Technically we could decrypt the Ep3 config struct within the player data, but this may confuse the upstream
// data, but this may confuse some non-newserv upstream servers if they // server if it implements this structure incorrectly. The decryption would go like:
// implement this structure incorrectly. The decryption would go like:
// if (ep3_pd.ep3_config.is_encrypted) { // if (ep3_pd.ep3_config.is_encrypted) {
// decrypt_trivial_gci_data( // decrypt_trivial_gci_data(
// &ep3_pd.ep3_config.card_counts, // &ep3_pd.ep3_config.card_counts,
@@ -1206,8 +1177,7 @@ static asio::awaitable<HandlerResult> C_GX_D9(shared_ptr<Client>, Channel::Messa
while (msg.data.size() & 3) { while (msg.data.size() & 3) {
msg.data.push_back(0); msg.data.push_back(0);
} }
// TODO: We should check if the info board text was actually modified and // TODO: We should check if the info board text was actually modified and return FORWARD if not.
// return FORWARD if not.
co_return HandlerResult::MODIFIED; co_return HandlerResult::MODIFIED;
} }
@@ -1226,8 +1196,7 @@ static asio::awaitable<HandlerResult> C_B_D9(shared_ptr<Client> c, Channel::Mess
} catch (const runtime_error& e) { } catch (const runtime_error& e) {
c->log.warning_f("Failed to decode and unescape D9 command: {}", e.what()); c->log.warning_f("Failed to decode and unescape D9 command: {}", e.what());
} }
// TODO: We should check if the info board text was actually modified and // TODO: We should check if the info board text was actually modified and return HandlerResult::FORWARD if not.
// return HandlerResult::FORWARD if not.
co_return HandlerResult::MODIFIED; co_return HandlerResult::MODIFIED;
} }
@@ -1250,8 +1219,8 @@ static asio::awaitable<HandlerResult> S_44_A6(shared_ptr<Client> c, Channel::Mes
} else { } else {
basename = filename; basename = filename;
} }
output_filename = std::format("{}.{}.{}{}", output_filename = std::format(
basename, is_download ? "download" : "online", phosg::now(), extension); "{}.{}.{}{}", basename, is_download ? "download" : "online", phosg::now(), extension);
for (size_t x = 0; x < output_filename.size(); x++) { for (size_t x = 0; x < output_filename.size(); x++) {
if (output_filename[x] < 0x20 || output_filename[x] > 0x7E || output_filename[x] == '/') { if (output_filename[x] < 0x20 || output_filename[x] > 0x7E || output_filename[x] == '/') {
@@ -1420,8 +1389,8 @@ static asio::awaitable<HandlerResult> S_G_B8(shared_ptr<Client> c, Channel::Mess
c->log.info_f("Wrote {} bytes to {}", size, output_filename); c->log.info_f("Wrote {} bytes to {}", size, output_filename);
} }
// Unset the flag specifying that the client has newserv's card definitions, // Unset the flag specifying that the client has newserv's card definitions, so the file sill be sent again if the
// so the file sill be sent again if the client returns to newserv. // client returns to newserv.
c->clear_flag(Client::Flag::HAS_EP3_CARD_DEFS); c->clear_flag(Client::Flag::HAS_EP3_CARD_DEFS);
co_return is_ep3(c->version()) ? HandlerResult::FORWARD : HandlerResult::SUPPRESS; co_return is_ep3(c->version()) ? HandlerResult::FORWARD : HandlerResult::SUPPRESS;
@@ -1454,8 +1423,7 @@ static asio::awaitable<HandlerResult> S_G_B9(shared_ptr<Client> c, Channel::Mess
} }
} }
// This command exists only in final Episode 3 and not in Trial Edition // This command exists only in final Episode 3 and not in Trial Edition (hence not using is_ep3() here)
// (hence not using is_ep3() here)
co_return (c->version() == Version::GC_EP3) ? HandlerResult::FORWARD : HandlerResult::SUPPRESS; co_return (c->version() == Version::GC_EP3) ? HandlerResult::FORWARD : HandlerResult::SUPPRESS;
} }
@@ -1483,8 +1451,7 @@ static asio::awaitable<HandlerResult> S_G_EF(shared_ptr<Client> c, Channel::Mess
} }
static asio::awaitable<HandlerResult> S_B_EF(shared_ptr<Client>, Channel::Message&) { static asio::awaitable<HandlerResult> S_B_EF(shared_ptr<Client>, Channel::Message&) {
// See the comments on EF in CommandFormats.hh for why we unconditionally // See the comments on EF in CommandFormats.hh for why we unconditionally suppress these.
// suppress these.
co_return HandlerResult::SUPPRESS; co_return HandlerResult::SUPPRESS;
} }
@@ -1526,11 +1493,6 @@ static asio::awaitable<HandlerResult> S_65_67_68_EB(shared_ptr<Client> c, Channe
c->proxy_session->item_creator.reset(); c->proxy_session->item_creator.reset();
c->proxy_session->map_state.reset(); c->proxy_session->map_state.reset();
// This command can cause the client to no longer send D6 responses when
// 1A/D5 large message boxes are closed. newserv keeps track of this
// behavior in the client config, so if it happens during a proxy session,
// update the client config that we'll restore if the client uses the change
// ship or change block command.
if (c->check_flag(Client::Flag::NO_D6_AFTER_LOBBY)) { if (c->check_flag(Client::Flag::NO_D6_AFTER_LOBBY)) {
c->set_flag(Client::Flag::NO_D6); c->set_flag(Client::Flag::NO_D6);
} }
@@ -1557,8 +1519,7 @@ static asio::awaitable<HandlerResult> S_65_67_68_EB(shared_ptr<Client> c, Channe
modified = true; modified = true;
} }
} else if (c->check_flag(Client::Flag::PROXY_PLAYER_NOTIFICATIONS_ENABLED) && (msg.command != 0x67)) { } else if (c->check_flag(Client::Flag::PROXY_PLAYER_NOTIFICATIONS_ENABLED) && (msg.command != 0x67)) {
send_text_message_fmt(c->channel, "$C6Join: {}/{}\n{}", send_text_message_fmt(c->channel, "$C6Join: {}/{}\n{}", index, entry.lobby_data.guild_card_number, name);
index, entry.lobby_data.guild_card_number, name);
} }
auto& p = c->proxy_session->lobby_players[index]; auto& p = c->proxy_session->lobby_players[index];
p.guild_card_number = entry.lobby_data.guild_card_number; p.guild_card_number = entry.lobby_data.guild_card_number;
@@ -1566,8 +1527,7 @@ static asio::awaitable<HandlerResult> S_65_67_68_EB(shared_ptr<Client> c, Channe
p.language = entry.inventory.language; p.language = entry.inventory.language;
p.section_id = entry.disp.visual.section_id; p.section_id = entry.disp.visual.section_id;
p.char_class = entry.disp.visual.char_class; p.char_class = entry.disp.visual.char_class;
c->log.info_f("Added lobby player: ({}) {} {}", c->log.info_f("Added lobby player: ({}) {} {}", index, p.guild_card_number, p.name);
index, p.guild_card_number, p.name);
} }
} }
if (num_replacements > 1) { if (num_replacements > 1) {
@@ -1647,8 +1607,7 @@ static asio::awaitable<HandlerResult> S_64(shared_ptr<Client> c, Channel::Messag
cmd = &msg.check_size_t<CmdT>(sizeof(S_JoinGame_Ep3_64)); cmd = &msg.check_size_t<CmdT>(sizeof(S_JoinGame_Ep3_64));
cmd_ep3 = &msg.check_size_t<S_JoinGame_Ep3_64>(); cmd_ep3 = &msg.check_size_t<S_JoinGame_Ep3_64>();
} else if (c->version() == Version::XB_V3) { } else if (c->version() == Version::XB_V3) {
// Schtserv doesn't send the unknown_a1 field in this command, and we don't // Schtserv doesn't send the unknown_a1 field here, and we don't use it, so we allow it to be omitted.
// use it here, so we allow it to be omitted.
cmd = &msg.check_size_t<CmdT>(sizeof(CmdT) - 0x18, sizeof(CmdT)); cmd = &msg.check_size_t<CmdT>(sizeof(CmdT) - 0x18, sizeof(CmdT));
} else { } else {
cmd = &msg.check_size_t<CmdT>(0xFFFF); cmd = &msg.check_size_t<CmdT>(0xFFFF);
@@ -1665,8 +1624,8 @@ static asio::awaitable<HandlerResult> S_64(shared_ptr<Client> c, Channel::Messag
c->proxy_session->lobby_event = cmd->event; c->proxy_session->lobby_event = cmd->event;
c->proxy_session->lobby_difficulty = cmd->difficulty; c->proxy_session->lobby_difficulty = cmd->difficulty;
c->proxy_session->lobby_section_id = cmd->section_id; c->proxy_session->lobby_section_id = cmd->section_id;
// We only need the game mode for overriding drops, and SOLO behaves the same // We only need the game mode for overriding drops, and SOLO behaves the same as NORMAL in that regard, so we can
// as NORMAL in that regard, so we can conveniently ignore SOLO here // conveniently ignore SOLO here
if (cmd->battle_mode) { if (cmd->battle_mode) {
c->proxy_session->lobby_mode = GameMode::BATTLE; c->proxy_session->lobby_mode = GameMode::BATTLE;
} else if (cmd->challenge_mode) { } else if (cmd->challenge_mode) {
@@ -1703,8 +1662,7 @@ static asio::awaitable<HandlerResult> S_64(shared_ptr<Client> c, Channel::Messag
} }
if (c->version() == Version::GC_NTE) { if (c->version() == Version::GC_NTE) {
// GC NTE ignores the variations field entirely, so clear the array to // GC NTE ignores the variations field entirely, so clear the array to ensure we'll load the correct maps
// ensure we'll load the correct maps
cmd->variations = Variations(); cmd->variations = Variations();
} }
@@ -1713,7 +1671,10 @@ static asio::awaitable<HandlerResult> S_64(shared_ptr<Client> c, Channel::Messag
c->proxy_session->set_drop_mode(s, c->version(), c->override_random_seed, c->proxy_session->drop_mode); c->proxy_session->set_drop_mode(s, c->version(), c->override_random_seed, c->proxy_session->drop_mode);
if (!is_ep3(c->version()) && (c->proxy_session->lobby_mode != GameMode::CHALLENGE)) { if (!is_ep3(c->version()) && (c->proxy_session->lobby_mode != GameMode::CHALLENGE)) {
auto supermaps = s->supermaps_for_variations( auto supermaps = s->supermaps_for_variations(
c->proxy_session->lobby_episode, c->proxy_session->lobby_mode, c->proxy_session->lobby_difficulty, cmd->variations); c->proxy_session->lobby_episode,
c->proxy_session->lobby_mode,
c->proxy_session->lobby_difficulty,
cmd->variations);
c->proxy_session->map_state = make_shared<MapState>( c->proxy_session->map_state = make_shared<MapState>(
c->id, c->id,
c->proxy_session->lobby_difficulty, c->proxy_session->lobby_difficulty,
@@ -1831,8 +1792,8 @@ static asio::awaitable<HandlerResult> S_AC(shared_ptr<Client> c, Channel::Messag
} }
static asio::awaitable<HandlerResult> S_66_69_E9(shared_ptr<Client> c, Channel::Message& msg) { static asio::awaitable<HandlerResult> S_66_69_E9(shared_ptr<Client> c, Channel::Message& msg) {
// Schtserv sends a large command here for unknown reasons. The client ignores // Schtserv sends a large command here for unknown reasons. The client ignores the extra data, so we allow the large
// the extra data, so we allow the large command here. // command here.
const auto& cmd = msg.check_size_t<S_LeaveLobby_66_69_Ep3_E9>(0xFFFF); const auto& cmd = msg.check_size_t<S_LeaveLobby_66_69_Ep3_E9>(0xFFFF);
size_t index = cmd.client_id; size_t index = cmd.client_id;
if (index >= c->proxy_session->lobby_players.size()) { if (index >= c->proxy_session->lobby_players.size()) {
@@ -1996,9 +1957,8 @@ asio::awaitable<HandlerResult> C_6x(shared_ptr<Client> c, Channel::Message& msg)
break; break;
case 0x06: case 0x06:
// On BB, the 6x06 command is blank - the server generates the actual // On BB, the 6x06 command is blank - the server generates the actual Guild Card contents and sends it to the
// Guild Card contents and sends it to the target client, so we only // target client, so we only expect data here if the client isn't BB.
// expect data here if the client isn't BB.
if (!is_v4(c->version()) && if (!is_v4(c->version()) &&
c->login && c->login &&
c->login->account->account_id != c->proxy_session->remote_guild_card_number) { c->login->account->account_id != c->proxy_session->remote_guild_card_number) {
@@ -2134,8 +2094,8 @@ constexpr on_message_t C_X_6x = &C_6x<G_SendGuildCard_XB_6x06>;
constexpr on_message_t C_B_6x = &C_6x<G_SendGuildCard_BB_6x06>; constexpr on_message_t C_B_6x = &C_6x<G_SendGuildCard_BB_6x06>;
static asio::awaitable<HandlerResult> C_V123_A0_A1(shared_ptr<Client> c, Channel::Message&) { static asio::awaitable<HandlerResult> C_V123_A0_A1(shared_ptr<Client> c, Channel::Message&) {
// We override Change Ship and Change Block to send the player back to the // We override Change Ship and Change Block to send the player back to the original server (ending the proxy
// original server (ending the proxy session), except on BB. // session), except on BB.
c->proxy_session->server_channel->disconnect(); c->proxy_session->server_channel->disconnect();
co_return HandlerResult::SUPPRESS; co_return HandlerResult::SUPPRESS;
} }
@@ -2454,8 +2414,7 @@ asio::awaitable<void> on_proxy_command(shared_ptr<Client> c, bool from_server, u
asio::awaitable<void> handle_proxy_server_commands( asio::awaitable<void> handle_proxy_server_commands(
shared_ptr<Client> c, shared_ptr<ProxySession> ses, shared_ptr<Channel> channel) { shared_ptr<Client> c, shared_ptr<ProxySession> ses, shared_ptr<Channel> channel) {
std::string error_str; std::string error_str;
// server_channel can be changed by receiving a 19 command, hence the // server_channel can be changed by receiving a 19 command, hence the exception handler is inside the loop here
// exception handler is inside the loop here
while ((c->proxy_session == ses) && (ses->server_channel == channel) && channel->connected()) { while ((c->proxy_session == ses) && (ses->server_channel == channel) && channel->connected()) {
unique_ptr<Channel::Message> msg; unique_ptr<Channel::Message> msg;
try { try {
@@ -2472,8 +2431,7 @@ asio::awaitable<void> handle_proxy_server_commands(
if (ec == asio::error::eof || ec == asio::error::connection_reset) { if (ec == asio::error::eof || ec == asio::error::connection_reset) {
error_str = "Server channel\ndisconnected"; error_str = "Server channel\ndisconnected";
} else if (ec == asio::error::operation_aborted) { } else if (ec == asio::error::operation_aborted) {
// This happens when the player chooses Change Ship/Change Block, so we // This happens when the player chooses Change Ship/Change Block, so we don't show an error message
// don't show an error message
} else { } else {
error_str = e.what(); error_str = e.what();
} }
+2 -3
View File
@@ -55,9 +55,8 @@ struct ProxySession {
std::shared_ptr<MapState> map_state; std::shared_ptr<MapState> map_state;
std::shared_ptr<const std::string> last_bin_contents; std::shared_ptr<const std::string> last_bin_contents;
std::shared_ptr<const std::string> last_dat_contents; std::shared_ptr<const std::string> last_dat_contents;
// Note: We intentionally don't use the client's item ID space because the // Note: We intentionally don't use the client's item ID space because the client may create items at the same time
// client may create items at the same time as the proxy, so server/client // as the proxy, so server/client state could go out of sync
// state could go out of sync
uint32_t next_item_id = 0x44000000; uint32_t next_item_id = 0x44000000;
struct PersistentConfig { struct PersistentConfig {
+75 -118
View File
@@ -22,8 +22,7 @@
using namespace std; using namespace std;
QuestCategoryIndex::Category::Category(uint32_t category_id, const phosg::JSON& json) QuestCategoryIndex::Category::Category(uint32_t category_id, const phosg::JSON& json) : category_id(category_id) {
: category_id(category_id) {
this->enabled_flags = json.get_int(0); this->enabled_flags = json.get_int(0);
this->directory_name = json.get_string(1); this->directory_name = json.get_string(1);
this->name = json.get_string(2); this->name = json.get_string(2);
@@ -46,9 +45,8 @@ shared_ptr<const QuestCategoryIndex::Category> QuestCategoryIndex::at(uint32_t c
template <bool BE> template <bool BE>
struct PSOMemCardDLQFileEncryptedHeaderT { struct PSOMemCardDLQFileEncryptedHeaderT {
U32T<BE> round2_seed; U32T<BE> round2_seed;
// To compute checksum, set checksum to zero, then compute the CRC32 of the // To compute checksum, set checksum to zero, then compute the CRC32 of the entire data section, including this
// entire data section, including this header struct (but not the unencrypted // header struct (but not the unencrypted header struct).
// header struct).
U32T<BE> checksum; U32T<BE> checksum;
le_uint32_t decompressed_size; le_uint32_t decompressed_size;
le_uint32_t round3_seed; le_uint32_t round3_seed;
@@ -67,15 +65,12 @@ string decrypt_download_quest_data_section(
size_t orig_size = decrypted.size(); size_t orig_size = decrypted.size();
decrypted.resize((decrypted.size() + 3) & (~3)); decrypted.resize((decrypted.size() + 3) & (~3));
// Note: Other PSO save files have the round 2 seed at the end of the data, // Other PSO save files have the round 2 seed at the end, not at the beginning. Presumably this is because the
// not at the beginning. Presumably they did this because the system, // system, character, and Guild Card files are a constant size, but download quest files can vary in size.
// character, and Guild Card files are a constant size, but download quest
// files can vary in size.
using HeaderT = PSOMemCardDLQFileEncryptedHeaderT<BE>; using HeaderT = PSOMemCardDLQFileEncryptedHeaderT<BE>;
auto* header = reinterpret_cast<HeaderT*>(decrypted.data()); auto* header = reinterpret_cast<HeaderT*>(decrypted.data());
PSOV2Encryption round2_crypt(header->round2_seed); PSOV2Encryption round2_crypt(header->round2_seed);
round2_crypt.encrypt_t<BE>( round2_crypt.encrypt_t<BE>(decrypted.data() + 4, (decrypted.size() - 4));
decrypted.data() + 4, (decrypted.size() - 4));
if (is_ep3_trial) { if (is_ep3_trial) {
phosg::StringReader r(decrypted); phosg::StringReader r(decrypted);
@@ -85,9 +80,8 @@ string decrypt_download_quest_data_section(
} }
r.skip(9); r.skip(9);
// Some Ep3 trial download quests don't have a stop opcode in the PRS // Some Ep3 trial download quests don't have a stop opcode in the PRS stream; it seems the client just
// stream; it seems the client just automatically stops when the correct // automatically stops when the correct amount of data has been produced. To handle this, we allow the PRS stream
// amount of data has been produced. To handle this, we allow the PRS stream
// to be unterminated here. // to be unterminated here.
size_t decompressed_size = prs_decompress_size( size_t decompressed_size = prs_decompress_size(
r.getv(r.remaining(), false), r.remaining(), sizeof(Episode3::MapDefinitionTrial), true); r.getv(r.remaining(), false), r.remaining(), sizeof(Episode3::MapDefinitionTrial), true);
@@ -100,8 +94,7 @@ string decrypt_download_quest_data_section(
} else { } else {
if (header->decompressed_size & 0xFFF00000) { if (header->decompressed_size & 0xFFF00000) {
throw runtime_error(std::format( throw runtime_error(std::format("decompressed_size too large ({:08X})", header->decompressed_size));
"decompressed_size too large ({:08X})", header->decompressed_size));
} }
if (!skip_checksum) { if (!skip_checksum) {
@@ -111,29 +104,23 @@ string decrypt_download_quest_data_section(
header->checksum = expected_crc; header->checksum = expected_crc;
if (expected_crc != actual_crc && expected_crc != phosg::bswap32(actual_crc)) { if (expected_crc != actual_crc && expected_crc != phosg::bswap32(actual_crc)) {
throw runtime_error(std::format( throw runtime_error(std::format(
"incorrect decrypted data section checksum: expected {:08X}; received {:08X}", "incorrect decrypted data section checksum: expected {:08X}; received {:08X}", expected_crc, actual_crc));
expected_crc, actual_crc));
} }
} }
// Unlike the above rounds, round 3 is always little-endian (it corresponds to // Unlike the above rounds, round 3 is always little-endian (it corresponds to the round of encryption done on the
// the round of encryption done on the server before sending the file to the // server before sending the file to the client in the first place)
// client in the first place)
PSOV2Encryption(header->round3_seed).decrypt(decrypted.data() + sizeof(HeaderT), decrypted.size() - sizeof(HeaderT)); PSOV2Encryption(header->round3_seed).decrypt(decrypted.data() + sizeof(HeaderT), decrypted.size() - sizeof(HeaderT));
decrypted.resize(orig_size); decrypted.resize(orig_size);
// Some download quest GCI files have decompressed_size fields that are 8 // Some download quest GCI files have decompressed_size fields that are 8 bytes smaller than the actual
// bytes smaller than the actual decompressed size of the data. They seem to // decompressed size of the data. They seem to work fine, so we accept both cases as correct.
// work fine, so we accept both cases as correct.
size_t decompressed_size = prs_decompress_size( size_t decompressed_size = prs_decompress_size(
decrypted.data() + sizeof(HeaderT), decrypted.data() + sizeof(HeaderT), decrypted.size() - sizeof(HeaderT));
decrypted.size() - sizeof(HeaderT));
size_t expected_decompressed_size = header->decompressed_size; size_t expected_decompressed_size = header->decompressed_size;
if ((decompressed_size != expected_decompressed_size) && if ((decompressed_size != expected_decompressed_size) && (decompressed_size != expected_decompressed_size - 8)) {
(decompressed_size != expected_decompressed_size - 8)) {
throw runtime_error(std::format( throw runtime_error(std::format(
"decompressed size ({}) does not match expected size ({})", "decompressed size ({}) does not match expected size ({})", decompressed_size, expected_decompressed_size));
decompressed_size, expected_decompressed_size));
} }
return decrypted.substr(sizeof(HeaderT)); return decrypted.substr(sizeof(HeaderT));
@@ -169,8 +156,7 @@ string find_seed_and_decrypt_download_quest_data_section(
string result; string result;
uint64_t result_seed = phosg::parallel_range_blocks<uint64_t>([&](uint64_t seed, size_t) { uint64_t result_seed = phosg::parallel_range_blocks<uint64_t>([&](uint64_t seed, size_t) {
try { try {
string ret = decrypt_download_quest_data_section<BE>( string ret = decrypt_download_quest_data_section<BE>(data_section, size, seed, skip_checksum, is_ep3_trial);
data_section, size, seed, skip_checksum, is_ep3_trial);
lock_guard<mutex> g(result_lock); lock_guard<mutex> g(result_lock);
result = std::move(ret); result = std::move(ret);
return true; return true;
@@ -300,8 +286,7 @@ string VersionedQuest::encode_qst() const {
return encode_qst_file(files, this->meta.name, this->meta.quest_number, xb_filename, this->meta.version, this->is_dlq_encoded); return encode_qst_file(files, this->meta.name, this->meta.quest_number, xb_filename, this->meta.version, this->is_dlq_encoded);
} }
Quest::Quest(shared_ptr<const VersionedQuest> initial_version) Quest::Quest(shared_ptr<const VersionedQuest> initial_version) : meta(initial_version->meta), supermap(nullptr) {
: meta(initial_version->meta), supermap(nullptr) {
this->add_version(initial_version); this->add_version(initial_version);
} }
@@ -320,10 +305,7 @@ phosg::JSON Quest::json() const {
})); }));
} }
return phosg::JSON::dict({ return phosg::JSON::dict({{"Metadata", this->meta.json()}, {"Versions", std::move(versions_json)}});
{"Metadata", this->meta.json()},
{"Versions", std::move(versions_json)},
});
} }
uint32_t Quest::versions_key(Version v, Language language) { uint32_t Quest::versions_key(Version v, Language language) {
@@ -430,9 +412,9 @@ shared_ptr<const VersionedQuest> Quest::version(Version v, Language language) co
return it->second; return it->second;
} }
QuestIndex::QuestIndex(const string& directory, shared_ptr<const QuestCategoryIndex> category_index, bool raise_on_any_failure) QuestIndex::QuestIndex(
: directory(directory), const string& directory, shared_ptr<const QuestCategoryIndex> category_index, bool raise_on_any_failure)
category_index(category_index) { : directory(directory), category_index(category_index) {
struct FileData { struct FileData {
string filename; string filename;
@@ -462,9 +444,8 @@ QuestIndex::QuestIndex(const string& directory, shared_ptr<const QuestCategoryIn
if (!files.emplace(basename, FileData{filename, data_ptr}).second) { if (!files.emplace(basename, FileData{filename, data_ptr}).second) {
throw runtime_error("file " + basename + " already exists"); throw runtime_error("file " + basename + " already exists");
} }
// There is a bug in the client that prevents quests from loading properly // There is a bug in the client that prevents quests from loading properly if any file's size is a multiple of
// if any file's size is a multiple of 0x400. See the comments on the 13 // 0x400. See the comments on the 13 command in CommandFormats.hh for more details.
// command in CommandFormats.hh for more details.
if (check_chunk_size && !(data_ptr->size() & 0x3FF)) { if (check_chunk_size && !(data_ptr->size() & 0x3FF)) {
data_ptr->push_back(0x00); data_ptr->push_back(0x00);
} }
@@ -587,9 +568,8 @@ QuestIndex::QuestIndex(const string& directory, shared_ptr<const QuestCategoryIn
} }
} }
// All quests have a bin file (even in Episode 3, though its format is // All quests have a bin file (even in Episode 3, though its format is different), so we use bin_files as the primary
// different), so we use bin_files as the primary list of all quests that // list of all quests that should be indexed
// should be indexed
unordered_map<const FileData*, shared_ptr<const phosg::JSON>> parsed_json_files; unordered_map<const FileData*, shared_ptr<const phosg::JSON>> parsed_json_files;
for (auto& [basename, entry] : bin_files) { for (auto& [basename, entry] : bin_files) {
try { try {
@@ -601,8 +581,7 @@ QuestIndex::QuestIndex(const string& directory, shared_ptr<const QuestCategoryIn
// VERS = PSO version that the quest is for (dc, pc, gc, etc.) // VERS = PSO version that the quest is for (dc, pc, gc, etc.)
// LANG = client language (j, e, g, f, s) // LANG = client language (j, e, g, f, s)
// EXT = file type (bin, bind, bin.dlq, qst, etc.) // EXT = file type (bin, bind, bin.dlq, qst, etc.)
// EXT has already been stripped off by the time we get here, so we just // EXT has already been stripped off by the time we get here, so we just parse the remaining fields.
// parse the remaining fields.
string quest_number_token, version_token, language_token; string quest_number_token, version_token, language_token;
{ {
vector<string> filename_tokens = phosg::split(basename, '-'); vector<string> filename_tokens = phosg::split(basename, '-');
@@ -653,8 +632,8 @@ QuestIndex::QuestIndex(const string& directory, shared_ptr<const QuestCategoryIn
auto bin_decompressed = prs_decompress(*entry.data); auto bin_decompressed = prs_decompress(*entry.data);
populate_quest_metadata_from_script(vq->meta, bin_decompressed.data(), bin_decompressed.size(), vq->meta.version, vq->meta.language); populate_quest_metadata_from_script(vq->meta, bin_decompressed.data(), bin_decompressed.size(), vq->meta.version, vq->meta.language);
// Find the corresponding dat and pvr files with the same basename as the // Find the corresponding dat and pvr files with the same basename as the bin file; if not found, look for them
// bin file; if not found, look for them without the language suffix // without the language suffix
const DATFileData* dat_filedata = nullptr; const DATFileData* dat_filedata = nullptr;
const FileData* pvr_filedata = nullptr; const FileData* pvr_filedata = nullptr;
try { try {
@@ -672,8 +651,7 @@ QuestIndex::QuestIndex(const string& directory, shared_ptr<const QuestCategoryIn
try { try {
pvr_filedata = &pvr_files.at(quest_number_token + "-" + version_token); pvr_filedata = &pvr_files.at(quest_number_token + "-" + version_token);
} catch (const out_of_range&) { } catch (const out_of_range&) {
// pvr files aren't required (and most quests do not have them), so // pvr files aren't required (and most quests do not have them), so don't fail if it's missing
// don't fail if it's missing
} }
} }
vq->bin_contents = entry.data; vq->bin_contents = entry.data;
@@ -798,10 +776,7 @@ shared_ptr<const Quest> QuestIndex::get(const std::string& name) const {
} }
vector<shared_ptr<const QuestCategoryIndex::Category>> QuestIndex::categories( vector<shared_ptr<const QuestCategoryIndex::Category>> QuestIndex::categories(
QuestMenuType menu_type, QuestMenuType menu_type, Episode episode, uint16_t version_flags, IncludeCondition include_condition) const {
Episode episode,
uint16_t version_flags,
IncludeCondition include_condition) const {
vector<shared_ptr<const QuestCategoryIndex::Category>> ret; vector<shared_ptr<const QuestCategoryIndex::Category>> ret;
for (const auto& cat : this->category_index->categories) { for (const auto& cat : this->category_index->categories) {
if (cat->check_flag(menu_type) && !this->filter(episode, version_flags, cat->category_id, include_condition, 1).empty()) { if (cat->check_flag(menu_type) && !this->filter(episode, version_flags, cat->category_id, include_condition, 1).empty()) {
@@ -852,9 +827,8 @@ vector<pair<QuestIndex::IncludeState, shared_ptr<const Quest>>> QuestIndex::filt
} }
string encode_download_quest_data(const string& compressed_data, size_t decompressed_size, uint32_t encryption_seed) { string encode_download_quest_data(const string& compressed_data, size_t decompressed_size, uint32_t encryption_seed) {
// Download quest files are like normal (PRS-compressed) quest files, but they // Download quest files are like normal (PRS-compressed) quest files, but they are encrypted with PSO V2 encryption
// are encrypted with PSO V2 encryption (even on V3 / PSO GC), and a small // (even on V3 / PSO GC), and a small header (PSODownloadQuestHeader) is prepended to the encrypted data.
// header (PSODownloadQuestHeader) is prepended to the encrypted data.
if (encryption_seed == 0) { if (encryption_seed == 0) {
encryption_seed = phosg::random_object<uint32_t>(); encryption_seed = phosg::random_object<uint32_t>();
@@ -869,8 +843,7 @@ string encode_download_quest_data(const string& compressed_data, size_t decompre
header->encryption_seed = encryption_seed; header->encryption_seed = encryption_seed;
data += compressed_data; data += compressed_data;
// Add temporary extra bytes if necessary so encryption won't fail - the data // Add extra bytes if necessary so encryption won't fail; the data size must be a multiple of 4 for PSO V2 encryption
// size must be a multiple of 4 for PSO V2 encryption.
size_t original_size = data.size(); size_t original_size = data.size();
data.resize((data.size() + 3) & (~3)); data.resize((data.size() + 3) & (~3));
@@ -882,9 +855,8 @@ string encode_download_quest_data(const string& compressed_data, size_t decompre
} }
shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(Language override_language) const { shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(Language override_language) const {
// The download flag needs to be set in the bin header, or else the client // The download flag needs to be set in the bin header, or else the client will ignore it when scanning for download
// will ignore it when scanning for download quests in an offline game. To set // quests in an offline game. To set this flag, we need to decompress the quest's .bin file, set the flag, then
// this flag, we need to decompress the quest's .bin file, set the flag, then
// recompress it again. // recompress it again.
string decompressed_bin = prs_decompress(*this->bin_contents); string decompressed_bin = prs_decompress(*this->bin_contents);
@@ -934,8 +906,7 @@ shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(Language overri
string compressed_bin = prs_compress(decompressed_bin); string compressed_bin = prs_compress(decompressed_bin);
// Return a new VersionedQuest object with appropriately-processed .bin and // Return a new VersionedQuest object with appropriately-processed .bin and .dat file contents
// .dat file contents
auto dlq = make_shared<VersionedQuest>(*this); auto dlq = make_shared<VersionedQuest>(*this);
dlq->bin_contents = make_shared<string>(encode_download_quest_data(compressed_bin, decompressed_bin.size())); dlq->bin_contents = make_shared<string>(encode_download_quest_data(compressed_bin, decompressed_bin.size()));
dlq->dat_contents = make_shared<string>(encode_download_quest_data(*this->dat_contents)); dlq->dat_contents = make_shared<string>(encode_download_quest_data(*this->dat_contents));
@@ -944,20 +915,15 @@ shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(Language overri
return dlq; return dlq;
} }
string decode_gci_data( string decode_gci_data(const string& data, ssize_t find_seed_num_threads, int64_t known_seed, bool skip_checksum) {
const string& data,
ssize_t find_seed_num_threads,
int64_t known_seed,
bool skip_checksum) {
phosg::StringReader r(data); phosg::StringReader r(data);
const auto& header = r.get<PSOGCIFileHeader>(); const auto& header = r.get<PSOGCIFileHeader>();
header.check(); header.check();
if (header.is_ep12()) { if (header.is_ep12()) {
const auto& dlq_header = r.get<PSOGCIDLQFileEncryptedHeader>(false); const auto& dlq_header = r.get<PSOGCIDLQFileEncryptedHeader>(false);
// Unencrypted GCI files appear to always have zeroes in these fields. // Unencrypted GCI files appear to always have zeroes in these fields. Encrypted GCI files are highly unlikely to
// Encrypted GCI files are highly unlikely to have zeroes in ALL of these // have zeroes in ALL of these fields, so assume it's encrypted if any of them are nonzero.
// fields, so assume it's encrypted if any of them are nonzero.
if (dlq_header.round2_seed || dlq_header.checksum || dlq_header.round3_seed) { if (dlq_header.round2_seed || dlq_header.checksum || dlq_header.round3_seed) {
if (known_seed >= 0) { if (known_seed >= 0) {
return decrypt_download_quest_data_section<true>( return decrypt_download_quest_data_section<true>(
@@ -1010,14 +976,13 @@ string decode_gci_data(
} }
} else { } else {
// The first 0x10 bytes in the data segment appear to be unused. In most // The first 0x10 bytes in the data segment appear to be unused. In most files I've seen, the last half of it (8
// files I've seen, the last half of it (8 bytes) are duplicates of the // bytes) are duplicates of the first 8 bytes of the unscrambled, compressed data, though this is the result of
// first 8 bytes of the unscrambled, compressed data, though this is the // an uninitialized memory bug when the client encodes the file and not an actual constraint on what should be in
// result of an uninitialized memory bug when the client encodes the file // these 8 bytes.
// and not an actual constraint on what should be in these 8 bytes.
r.skip(16); r.skip(16);
// The game treats this field as a 16-byte string (including the \0). The 8 // The game treats this field as a 16-byte string (including the \0). The 8 bytes after it appear to be
// bytes after it appear to be completely unused. // completely unused.
if (r.readx(15) != "SONICTEAM,SEGA.") { if (r.readx(15) != "SONICTEAM,SEGA.") {
throw runtime_error("Episode 3 GCI file is not a quest"); throw runtime_error("Episode 3 GCI file is not a quest");
} }
@@ -1025,9 +990,8 @@ string decode_gci_data(
string decrypted = r.readx(header.data_size - 40); string decrypted = r.readx(header.data_size - 40);
// For some reason, Sega decided not to encrypt Episode 3 quest files in the // For some reason, Sega decided not to encrypt Episode 3 quest files in the same way as Episodes 1&2 quest files
// same way as Episodes 1&2 quest files (see above). Instead, they just // (see above). Instead, they just wrote a fairly trivial XOR loop over the first 0x100 bytes, leaving the
// wrote a fairly trivial XOR loop over the first 0x100 bytes, leaving the
// remaining bytes completely unencrypted (but still compressed). // remaining bytes completely unencrypted (but still compressed).
size_t unscramble_size = min<size_t>(0x100, data.size()); size_t unscramble_size = min<size_t>(0x100, data.size());
decrypt_trivial_gci_data(decrypted.data(), unscramble_size, 0); decrypt_trivial_gci_data(decrypted.data(), unscramble_size, 0);
@@ -1046,11 +1010,7 @@ string decode_gci_data(
} }
} }
string decode_vms_data( string decode_vms_data(const string& data, ssize_t find_seed_num_threads, int64_t known_seed, bool skip_checksum) {
const string& data,
ssize_t find_seed_num_threads,
int64_t known_seed,
bool skip_checksum) {
phosg::StringReader r(data); phosg::StringReader r(data);
const auto& header = r.get<PSOVMSFileHeader>(); const auto& header = r.get<PSOVMSFileHeader>();
if (!header.checksum_correct()) { if (!header.checksum_correct()) {
@@ -1065,8 +1025,7 @@ string decode_vms_data(
} }
if (known_seed >= 0) { if (known_seed >= 0) {
return decrypt_download_quest_data_section<false>( return decrypt_download_quest_data_section<false>(data_section, header.data_size, known_seed);
data_section, header.data_size, known_seed);
} else { } else {
if (find_seed_num_threads < 0) { if (find_seed_num_threads < 0) {
@@ -1085,10 +1044,9 @@ string decode_dlq_data(const string& data) {
uint32_t decompressed_size = r.get_u32l(); uint32_t decompressed_size = r.get_u32l();
uint32_t key = r.get_u32l(); uint32_t key = r.get_u32l();
// The compressed data size does not need to be a multiple of 4, but the V2 // The compressed data size does not need to be a multiple of 4, but the V2 encryption (which is used for all
// encryption (which is used for all download quests, even in V3) requires the // download quests, even in V3) requires the data size to be a multiple of 4. We'll just temporarily stick a few
// data size to be a multiple of 4. We'll just temporarily stick a few bytes // bytes on the end, then throw them away later if needed.
// on the end, then throw them away later if needed.
string decrypted = r.read(r.remaining()); string decrypted = r.read(r.remaining());
PSOV2Encryption encr(key); PSOV2Encryption encr(key);
size_t original_size = data.size(); size_t original_size = data.size();
@@ -1150,9 +1108,8 @@ static unordered_map<string, string> decode_qst_data_t(const string& data) {
} }
} else if (header.command == 0x13 || header.command == 0xA7) { } else if (header.command == 0x13 || header.command == 0xA7) {
// We have to allow larger commands here, because it seems some tools // We have to allow larger commands here, because it seems some tools encoded QST files with BB's extra 4 padding
// encoded QST files with BB's extra 4 padding bytes included in the // bytes included in the command size.
// command size.
if (header.size < sizeof(HeaderT) + sizeof(S_WriteFile_13_A7)) { if (header.size < sizeof(HeaderT) + sizeof(S_WriteFile_13_A7)) {
throw runtime_error("qst write file command has incorrect size"); throw runtime_error("qst write file command has incorrect size");
} }
@@ -1195,9 +1152,8 @@ static unordered_map<string, string> decode_qst_data_t(const string& data) {
} }
unordered_map<string, string> decode_qst_data(const string& data) { unordered_map<string, string> decode_qst_data(const string& data) {
// QST files start with an open file command, but the format differs depending // QST files start with an open file command, but the format differs depending on the PSO version that the qst file
// on the PSO version that the qst file is for. We can detect the format from // is for. We can detect the format from 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/GC: 44 ?? 3C 00 or A6 ?? 3C 00 // - DC/GC: 44 ?? 3C 00 or A6 ?? 3C 00
@@ -1209,10 +1165,9 @@ unordered_map<string, string> decode_qst_data(const string& 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
// the filename was shifted over by one byte. To detect this, we check if // byte. To detect this, we check if the V3 type field has a reasonable value, and if not, we assume the file is
// the V3 type field has a reasonable value, and if not, we assume the file // for PSO DC.
// is for PSO DC.
if (r.pget_u16l(sizeof(PSOCommandHeaderDCV3) + offsetof(S_OpenFile_PC_GC_44_A6, type)) > 3) { if (r.pget_u16l(sizeof(PSOCommandHeaderDCV3) + offsetof(S_OpenFile_PC_GC_44_A6, type)) > 3) {
return decode_qst_data_t<PSOCommandHeaderDCV3, S_OpenFile_DC_44_A6>(data); return decode_qst_data_t<PSOCommandHeaderDCV3, S_OpenFile_DC_44_A6>(data);
} else { } else {
@@ -1249,8 +1204,7 @@ void add_open_file_command_t(
cmd.filename.encode(filename); cmd.filename.encode(filename);
cmd.type = 0; cmd.type = 0;
cmd.file_size = file_size; cmd.file_size = file_size;
// TODO: It'd be nice to have something like w.emplace(...) to avoid copying // TODO: It'd be nice to have something like w.emplace(...) to avoid copying the structs into the StringWriter.
// the command structs into the StringWriter.
w.put(cmd); w.put(cmd);
} }
@@ -1289,9 +1243,8 @@ void add_write_file_commands_t(
memcpy(cmd.data.data(), &data[z], chunk_size); memcpy(cmd.data.data(), &data[z], chunk_size);
cmd.data_size = chunk_size; cmd.data_size = chunk_size;
w.put(cmd); w.put(cmd);
// On BB, the write file command size is a multiple of 4 but not a multiple // On BB, the write file command size is a multiple of 4 but not a multiple of 8; in QST format the implicit extra
// of 8; in QST format the implicit extra 4 bytes are apparently stored in // 4 bytes are apparently stored in the file.
// the file.
if (bb_alignment) { if (bb_alignment) {
w.put_u32(0); w.put_u32(0);
} }
@@ -1307,15 +1260,15 @@ string encode_qst_file(
bool is_dlq_encoded) { bool is_dlq_encoded) {
phosg::StringWriter w; phosg::StringWriter w;
// 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.
switch (version) { switch (version) {
case Version::DC_NTE: // DC NTE doesn't support quests, but we support encoding QST files anyway case Version::DC_NTE: // DC NTE doesn't support quests, but we support encoding QST files anyway
case Version::DC_11_2000: case Version::DC_11_2000:
case Version::DC_V1: case Version::DC_V1:
case Version::DC_V2: case Version::DC_V2:
for (const auto& it : files) { for (const auto& it : files) {
add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_DC_44_A6>(w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded); add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_DC_44_A6>(
w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded);
} }
for (const auto& it : files) { for (const auto& it : files) {
add_write_file_commands_t<PSOCommandHeaderDCV3>(w, it.first, *it.second, is_dlq_encoded, false); add_write_file_commands_t<PSOCommandHeaderDCV3>(w, it.first, *it.second, is_dlq_encoded, false);
@@ -1324,7 +1277,8 @@ string encode_qst_file(
case Version::PC_NTE: case Version::PC_NTE:
case Version::PC_V2: case Version::PC_V2:
for (const auto& it : files) { for (const auto& it : files) {
add_open_file_command_t<PSOCommandHeaderPC, S_OpenFile_PC_GC_44_A6>(w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded); add_open_file_command_t<PSOCommandHeaderPC, S_OpenFile_PC_GC_44_A6>(
w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded);
} }
for (const auto& it : files) { for (const auto& it : files) {
add_write_file_commands_t<PSOCommandHeaderPC>(w, it.first, *it.second, is_dlq_encoded, false); add_write_file_commands_t<PSOCommandHeaderPC>(w, it.first, *it.second, is_dlq_encoded, false);
@@ -1335,7 +1289,8 @@ string encode_qst_file(
case Version::GC_EP3_NTE: case Version::GC_EP3_NTE:
case Version::GC_EP3: case Version::GC_EP3:
for (const auto& it : files) { for (const auto& it : files) {
add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_PC_GC_44_A6>(w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded); add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_PC_GC_44_A6>(
w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded);
} }
for (const auto& it : files) { for (const auto& it : files) {
add_write_file_commands_t<PSOCommandHeaderDCV3>(w, it.first, *it.second, is_dlq_encoded, false); add_write_file_commands_t<PSOCommandHeaderDCV3>(w, it.first, *it.second, is_dlq_encoded, false);
@@ -1343,7 +1298,8 @@ string encode_qst_file(
break; break;
case Version::XB_V3: case Version::XB_V3:
for (const auto& it : files) { for (const auto& it : files) {
add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_XB_44_A6>(w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded); add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_XB_44_A6>(
w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded);
} }
for (const auto& it : files) { for (const auto& it : files) {
add_write_file_commands_t<PSOCommandHeaderDCV3>(w, it.first, *it.second, is_dlq_encoded, false); add_write_file_commands_t<PSOCommandHeaderDCV3>(w, it.first, *it.second, is_dlq_encoded, false);
@@ -1351,7 +1307,8 @@ string encode_qst_file(
break; break;
case Version::BB_V4: case Version::BB_V4:
for (const auto& it : files) { for (const auto& it : files) {
add_open_file_command_t<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded); add_open_file_command_t<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(
w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded);
} }
for (const auto& it : files) { for (const auto& it : files) {
add_write_file_commands_t<PSOCommandHeaderBB>(w, it.first, *it.second, is_dlq_encoded, true); add_write_file_commands_t<PSOCommandHeaderBB>(w, it.first, *it.second, is_dlq_encoded, true);
+5 -13
View File
@@ -69,8 +69,8 @@ struct QuestCategoryIndex {
struct VersionedQuest { struct VersionedQuest {
QuestMetadata meta; QuestMetadata meta;
// Most of these default values are intentionally invalid; we use these // Most of these default values are intentionally invalid; we use these values to check if each field was parsed
// values to check if each field was parsed during quest indexing. // during quest indexing.
std::shared_ptr<const std::string> bin_contents; std::shared_ptr<const std::string> bin_contents;
std::shared_ptr<const std::string> dat_contents; std::shared_ptr<const std::string> dat_contents;
std::shared_ptr<const MapFile> map_file; std::shared_ptr<const MapFile> map_file;
@@ -151,20 +151,12 @@ struct QuestIndex {
}; };
std::string encode_download_quest_data( std::string encode_download_quest_data(
const std::string& compressed_data, const std::string& compressed_data, size_t decompressed_size = 0, uint32_t encryption_seed = 0);
size_t decompressed_size = 0,
uint32_t encryption_seed = 0);
std::string decode_gci_data( std::string decode_gci_data(
const std::string& data, const std::string& data, ssize_t find_seed_num_threads = -1, int64_t known_seed = -1, bool skip_checksum = false);
ssize_t find_seed_num_threads = -1,
int64_t known_seed = -1,
bool skip_checksum = false);
std::string decode_vms_data( std::string decode_vms_data(
const std::string& data, const std::string& data, ssize_t find_seed_num_threads = -1, int64_t known_seed = -1, bool skip_checksum = false);
ssize_t find_seed_num_threads = -1,
int64_t known_seed = -1,
bool skip_checksum = false);
std::string decode_dlq_data(const std::string& data); std::string decode_dlq_data(const std::string& data);
std::unordered_map<std::string, std::string> decode_qst_data(const std::string& data); std::unordered_map<std::string, std::string> decode_qst_data(const std::string& data);
+17 -16
View File
@@ -13,7 +13,8 @@ phosg::JSON QuestMetadata::FloorAssignment::json() const {
} }
std::string QuestMetadata::FloorAssignment::str() const { std::string QuestMetadata::FloorAssignment::str() const {
return std::format("FloorAssignment(floor=0x{:02X}, area=0x{:02X}, type=0x{:02X}, layout_var=0x{:02X}, entities_var=0x{:02X})", return std::format(
"FloorAssignment(floor=0x{:02X}, area=0x{:02X}, type=0x{:02X}, layout_var=0x{:02X}, entities_var=0x{:02X})",
this->floor, this->area, this->type, this->layout_var, this->entities_var); this->floor, this->area, this->type, this->layout_var, this->entities_var);
} }
@@ -82,8 +83,7 @@ void QuestMetadata::assign_default_floors() {
void QuestMetadata::assert_compatible(const QuestMetadata& other) const { void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
if (this->quest_number != other.quest_number) { if (this->quest_number != other.quest_number) {
throw logic_error(std::format( throw logic_error(std::format(
"incorrect versioned quest number (existing: {:08X}, new: {:08X})", "incorrect versioned quest number (existing: {:08X}, new: {:08X})", this->quest_number, other.quest_number));
this->quest_number, other.quest_number));
} }
if (this->category_id != other.category_id) { if (this->category_id != other.category_id) {
throw runtime_error(std::format( throw runtime_error(std::format(
@@ -98,7 +98,8 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
if (this->allow_start_from_chat_command != other.allow_start_from_chat_command) { if (this->allow_start_from_chat_command != other.allow_start_from_chat_command) {
throw runtime_error(std::format( throw runtime_error(std::format(
"quest version has a different allow_start_from_chat_command state (existing: {}, new: {})", "quest version has a different allow_start_from_chat_command state (existing: {}, new: {})",
this->allow_start_from_chat_command ? "true" : "false", other.allow_start_from_chat_command ? "true" : "false")); this->allow_start_from_chat_command ? "true" : "false",
other.allow_start_from_chat_command ? "true" : "false"));
} }
if (this->joinable != other.joinable) { if (this->joinable != other.joinable) {
throw runtime_error(std::format( throw runtime_error(std::format(
@@ -109,7 +110,8 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
bool other_has_player_limit = (other.max_players != 0) && (other.max_players != 4); bool other_has_player_limit = (other.max_players != 0) && (other.max_players != 4);
if ((this_has_player_limit || other_has_player_limit) && (this->max_players != other.max_players)) { if ((this_has_player_limit || other_has_player_limit) && (this->max_players != other.max_players)) {
throw runtime_error(std::format( throw runtime_error(std::format(
"quest version has a different maximum player count (existing: {}, new: {})", this->max_players, other.max_players)); "quest version has a different maximum player count (existing: {}, new: {})",
this->max_players, other.max_players));
} }
if (this->lock_status_register != other.lock_status_register) { if (this->lock_status_register != other.lock_status_register) {
throw runtime_error(std::format( throw runtime_error(std::format(
@@ -122,8 +124,7 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
if (this->solo_unlock_flags != other.solo_unlock_flags) { if (this->solo_unlock_flags != other.solo_unlock_flags) {
throw runtime_error(std::format("quest version has a different set of solo unlock flags")); throw runtime_error(std::format("quest version has a different set of solo unlock flags"));
} }
if (!this->create_item_mask_entries.empty() && if (!this->create_item_mask_entries.empty() && !other.create_item_mask_entries.empty() &&
!other.create_item_mask_entries.empty() &&
this->create_item_mask_entries != other.create_item_mask_entries) { this->create_item_mask_entries != other.create_item_mask_entries) {
string this_str, other_str; string this_str, other_str;
for (const auto& item : this->create_item_mask_entries) { for (const auto& item : this->create_item_mask_entries) {
@@ -150,8 +151,7 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
string existing_str = this->battle_rules->json().serialize(); string existing_str = this->battle_rules->json().serialize();
string new_str = other.battle_rules->json().serialize(); string new_str = other.battle_rules->json().serialize();
throw runtime_error(std::format( throw runtime_error(std::format(
"quest version has different battle rules (existing: {}, new: {})", "quest version has different battle rules (existing: {}, new: {})", existing_str, new_str));
existing_str, new_str));
} }
if (this->challenge_template_index != other.challenge_template_index) { if (this->challenge_template_index != other.challenge_template_index) {
throw runtime_error(std::format( throw runtime_error(std::format(
@@ -173,7 +173,8 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
const auto& other_fa = other.floor_assignments[z]; const auto& other_fa = other.floor_assignments[z];
if (this_fa.area != other_fa.area) { if (this_fa.area != other_fa.area) {
throw runtime_error(std::format( throw runtime_error(std::format(
"quest version has different area on floor 0x{:02X} (existing: {}, new: {})", z, this_fa.str(), other_fa.str())); "quest version has different area on floor 0x{:02X} (existing: {}, new: {})",
z, this_fa.str(), other_fa.str()));
} }
} }
if (this->description_flag != other.description_flag) { if (this->description_flag != other.description_flag) {
@@ -190,8 +191,7 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
string existing_str = this->available_expression->str(); string existing_str = this->available_expression->str();
string new_str = other.available_expression->str(); string new_str = other.available_expression->str();
throw runtime_error(std::format( throw runtime_error(std::format(
"quest version has a different available expression (existing: {}, new: {})", "quest version has a different available expression (existing: {}, new: {})", existing_str, new_str));
existing_str, new_str));
} }
if (!this->enabled_expression != !other.enabled_expression) { if (!this->enabled_expression != !other.enabled_expression) {
throw runtime_error(std::format( throw runtime_error(std::format(
@@ -202,8 +202,7 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
string existing_str = this->enabled_expression->str(); string existing_str = this->enabled_expression->str();
string new_str = other.enabled_expression->str(); string new_str = other.enabled_expression->str();
throw runtime_error(std::format( throw runtime_error(std::format(
"quest version has a different enabled expression (existing: {}, new: {})", "quest version has a different enabled expression (existing: {}, new: {})", existing_str, new_str));
existing_str, new_str));
} }
if (this->common_item_set_name != other.common_item_set_name) { if (this->common_item_set_name != other.common_item_set_name) {
throw runtime_error(std::format( throw runtime_error(std::format(
@@ -224,7 +223,8 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
phosg::name_for_enum(this->default_drop_mode), phosg::name_for_enum(other.default_drop_mode))); phosg::name_for_enum(this->default_drop_mode), phosg::name_for_enum(other.default_drop_mode)));
} }
if (this->enable_schtserv_commands != other.enable_schtserv_commands) { if (this->enable_schtserv_commands != other.enable_schtserv_commands) {
throw runtime_error(format("quest version has different value for enable_schtserv_commands (existing: {}, new: {})", throw runtime_error(format(
"quest version has different value for enable_schtserv_commands (existing: {}, new: {})",
this->enable_schtserv_commands ? "true" : "false", other.enable_schtserv_commands ? "true" : "false")); this->enable_schtserv_commands ? "true" : "false", other.enable_schtserv_commands ? "true" : "false"));
} }
} }
@@ -239,7 +239,8 @@ phosg::JSON QuestMetadata::json() const {
auto difficulty = static_cast<Difficulty>((key >> 24) & 3); auto difficulty = static_cast<Difficulty>((key >> 24) & 3);
auto floor = static_cast<uint8_t>((key >> 16) & 0xFF); auto floor = static_cast<uint8_t>((key >> 16) & 0xFF);
auto enemy_type = static_cast<EnemyType>(key & 0xFFFF); auto enemy_type = static_cast<EnemyType>(key & 0xFFFF);
auto key_str = std::format("{}:0x{:02X}:{}", name_for_difficulty(difficulty), floor, phosg::name_for_enum(enemy_type)); auto key_str = std::format(
"{}:0x{:02X}:{}", name_for_difficulty(difficulty), floor, phosg::name_for_enum(enemy_type));
enemy_exp_overrides_json.emplace(key_str, exp_override); enemy_exp_overrides_json.emplace(key_str, exp_override);
} }
+3 -4
View File
@@ -17,10 +17,9 @@
#include "StaticGameData.hh" #include "StaticGameData.hh"
struct QuestMetadata { struct QuestMetadata {
// This structure contains configuration that should be the same across all // This structure contains configuration that should be the same across all versions of the quest, except for the
// versions of the quest, except for the name and description strings. This // name and description strings. This is used in both the Quest and VersionedQuest structures; in Quest, the name and
// is used in both the Quest and VersionedQuest structures; in Quest, the // description are used only internally.
// name and description are used only internally.
Version version; Version version;
Language language; Language language;
+995 -1285
View File
File diff suppressed because it is too large Load Diff
+6 -8
View File
@@ -70,10 +70,9 @@ struct PSOQuestHeaderV3 {
/* 000E */ le_uint16_t unknown_a2 = 0xFFFF; /* 000E */ le_uint16_t unknown_a2 = 0xFFFF;
/* 0010 */ Language language = Language::JAPANESE; /* 0010 */ Language language = Language::JAPANESE;
/* 0011 */ uint8_t unknown_a3 = 0; /* 0011 */ uint8_t unknown_a3 = 0;
// Note: The GC client byteswaps this field, then loads it as a byte, so // Note: The GC client byteswaps this field, then loads it as a byte, so technically the high byte of this is what
// technically the high byte of this is what the client uses as the quest // the client uses as the quest number. In practice, this only matters if the quest runs send_statistic without
// number. In practice, this only matters if the quest runs send_statistic // running prepare_statistic first, which is not the intended usage.
// without running prepare_statistic first, which is not the intended usage.
/* 0012 */ le_uint16_t quest_number = 0; /* 0012 */ le_uint16_t quest_number = 0;
/* 0014 */ pstring<TextEncoding::MARKED, 0x20> name; /* 0014 */ pstring<TextEncoding::MARKED, 0x20> name;
/* 0034 */ pstring<TextEncoding::MARKED, 0x80> short_description; /* 0034 */ pstring<TextEncoding::MARKED, 0x80> short_description;
@@ -95,8 +94,7 @@ struct CreateItemMaskEntry {
operator QuestMetadata::CreateItemMask() const; operator QuestMetadata::CreateItemMask() const;
} __packed_ws__(CreateItemMaskEntry, 0x38); } __packed_ws__(CreateItemMaskEntry, 0x38);
// Some quest authoring tools don't generate the full quest header, hence the // Some quest authoring tools don't generate the full quest header, hence the split structure here.
// split structure here.
struct PSOQuestHeaderBBBase { struct PSOQuestHeaderBBBase {
/* 0000 */ le_uint32_t text_offset = 0; /* 0000 */ le_uint32_t text_offset = 0;
/* 0004 */ le_uint32_t label_table_offset = 0; /* 0004 */ le_uint32_t label_table_offset = 0;
@@ -117,8 +115,8 @@ struct PSOQuestHeaderBBBase {
struct PSOQuestHeaderBB : PSOQuestHeaderBBBase { struct PSOQuestHeaderBB : PSOQuestHeaderBBBase {
struct FloorAssignment { struct FloorAssignment {
// These fields match the bb_map_designate arguments (see QuestScript.cc). // These fields match the bb_map_designate arguments (see QuestScript.cc). Unused AreaAssignment structures should
// Unused AreaAssignment structures should have all fields set to 0xFF. // have all fields set to 0xFF.
uint8_t floor = 0xFF; uint8_t floor = 0xFF;
uint8_t area = 0xFF; uint8_t area = 0xFF;
uint8_t type = 0xFF; uint8_t type = 0xFF;
+8 -13
View File
@@ -32,10 +32,9 @@ uint32_t RareItemSet::expand_rate(uint8_t pc) {
// pc = bits SSSSSVVV // pc = bits SSSSSVVV
// shift = S - 4 (so shift is 0-27) // shift = S - 4 (so shift is 0-27)
// value = V + 7 (so value is 7-14) // value = V + 7 (so value is 7-14)
// Then, take the value 0x00000002, shift it left by shift (0-27), and // Then, take the value 0x00000002, shift it left by shift (0-27), and multiply the result by value (7-14) to get the
// multiply the result by value (7-14) to get the actual drop rate. The result // actual drop rate. The result is a probability out of 0xFFFFFFFF (so 0x40000000 means the item will drop 25% of the
// is a probability out of 0xFFFFFFFF (so 0x40000000 means the item will drop // time, for example).
// 25% of the time, for example).
int8_t shift = ((pc >> 3) & 0x1F) - 4; int8_t shift = ((pc >> 3) & 0x1F) - 4;
if (shift < 0) { if (shift < 0) {
shift = 0; shift = 0;
@@ -44,8 +43,7 @@ uint32_t RareItemSet::expand_rate(uint8_t pc) {
} }
uint8_t RareItemSet::compress_rate(uint32_t probability) { uint8_t RareItemSet::compress_rate(uint32_t probability) {
// I'm too lazy to figure out the reverse bitwise math, so we just compute all // I'm too lazy to figure out the reverse math, so we just compute all the expansions and take the closest one
// the expansions and take the closest one
static std::map<uint32_t, uint8_t> inverse_map; static std::map<uint32_t, uint8_t> inverse_map;
if (inverse_map.empty()) { if (inverse_map.empty()) {
for (size_t z = 0; z < 0x100; z++) { for (size_t z = 0; z < 0x100; z++) {
@@ -269,8 +267,7 @@ RareItemSet::RareItemSet(const GSLArchive& gsl, bool is_big_endian) {
string filename = this->gsl_entry_name_for_table(mode, episode, difficulty, section_id); string filename = this->gsl_entry_name_for_table(mode, episode, difficulty, section_id);
ParsedRELData rel(gsl.get_reader(filename), is_big_endian, false); ParsedRELData rel(gsl.get_reader(filename), is_big_endian, false);
this->collections.emplace( this->collections.emplace(
this->key_for_params(mode, episode, difficulty, section_id), this->key_for_params(mode, episode, difficulty, section_id), rel.as_collection());
rel.as_collection());
} catch (const out_of_range&) { } catch (const out_of_range&) {
} }
} }
@@ -290,8 +287,7 @@ RareItemSet::RareItemSet(const string& rel_data, bool is_big_endian) {
size_t index = (ep_index * 40) + static_cast<size_t>(difficulty) * 10 + section_id; size_t index = (ep_index * 40) + static_cast<size_t>(difficulty) * 10 + section_id;
ParsedRELData rel(r.sub(0x280 * index, 0x280), is_big_endian, false); ParsedRELData rel(r.sub(0x280 * index, 0x280), is_big_endian, false);
this->collections.emplace( this->collections.emplace(
this->key_for_params(GameMode::NORMAL, episode, difficulty, section_id), this->key_for_params(GameMode::NORMAL, episode, difficulty, section_id), rel.as_collection());
rel.as_collection());
} catch (const out_of_range&) { } catch (const out_of_range&) {
} }
} }
@@ -715,9 +711,8 @@ string RareItemSet::serialize_html(
ItemData example_item = spec.data; ItemData example_item = spec.data;
if (example_item.can_be_encoded_in_rel_rare_table()) { if (example_item.can_be_encoded_in_rel_rare_table()) {
// Apparently Return to Ragol has a patch that allows it to use the // Apparently Return to Ragol has a patch that allows it to use the value 5 in data1[0] to specify a specific
// value 5 in data1[0] to specify a specific tech disk, so we handle // tech disk, so we handle that here.
// that here.
if (example_item.data1[0] == 5) { if (example_item.data1[0] == 5) {
example_item.data1[4] = example_item.data1[1]; example_item.data1[4] = example_item.data1[1];
example_item.data1[0] = 0x03; example_item.data1[0] = 0x03;
+198 -296
View File
File diff suppressed because it is too large Load Diff
+179 -264
View File
@@ -23,6 +23,8 @@
using namespace std; using namespace std;
// The functions in this file are called when a client sends a game command (60, 62, 6C, 6D, C9, or CB).
struct SubcommandMessage { struct SubcommandMessage {
uint16_t command; uint16_t command;
uint32_t flag; uint32_t flag;
@@ -128,22 +130,6 @@ uint8_t translate_subcommand_number(Version to_version, Version from_version, ui
} }
} }
// The functions in this file are called when a client sends a game command
// (60, 62, 6C, 6D, C9, or CB).
// There are three different sets of subcommand numbers: the DC NTE set, the
// November 2000 prototype set, and the set used by all other versions of the
// game (starting from the December 2000 prototype, all the way through BB).
// Currently we do not support the November 2000 prototype, but we do support
// DC NTE. In general, DC NTE clients can only interact with non-NTE players in
// very limited ways, since most subcommand-based actions take place in games,
// and non-NTE players cannot join NTE games. Commands sent by DC NTE clients
// are not handled by the functions defined in subcommand_handlers, but are
// instead handled by handle_subcommand_dc_nte. This means we only have to
// consider sending to DC NTE clients in a small subset of the command handlers
// (those that can occur in the lobby), and we can skip sending most
// subcommands to DC NTE by default.
bool command_is_private(uint8_t command) { bool command_is_private(uint8_t command) {
return (command == 0x62) || (command == 0x6D); return (command == 0x62) || (command == 0x6D);
} }
@@ -263,11 +249,9 @@ static void forward_subcommand(shared_ptr<Client> c, SubcommandMessage& msg) {
} }
} }
// Before battle, forward only chat commands to watcher lobbies; during // Before battle, forward only chat commands to watcher lobbies; during battle, forward everything to watcher
// battle, forward everything to watcher lobbies. (This is necessary because // lobbies. (This is necessary because if we forward everything before battle, the blocking menu subcommands cause
// if we forward everything before battle, the blocking menu subcommands // the battle setup menu to appear in the spectator room, which looks weird and is generally undesirable.)
// cause the battle setup menu to appear in the spectator room, which looks
// weird and is generally undesirable.)
if ((l->ep3_server && (l->ep3_server->setup_phase != Episode3::SetupPhase::REGISTRATION)) || if ((l->ep3_server && (l->ep3_server->setup_phase != Episode3::SetupPhase::REGISTRATION)) ||
(def_flags & SDF::ALWAYS_FORWARD_TO_WATCHERS)) { (def_flags & SDF::ALWAYS_FORWARD_TO_WATCHERS)) {
for (const auto& watcher_lobby : l->watcher_lobbies) { for (const auto& watcher_lobby : l->watcher_lobbies) {
@@ -333,8 +317,7 @@ static asio::awaitable<void> on_forward_check_game_quest(shared_ptr<Client> c, S
template <typename CmdT> template <typename CmdT>
void forward_subcommand_with_item_transcode_t(shared_ptr<Client> c, uint8_t command, uint8_t flag, const CmdT& cmd) { void forward_subcommand_with_item_transcode_t(shared_ptr<Client> c, uint8_t command, uint8_t flag, const CmdT& cmd) {
// I'm lazy and this should never happen for item commands (since all players // I'm lazy and this should never happen for item commands (since all players need to stay in sync)
// need to stay in sync)
if (command_is_private(command)) { if (command_is_private(command)) {
throw runtime_error("item subcommand sent via private command"); throw runtime_error("item subcommand sent via private command");
} }
@@ -363,8 +346,7 @@ void forward_subcommand_with_item_transcode_t(shared_ptr<Client> c, uint8_t comm
template <typename CmdT, bool ForwardIfMissing = false, size_t EntityIDOffset = offsetof(G_EntityIDHeader, entity_id)> template <typename CmdT, bool ForwardIfMissing = false, size_t EntityIDOffset = offsetof(G_EntityIDHeader, entity_id)>
asio::awaitable<void> forward_subcommand_with_entity_id_transcode_t(shared_ptr<Client> c, SubcommandMessage& msg) { asio::awaitable<void> forward_subcommand_with_entity_id_transcode_t(shared_ptr<Client> c, SubcommandMessage& msg) {
// I'm lazy and this should never happen for item commands (since all players // I'm lazy and this should never happen for item commands (since all players need to stay in sync)
// need to stay in sync)
if (command_is_private(msg.command)) { if (command_is_private(msg.command)) {
throw runtime_error("entity subcommand sent via private command"); throw runtime_error("entity subcommand sent via private command");
} }
@@ -416,8 +398,7 @@ asio::awaitable<void> forward_subcommand_with_entity_id_transcode_t(shared_ptr<C
template <typename HeaderT> template <typename HeaderT>
asio::awaitable<void> forward_subcommand_with_entity_targets_transcode_t(shared_ptr<Client> c, SubcommandMessage& msg) { asio::awaitable<void> forward_subcommand_with_entity_targets_transcode_t(shared_ptr<Client> c, SubcommandMessage& msg) {
// I'm lazy and this should never happen for item commands (since all players // I'm lazy and this should never happen for item commands (since all players need to stay in sync)
// need to stay in sync)
if (command_is_private(msg.command)) { if (command_is_private(msg.command)) {
throw runtime_error("entity subcommand sent via private command"); throw runtime_error("entity subcommand sent via private command");
} }
@@ -490,7 +471,8 @@ asio::awaitable<void> forward_subcommand_with_entity_targets_transcode_t(shared_
co_return; co_return;
} }
static shared_ptr<Client> get_sync_target(shared_ptr<Client> sender_c, uint8_t command, uint8_t flag, bool allow_if_not_loading) { static shared_ptr<Client> get_sync_target(
shared_ptr<Client> sender_c, uint8_t command, uint8_t flag, bool allow_if_not_loading) {
if (!command_is_private(command)) { if (!command_is_private(command)) {
throw runtime_error("sync data sent via public command"); throw runtime_error("sync data sent via public command");
} }
@@ -541,8 +523,7 @@ static asio::awaitable<void> on_sync_joining_player_compressed_state(shared_ptr<
} }
// Assume all v1 and v2 versions are the same, and assume GC/XB are the same. // Assume all v1 and v2 versions are the same, and assume GC/XB are the same.
// TODO: We should do this by checking if the supermaps are the same instead // TODO: We should do this by checking if the supermaps are the same instead of hardcoding this here.
// of hardcoding this here.
auto collapse_version = +[](Version v) -> Version { auto collapse_version = +[](Version v) -> Version {
// Collapse DC v1/v2 and PC into PC_V2 // Collapse DC v1/v2 and PC into PC_V2
if (is_v1_or_v2(v) && !is_pre_v1(v) && (v != Version::GC_NTE)) { if (is_v1_or_v2(v) && !is_pre_v1(v) && (v != Version::GC_NTE)) {
@@ -635,9 +616,8 @@ static asio::awaitable<void> on_sync_joining_player_compressed_state(shared_ptr<
} }
} }
// The leader's item state is never forwarded since the leader may be able // The leader's item state is never forwarded since the leader may be able to see items that the joining player
// to see items that the joining player should not see. We always generate // should not see. We always generate a new item state for the joining player instead.
// a new item state for the joining player instead.
send_game_item_state(target); send_game_item_state(target);
break; break;
} }
@@ -663,8 +643,7 @@ static asio::awaitable<void> on_sync_joining_player_compressed_state(shared_ptr<
true, set_flags_header.num_enemy_sets * sizeof(le_uint16_t)); true, set_flags_header.num_enemy_sets * sizeof(le_uint16_t));
size_t event_set_flags_count = dec_header.event_set_flags_size / sizeof(le_uint16_t); size_t event_set_flags_count = dec_header.event_set_flags_size / sizeof(le_uint16_t);
const auto* event_set_flags = &r.pget<le_uint16_t>( const auto* event_set_flags = &r.pget<le_uint16_t>(
r.where() + dec_header.entity_set_flags_size, r.where() + dec_header.entity_set_flags_size, event_set_flags_count * sizeof(le_uint16_t));
event_set_flags_count * sizeof(le_uint16_t));
l->map_state->import_flag_states_from_sync( l->map_state->import_flag_states_from_sync(
c->version(), c->version(),
object_set_flags, object_set_flags,
@@ -685,19 +664,17 @@ static asio::awaitable<void> on_sync_joining_player_compressed_state(shared_ptr<
if (l->switch_flags) { if (l->switch_flags) {
phosg::StringReader switch_flags_r = r.sub(r.where() + dec_header.entity_set_flags_size + dec_header.event_set_flags_size); phosg::StringReader switch_flags_r = r.sub(r.where() + dec_header.entity_set_flags_size + dec_header.event_set_flags_size);
for (size_t floor = 0; floor < expected_switch_flag_num_floors; floor++) { for (size_t floor = 0; floor < expected_switch_flag_num_floors; floor++) {
// There is a bug in most (perhaps all) versions of the game, which // There is a bug in most (perhaps all) versions of the game, which causes this array to be too small. It
// causes this array to be too small. It looks like Sega forgot to // looks like Sega forgot to account for the header (G_SyncSetFlagState_6x6E_Decompressed) before compressing
// account for the header (G_SyncSetFlagState_6x6E_Decompressed) // the buffer, so the game cuts off the last 8 bytes of the switch flags. Since this only affects the last
// before compressing the buffer, so the game cuts off the last 8 // floor, which rarely has any switches on it (or is even accessible by the player), it's not surprising that
// bytes of the switch flags. Since this only affects the last floor, // no one noticed this. But it does mean we have to check switch_flags_r.eof() here.
// which rarely has any switches on it (or is even accessible by the
// player), it's not surprising that no one noticed this. But it does
// mean we have to check switch_flags_r.eof() here.
for (size_t z = 0; (z < 0x20) && !switch_flags_r.eof(); z++) { for (size_t z = 0; (z < 0x20) && !switch_flags_r.eof(); z++) {
uint8_t& l_flags = l->switch_flags->array(floor).data[z]; uint8_t& l_flags = l->switch_flags->array(floor).data[z];
uint8_t r_flags = switch_flags_r.get_u8(); uint8_t r_flags = switch_flags_r.get_u8();
if (l_flags != r_flags) { if (l_flags != r_flags) {
l->log.warning_f("Switch flags do not match at floor {:02X} byte {:02X} (expected {:02X}, received {:02X})", l->log.warning_f(
"Switch flags do not match at floor {:02X} byte {:02X} (expected {:02X}, received {:02X})",
floor, z, l_flags, r_flags); floor, z, l_flags, r_flags);
l_flags = r_flags; l_flags = r_flags;
} }
@@ -942,7 +919,11 @@ G_SyncPlayerDispAndInventory_DCNTE_6x70 Parsed6x70Data::as_dc_nte(shared_ptr<Ser
ret.items = this->items; ret.items = this->items;
transcode_inventory_items( transcode_inventory_items(
ret.items, ret.num_items, this->item_version, Version::DC_NTE, s->item_parameter_table_for_encode(Version::DC_NTE)); ret.items,
ret.num_items,
this->item_version,
Version::DC_NTE,
s->item_parameter_table_for_encode(Version::DC_NTE));
ret.visual.enforce_lobby_join_limits_for_version(Version::DC_NTE); ret.visual.enforce_lobby_join_limits_for_version(Version::DC_NTE);
uint32_t name_color = s->name_color_for_client(this->from_version, this->from_client_customization); uint32_t name_color = s->name_color_for_client(this->from_version, this->from_client_customization);
@@ -970,7 +951,11 @@ G_SyncPlayerDispAndInventory_DC112000_6x70 Parsed6x70Data::as_dc_112000(shared_p
ret.items = this->items; ret.items = this->items;
transcode_inventory_items( transcode_inventory_items(
ret.items, ret.num_items, this->item_version, Version::DC_11_2000, s->item_parameter_table_for_encode(Version::DC_11_2000)); ret.items,
ret.num_items,
this->item_version,
Version::DC_11_2000,
s->item_parameter_table_for_encode(Version::DC_11_2000));
ret.visual.enforce_lobby_join_limits_for_version(Version::DC_11_2000); ret.visual.enforce_lobby_join_limits_for_version(Version::DC_11_2000);
uint32_t name_color = s->name_color_for_client(this->from_version, this->from_client_customization); uint32_t name_color = s->name_color_for_client(this->from_version, this->from_client_customization);
@@ -1156,18 +1141,15 @@ G_6x70_Base_V1 Parsed6x70Data::base_v1(bool is_v3) const {
} }
uint32_t Parsed6x70Data::convert_game_flags(uint32_t game_flags, bool to_v3) { uint32_t Parsed6x70Data::convert_game_flags(uint32_t game_flags, bool to_v3) {
// The format of game_flags for players was changed significantly between v2 // The format of game_flags for players was changed significantly between v2 and v3, and not accounting for this
// and v3, and not accounting for this results in odd effects like other // results in odd effects like other characters not appearing when joining a game. Unfortunately, some bits were
// characters not appearing when joining a game. Unfortunately, some bits // deleted on v3 and other bits were added, so it doesn't suffice to simply store the most complete format of this
// were deleted on v3 and other bits were added, so it doesn't suffice to // field - we have to be able to convert between the two.
// simply store the most complete format of this field - we have to be able
// to convert between the two.
// Bits on v2: JIHCBAzy xwvutsrq ponmlkji hgfedcba // Bits on v2: JIHCBAzy xwvutsrq ponmlkji hgfedcba
// Bits on v3: JIHGFEDC BAzyxwvu srqponkj hgfedcba // Bits on v3: JIHGFEDC BAzyxwvu srqponkj hgfedcba
// The bits ilmt were removed in v3 and the bits to their left were shifted // The bits ilmt were removed in v3 and the bits to their left were shifted right. The bits DEFG were added in v3 and
// right. The bits DEFG were added in v3 and do not exist on v2. // do not exist on v2. Known meanings for these bits so far:
// Known meanings for these bits:
// o = is dead // o = is dead
// n = should play hit animation // n = should play hit animation
// y = is near enemy // y = is near enemy
@@ -1198,17 +1180,16 @@ static asio::awaitable<void> on_sync_joining_player_disp_and_inventory(
shared_ptr<Client> c, SubcommandMessage& msg) { shared_ptr<Client> c, SubcommandMessage& msg) {
auto s = c->require_server_state(); 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
// finished loading, so we don't check l->any_client_loading() here. // l->any_client_loading() here.
auto target = get_sync_target(c, msg.command, msg.flag, true); auto target = get_sync_target(c, msg.command, msg.flag, true);
if (!target) { if (!target) {
co_return; co_return;
} }
// If the sender is the leader and is pre-V1, and the target is V1 or later, // If the sender is the leader and is pre-V1, and the target is V1 or later, we need to synthesize a 6x71 command to
// we need to synthesize a 6x71 command to tell the target all state has been // tell the target all state has been sent. (If both are pre-V1, the target won't expect this command; if both are V1
// sent. (If both are pre-V1, the target won't expect this command; if both // or later, the leader will send this command itself.)
// are V1 or later, the leader will send this command itself.)
Version target_v = target->version(); Version target_v = target->version();
Version c_v = c->version(); Version c_v = c->version();
if (is_pre_v1(c_v) && !is_pre_v1(target_v)) { if (is_pre_v1(c_v) && !is_pre_v1(target_v)) {
@@ -1327,7 +1308,7 @@ static asio::awaitable<void> on_forward_check_ep3_lobby(shared_ptr<Client> c, Su
co_return; co_return;
} }
//////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Ep3 subcommands // Ep3 subcommands
static asio::awaitable<void> on_ep3_battle_subs(shared_ptr<Client> c, SubcommandMessage& msg) { static asio::awaitable<void> on_ep3_battle_subs(shared_ptr<Client> c, SubcommandMessage& msg) {
@@ -1342,8 +1323,7 @@ static asio::awaitable<void> on_ep3_battle_subs(shared_ptr<Client> c, Subcommand
if (c->version() != Version::GC_EP3_NTE) { if (c->version() != Version::GC_EP3_NTE) {
set_mask_for_ep3_game_command(msg.data, msg.size, 0); set_mask_for_ep3_game_command(msg.data, msg.size, 0);
} else { } else {
// Ep3 NTE sends uninitialized data in this field; clear it so we know the // Ep3 NTE sends uninitialized data in this field; clear it so we know the command isn't masked
// command isn't masked
msg.check_size_t<G_CardBattleCommandHeader>(0xFFFF).mask_key = 0; msg.check_size_t<G_CardBattleCommandHeader>(0xFFFF).mask_key = 0;
} }
@@ -1408,7 +1388,7 @@ static asio::awaitable<void> on_ep3_trade_card_counts(shared_ptr<Client> c, Subc
forward_subcommand(c, msg); forward_subcommand(c, msg);
} }
//////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Chat commands and the like // Chat commands and the like
static asio::awaitable<void> on_send_guild_card(shared_ptr<Client> c, SubcommandMessage& msg) { static asio::awaitable<void> on_send_guild_card(shared_ptr<Client> c, SubcommandMessage& msg) {
@@ -1450,8 +1430,7 @@ static asio::awaitable<void> on_send_guild_card(shared_ptr<Client> c, Subcommand
break; break;
} }
case Version::BB_V4: case Version::BB_V4:
// Nothing to do... the command is blank; the server generates the guild // Nothing to do... the command is blank; the server generates the guild card to be sent
// card to be sent
break; break;
default: default:
throw logic_error("unsupported game version"); throw logic_error("unsupported game version");
@@ -1543,8 +1522,8 @@ static asio::awaitable<void> on_word_select_t(shared_ptr<Client> c, SubcommandMe
static asio::awaitable<void> on_word_select(shared_ptr<Client> c, SubcommandMessage& msg) { static asio::awaitable<void> on_word_select(shared_ptr<Client> c, SubcommandMessage& msg) {
if (is_pre_v1(c->version())) { if (is_pre_v1(c->version())) {
// The Word Select command is a different size in final vs. NTE and // The Word Select command is a different size in final vs. NTE and proto, so handle that here by appending
// proto, so handle that here by appending FFFFFFFF0000000000000000 // FFFFFFFF0000000000000000
string effective_data(reinterpret_cast<const char*>(msg.data), msg.size); string effective_data(reinterpret_cast<const char*>(msg.data), msg.size);
effective_data.resize(0x20, 0x00); effective_data.resize(0x20, 0x00);
effective_data[0x01] = 0x08; effective_data[0x01] = 0x08;
@@ -1596,13 +1575,12 @@ static asio::awaitable<void> on_set_player_visible(shared_ptr<Client> c, Subcomm
co_return; co_return;
} }
//////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static asio::awaitable<void> on_change_floor_6x1F(shared_ptr<Client> c, SubcommandMessage& msg) { static asio::awaitable<void> on_change_floor_6x1F(shared_ptr<Client> c, SubcommandMessage& msg) {
if (is_pre_v1(c->version())) { if (is_pre_v1(c->version())) {
msg.check_size_t<G_SetPlayerFloor_DCNTE_6x1F>(); msg.check_size_t<G_SetPlayerFloor_DCNTE_6x1F>();
// DC NTE and 11/2000 don't send 6F when they're done loading, so we clear // DC NTE and 11/2000 don't send 6F when they're done loading, so we clear the loading flag here instead.
// the loading flag here instead.
if (c->check_flag(Client::Flag::LOADING)) { if (c->check_flag(Client::Flag::LOADING)) {
c->clear_flag(Client::Flag::LOADING); c->clear_flag(Client::Flag::LOADING);
c->log.info_f("LOADING flag cleared"); c->log.info_f("LOADING flag cleared");
@@ -1670,9 +1648,8 @@ static asio::awaitable<void> on_player_revivable(shared_ptr<Client> c, Subcomman
const void* c_data = (!is_v1_or_v2(c->version()) || (c->version() == Version::GC_NTE)) const void* c_data = (!is_v1_or_v2(c->version()) || (c->version() == Version::GC_NTE))
? static_cast<const void*>(&v3_cmd) ? static_cast<const void*>(&v3_cmd)
: static_cast<const void*>(&v2_cmd); : static_cast<const void*>(&v2_cmd);
// TODO: We might need to send different versions of the command here to // TODO: We might need to send different versions of the command here to different clients in certain crossplay
// different clients in certain crossplay scenarios, so just using // scenarios, so just using echo_to_lobby would not suffice. Figure out a way to handle this.
// echo_to_lobby would not suffice. Figure out a way to handle this.
co_await send_protected_command(c, c_data, sizeof(v3_cmd), true); co_await send_protected_command(c, c_data, sizeof(v3_cmd), true);
} }
} }
@@ -1810,10 +1787,9 @@ static asio::awaitable<void> on_switch_state_changed(shared_ptr<Client> c, Subco
send_text_message_fmt(c, "$C5K-{:03X} A {}", obj_st->k_id, type_name); send_text_message_fmt(c, "$C5K-{:03X} A {}", obj_st->k_id, type_name);
} }
// Apparently sometimes 6x05 is sent with an invalid switch flag number. The // Apparently sometimes 6x05 is sent with an invalid switch flag number. The client seems to just ignore the command
// client seems to just ignore the command in that case, so we go ahead and // in that case, so we go ahead and forward it (in case the client's object update function is meaningful somehow)
// forward it (in case the client's object update function is meaningful // and just don't update our view of the switch flags.
// somehow) and just don't update our view of the switch flags.
if (l->switch_flags && (cmd.switch_flag_num < 0x100)) { if (l->switch_flags && (cmd.switch_flag_num < 0x100)) {
if (cmd.flags & 1) { if (cmd.flags & 1) {
l->switch_flags->set(cmd.switch_flag_floor, cmd.switch_flag_num); l->switch_flags->set(cmd.switch_flag_floor, cmd.switch_flag_num);
@@ -1834,15 +1810,15 @@ static asio::awaitable<void> on_switch_state_changed(shared_ptr<Client> c, Subco
static asio::awaitable<void> on_play_sound_from_player(shared_ptr<Client> c, SubcommandMessage& msg) { static asio::awaitable<void> on_play_sound_from_player(shared_ptr<Client> c, SubcommandMessage& msg) {
const auto& cmd = msg.check_size_t<G_PlaySoundFromPlayer_6xB2>(); const auto& cmd = msg.check_size_t<G_PlaySoundFromPlayer_6xB2>();
// This command can be used to play arbitrary sounds, but the client only // This command can be used to play arbitrary sounds, but the client only ever sends it for the camera shutter sound,
// ever sends it for the camera shutter sound, so we only allow that one. // so we only allow that one.
if (cmd.sound_id == 0x00051720) { if (cmd.sound_id == 0x00051720) {
forward_subcommand(c, msg); forward_subcommand(c, msg);
} }
co_return; co_return;
} }
//////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename CmdT> template <typename CmdT>
static asio::awaitable<void> on_movement_xz(shared_ptr<Client> c, SubcommandMessage& msg) { static asio::awaitable<void> on_movement_xz(shared_ptr<Client> c, SubcommandMessage& msg) {
@@ -1906,9 +1882,8 @@ static asio::awaitable<void> on_set_animation_state(shared_ptr<Client> c, Subcom
co_return; co_return;
} }
// The animation numbers were changed on V3. This is the most common one to // The animation numbers were changed on V3. This is the most common one to see in the lobby (it occurs when a player
// see in the lobby (it occurs when a player talks to the counter), so we // talks to the counter), so we take care to translate it specifically.
// take care to translate it specifically.
bool c_is_v1_or_v2 = is_v1_or_v2(c->version()); bool c_is_v1_or_v2 = is_v1_or_v2(c->version());
if (!((c_is_v1_or_v2 && (cmd.animation == 0x000A)) || (!c_is_v1_or_v2 && (cmd.animation == 0x0000)))) { if (!((c_is_v1_or_v2 && (cmd.animation == 0x000A)) || (!c_is_v1_or_v2 && (cmd.animation == 0x0000)))) {
forward_subcommand(c, msg); forward_subcommand(c, msg);
@@ -1926,7 +1901,7 @@ static asio::awaitable<void> on_set_animation_state(shared_ptr<Client> c, Subcom
} }
} }
//////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Item commands // Item commands
static asio::awaitable<void> on_player_drop_item(shared_ptr<Client> c, SubcommandMessage& msg) { static asio::awaitable<void> on_player_drop_item(shared_ptr<Client> c, SubcommandMessage& msg) {
@@ -1966,14 +1941,11 @@ static asio::awaitable<void> on_create_inventory_item_t(shared_ptr<Client> c, Su
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);
// Players cannot send this on behalf of another player, but they can send it // Players cannot send this on behalf of another player, but they can send it on behalf of an NPC; we don't track
// on behalf of an NPC; we don't track items for NPCs so in that case we just // items for NPCs so in that case we just mark the item ID as used and ignore it. This works for the most part,
// mark the item ID as used and ignore it. This works for the most part, // because when NPCs use or equip items, we ignore the command since it has the wrong client ID.
// because when NPCs use or equip items, we ignore the command since it has // TODO: This won't work if NPCs ever drop items that players can interact with. Presumably we would have to track
// the wrong client ID. // all NPCs' inventory items to handle that.
// TODO: This won't work if NPCs ever drop items that players can interact
// with. Presumably we would have to track all NPCs' inventory items to handle
// this.
auto s = c->require_server_state(); auto s = c->require_server_state();
if (cmd.header.client_id != c->lobby_client_id) { if (cmd.header.client_id != c->lobby_client_id) {
// Don't allow creating items in other players' inventories, only in NPCs' // Don't allow creating items in other players' inventories, only in NPCs'
@@ -2023,8 +1995,7 @@ static void on_drop_partial_stack_t(shared_ptr<Client> c, SubcommandMessage& msg
} }
// TODO: Should we check the client ID here too? // TODO: Should we check the client ID here too?
// We don't delete anything from the inventory here; the client will send a // We don't delete anything from the inventory here; the client will send a 6x29 to do so following this command.
// 6x29 to do so immediately following this command.
ItemData item = cmd.item_data; ItemData item = cmd.item_data;
item.decode_for_version(c->version()); item.decode_for_version(c->version());
@@ -2071,16 +2042,14 @@ static asio::awaitable<void> on_drop_partial_stack_bb(shared_ptr<Client> c, Subc
const auto& limits = *s->item_stack_limits(c->version()); const auto& limits = *s->item_stack_limits(c->version());
auto item = p->remove_item(cmd.item_id, cmd.amount, limits); auto item = p->remove_item(cmd.item_id, cmd.amount, limits);
// If a stack was split, the original item still exists, so the dropped item // If a stack was split, the original item still exists, so the dropped item needs a new ID. remove_item signals this
// needs a new ID. remove_item signals this by returning an item with an ID // by returning an item with an ID of 0xFFFFFFFF.
// of 0xFFFFFFFF.
if (item.id == 0xFFFFFFFF) { if (item.id == 0xFFFFFFFF) {
item.id = l->generate_item_id(c->lobby_client_id); item.id = l->generate_item_id(c->lobby_client_id);
} }
// PSOBB sends a 6x29 command after it receives the 6x5D, so we need to add // PSOBB sends a 6x29 command after it receives the 6x5D, so we need to add the item back to the player's inventory
// the item back to the player's inventory to correct for this (it will get // to correct for this (it will get removed again by the 6x29 handler)
// removed again by the 6x29 handler)
p->add_item(item, limits); p->add_item(item, limits);
l->add_item(cmd.floor, item, cmd.pos, nullptr, nullptr, 0x00F); l->add_item(cmd.floor, item, cmd.pos, nullptr, nullptr, 0x00F);
@@ -2161,8 +2130,7 @@ void send_item_notification_if_needed(shared_ptr<Client> c, const ItemData& item
template <typename CmdT> template <typename CmdT>
static void on_box_or_enemy_item_drop_t(shared_ptr<Client> c, SubcommandMessage& msg) { static void on_box_or_enemy_item_drop_t(shared_ptr<Client> c, SubcommandMessage& msg) {
// I'm lazy and this should never happen for item commands (since all players // I'm lazy and this should never happen for item commands (since all players need to stay in sync)
// need to stay in sync)
if (command_is_private(msg.command)) { if (command_is_private(msg.command)) {
throw runtime_error("item subcommand sent via private command"); throw runtime_error("item subcommand sent via private command");
} }
@@ -2242,15 +2210,13 @@ static asio::awaitable<void> on_pick_up_item_generic(
} }
if (!l->item_exists(floor, item_id)) { if (!l->item_exists(floor, item_id)) {
// This can happen if the network is slow, and the client tries to pick up // This can happen if the network is slow, and the client tries to pick up the same item multiple times. Or
// the same item multiple times. Or multiple clients could try to pick up // multiple clients could try to pick up the same item at approximately the same time; only one should get it.
// the same item at approximately the same time; only one should get it.
l->log.warning_f("Player {} requests to pick up {:08X}, but the item does not exist; dropping command", client_id, item_id); l->log.warning_f("Player {} requests to pick up {:08X}, but the item does not exist; dropping command", client_id, item_id);
} else { } else {
// This is handled by the server on BB, and by the leader on other versions. // This is handled by the server on BB, and by the leader on other versions. However, the client's logic is to
// However, the client's logic is to simply always send a 6x59 command when // simply always send a 6x59 command when it receives a 6x5A and the floor item exists, so we just implement that
// it receives a 6x5A and the floor item exists, so we just implement that
// logic here instead of forwarding the 6x5A to the leader. // logic here instead of forwarding the 6x5A to the leader.
auto p = c->character_file(); auto p = c->character_file();
@@ -2429,8 +2395,7 @@ static asio::awaitable<void> on_feed_mag(shared_ptr<Client> c, SubcommandMessage
size_t fed_index = p->inventory.find_item(cmd.fed_item_id); size_t fed_index = p->inventory.find_item(cmd.fed_item_id);
string mag_name, fed_name; string mag_name, fed_name;
{ {
// Note: We do this weird scoping thing because player_feed_mag will // Note: We downscope these because player_feed_mag will likely delete the items, which will break these references
// likely delete the items, which will break the references here.
const auto& fed_item = p->inventory.items[fed_index].data; const auto& fed_item = p->inventory.items[fed_index].data;
fed_name = s->describe_item(c->version(), fed_item); fed_name = s->describe_item(c->version(), fed_item);
const auto& mag_item = p->inventory.items[mag_index].data; const auto& mag_item = p->inventory.items[mag_index].data;
@@ -2438,10 +2403,9 @@ static asio::awaitable<void> on_feed_mag(shared_ptr<Client> c, SubcommandMessage
} }
player_feed_mag(c, mag_index, fed_index); player_feed_mag(c, mag_index, fed_index);
// On BB, the player only sends a 6x28; on other versions, the player sends // On BB, the player only sends a 6x28; on other versions, the player sends a 6x29 immediately after to destroy the
// a 6x29 immediately after to destroy the fed item. So on BB, we should // fed item. So on BB, we should remove the fed item here, but on other versions, we allow the following 6x29 command
// remove the fed item here, but on other versions, we allow the following // to do that.
// 6x29 command to do that.
if (c->version() == Version::BB_V4) { if (c->version() == Version::BB_V4) {
p->remove_item(cmd.fed_item_id, 1, *s->item_stack_limits(c->version())); p->remove_item(cmd.fed_item_id, 1, *s->item_stack_limits(c->version()));
} }
@@ -2488,8 +2452,8 @@ static asio::awaitable<void> on_gc_nte_exclusive(shared_ptr<Client> c, Subcomman
co_return; co_return;
} }
// Command should not be forwarded across the GC NTE boundary, but may be // Command should not be forwarded across the GC NTE boundary, but may be forwarded to other clients within that
// forwarded to other clients within that boundary // boundary
bool c_is_nte = (c->version() == Version::GC_NTE); bool c_is_nte = (c->version() == Version::GC_NTE);
auto l = c->require_lobby(); auto l = c->require_lobby();
@@ -2555,8 +2519,7 @@ bool validate_6xBB(G_SyncCardTradeServerState_Ep3_6xBB& cmd) {
return false; return false;
} }
// TTradeCardServer uses 4 to indicate the slot is empty, so we allow 4 in // TTradeCardServer uses 4 to indicate the slot is empty, so we allow 4 in the client ID checks below
// the client ID checks below
switch (cmd.what) { switch (cmd.what) {
case 1: case 1:
if (cmd.args[0] >= 5) { if (cmd.args[0] >= 5) {
@@ -2684,10 +2647,9 @@ static asio::awaitable<void> on_ep3_private_word_select_bb_bank_action(
} else { // Deposit item } else { // Deposit item
const auto& limits = *s->item_stack_limits(c->version()); const auto& limits = *s->item_stack_limits(c->version());
auto item = p->remove_item(cmd.item_id, cmd.item_amount, limits); auto item = p->remove_item(cmd.item_id, cmd.item_amount, limits);
// If a stack was split, the bank item retains the same item ID as the // If a stack was split, the bank item retains the same item ID as the inventory item. This is annoying but
// inventory item. This is annoying but doesn't cause any problems // doesn't cause any problems because we always generate a new item ID when withdrawing from the bank, so
// because we always generate a new item ID when withdrawing from the // there's no chance of conflict later.
// bank, so there's no chance of conflict later.
if (item.id == 0xFFFFFFFF) { if (item.id == 0xFFFFFFFF) {
item.id = cmd.item_id; item.id = cmd.item_id;
} }
@@ -2746,8 +2708,7 @@ static void on_sort_inventory_bb_inner(shared_ptr<Client> c, const SubcommandMes
const auto& cmd = msg.check_size_t<G_SortInventory_BB_6xC4>(); const auto& cmd = msg.check_size_t<G_SortInventory_BB_6xC4>();
auto p = c->character_file(); auto p = c->character_file();
// Make sure the set of item IDs passed in by the client exactly matches the // Make sure the set of item IDs passed in by the client exactly matches the set of item IDs present in the inventory
// set of item IDs present in the inventory
unordered_set<uint32_t> sorted_item_ids; unordered_set<uint32_t> sorted_item_ids;
size_t expected_count = 0; size_t expected_count = 0;
for (size_t x = 0; x < 30; x++) { for (size_t x = 0; x < 30; x++) {
@@ -2780,8 +2741,8 @@ static void on_sort_inventory_bb_inner(shared_ptr<Client> c, const SubcommandMes
sorted[x] = p->inventory.items[index]; sorted[x] = p->inventory.items[index];
} }
} }
// It's annoying that extension data is stored in the inventory items array, // It's annoying that extension data is stored in the inventory items array, because we have to be careful to avoid
// because we have to be careful to avoid sorting it here too. // sorting it here too.
for (size_t x = 0; x < 30; x++) { for (size_t x = 0; x < 30; x++) {
sorted[x].extension_data1 = p->inventory.items[x].extension_data1; sorted[x].extension_data1 = p->inventory.items[x].extension_data1;
sorted[x].extension_data2 = p->inventory.items[x].extension_data2; sorted[x].extension_data2 = p->inventory.items[x].extension_data2;
@@ -2790,15 +2751,14 @@ static void on_sort_inventory_bb_inner(shared_ptr<Client> c, const SubcommandMes
} }
static asio::awaitable<void> on_sort_inventory_bb(shared_ptr<Client> c, SubcommandMessage& msg) { static asio::awaitable<void> on_sort_inventory_bb(shared_ptr<Client> c, SubcommandMessage& msg) {
// There is a GCC bug that causes this function to not compile properly // There is a GCC bug that causes this function to not compile properly unless the sorting implementation is in a
// unless the sorting implementation is in a separate function. I think it's // separate function. I think it's something to do with how it allocates the coroutine's locals, but it's enough to
// something to do with how it allocates the coroutine's locals, but it's // avoid for now.
// enough to avoid for now.
on_sort_inventory_bb_inner(c, msg); on_sort_inventory_bb_inner(c, msg);
co_return; co_return;
} }
//////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// EXP/Drop Item commands // EXP/Drop Item commands
G_SpecializableItemDropRequest_6xA2 normalize_drop_request(const void* data, size_t size) { G_SpecializableItemDropRequest_6xA2 normalize_drop_request(const void* data, size_t size) {
@@ -2864,8 +2824,7 @@ DropReconcileResult reconcile_drop_request_with_map(
cmd.floor, res.obj_st->super_obj->floor); cmd.floor, res.obj_st->super_obj->floor);
} }
if (is_v1_or_v2(version) && (version != Version::GC_NTE)) { if (is_v1_or_v2(version) && (version != Version::GC_NTE)) {
// V1 and V2 don't have 6xA2, so we can't get ignore_def or the object // V1/V2 don't have 6xA2, so we can't get ignore_def or the object parameters from the client on those versions
// parameters from the client on those versions
cmd.param3 = set_entry->param3; cmd.param3 = set_entry->param3;
cmd.param4 = set_entry->param4; cmd.param4 = set_entry->param4;
cmd.param5 = set_entry->param5; cmd.param5 = set_entry->param5;
@@ -2889,8 +2848,7 @@ DropReconcileResult reconcile_drop_request_with_map(
c->log.info_f("Drop check for E-{:03X} (target E-{:03X}, type {})", c->log.info_f("Drop check for E-{:03X} (target E-{:03X}, type {})",
res.ref_ene_st->e_id, res.target_ene_st->e_id, phosg::name_for_enum(type)); res.ref_ene_st->e_id, res.target_ene_st->e_id, phosg::name_for_enum(type));
res.effective_rt_index = type_definition_for_enemy(type).rt_index; res.effective_rt_index = type_definition_for_enemy(type).rt_index;
// rt_indexes in Episode 4 don't match those sent in the command; we just // rt_indexes in Episode 4 don't match those sent in the command; we just ignore what the client sends.
// ignore what the client sends.
if ((area < 0x24) && (cmd.rt_index != res.effective_rt_index)) { if ((area < 0x24) && (cmd.rt_index != res.effective_rt_index)) {
// Special cases: BULCLAW => BULK and DARK_GUNNER => DEATH_GUNNER // Special cases: BULCLAW => BULK and DARK_GUNNER => DEATH_GUNNER
if (cmd.rt_index == 0x27 && type == EnemyType::BULCLAW) { if (cmd.rt_index == 0x27 && type == EnemyType::BULCLAW) {
@@ -2945,9 +2903,8 @@ static asio::awaitable<void> on_entity_drop_item_request(shared_ptr<Client> c, S
co_return; co_return;
} }
// Note: We always call reconcile_drop_request_with_map, even in client drop // Note: We always call reconcile_drop_request_with_map, even in client drop mode, so that we can correctly mark
// mode, so that we can correctly mark enemies and objects as having dropped // enemies and objects as having dropped their items in persistent games.
// their items in persistent games.
G_SpecializableItemDropRequest_6xA2 cmd = normalize_drop_request(msg.data, msg.size); G_SpecializableItemDropRequest_6xA2 cmd = normalize_drop_request(msg.data, msg.size);
auto rec = reconcile_drop_request_with_map(c, cmd, l->difficulty, l->event, l->map_state, true); auto rec = reconcile_drop_request_with_map(c, cmd, l->difficulty, l->event, l->map_state, true);
@@ -2957,9 +2914,8 @@ static asio::awaitable<void> on_entity_drop_item_request(shared_ptr<Client> c, S
co_return; co_return;
case ServerDropMode::CLIENT: { case ServerDropMode::CLIENT: {
// If the leader is BB, use SERVER_SHARED instead // If the leader is BB, use SERVER_SHARED instead
// TODO: We should also use server drops if any clients have incompatible // TODO: We should also use server drops if any clients have incompatible object lists, since they might generate
// object lists, since they might generate incorrect IDs for items and we // incorrect IDs for items and we can't override them
// can't override them
auto leader = l->clients[l->leader_id]; auto leader = l->clients[l->leader_id];
if (leader && leader->version() == Version::BB_V4) { if (leader && leader->version() == Version::BB_V4) {
drop_mode = ServerDropMode::SERVER_SHARED; drop_mode = ServerDropMode::SERVER_SHARED;
@@ -2985,9 +2941,9 @@ static asio::awaitable<void> on_entity_drop_item_request(shared_ptr<Client> c, S
cmd.entity_index, rec.obj_st->k_id, cmd.effective_area); cmd.entity_index, rec.obj_st->k_id, cmd.effective_area);
return l->item_creator->on_box_item_drop(cmd.effective_area); return l->item_creator->on_box_item_drop(cmd.effective_area);
} else { } else {
l->log.info_f("Creating item from box {:04X} => K-{:03X} (area {:02X}; specialized with {:g} {:08X} {:08X} {:08X})", l->log.info_f(
cmd.entity_index, rec.obj_st->k_id, cmd.effective_area, "Creating item from box {:04X} => K-{:03X} (area {:02X}; specialized with {:g} {:08X} {:08X} {:08X})",
cmd.param3, cmd.param4, cmd.param5, cmd.param6); cmd.entity_index, rec.obj_st->k_id, cmd.effective_area, cmd.param3, cmd.param4, cmd.param5, cmd.param6);
return l->item_creator->on_specialized_box_item_drop( return l->item_creator->on_specialized_box_item_drop(
cmd.effective_area, cmd.param3, cmd.param4, cmd.param5, cmd.param6); cmd.effective_area, cmd.param3, cmd.param4, cmd.param5, cmd.param6);
} }
@@ -3016,8 +2972,6 @@ static asio::awaitable<void> on_entity_drop_item_request(shared_ptr<Client> c, S
throw logic_error("unhandled simple drop mode"); throw logic_error("unhandled simple drop mode");
case ServerDropMode::SERVER_SHARED: case ServerDropMode::SERVER_SHARED:
case ServerDropMode::SERVER_DUPLICATE: { case ServerDropMode::SERVER_DUPLICATE: {
// TODO: In SERVER_DUPLICATE mode, should we reduce the rates for rare
// items? Maybe by a factor of l->count_clients()?
auto res = generate_item(); auto res = generate_item();
if (res.item.empty()) { if (res.item.empty()) {
l->log.info_f("No item was created"); l->log.info_f("No item was created");
@@ -3100,8 +3054,7 @@ static asio::awaitable<void> on_set_quest_flag(shared_ptr<Client> c, SubcommandM
difficulty = static_cast<Difficulty>(cmd.difficulty16.load()); difficulty = static_cast<Difficulty>(cmd.difficulty16.load());
} }
// The client explicitly checks action for both 0 and 1 - any other value // The client explicitly checks action for both 0 and 1 - any other value means no operation is performed.
// means no operation is performed.
if ((flag_num >= 0x400) || (static_cast<size_t>(difficulty) > 3) || (action > 1)) { if ((flag_num >= 0x400) || (static_cast<size_t>(difficulty) > 3) || (action > 1)) {
co_return; co_return;
} }
@@ -3135,9 +3088,8 @@ static asio::awaitable<void> on_set_quest_flag(shared_ptr<Client> c, SubcommandM
EnemyType boss_enemy_type = EnemyType::NONE; EnemyType boss_enemy_type = EnemyType::NONE;
uint8_t area = l->area_for_floor(c->version(), c->floor); uint8_t area = l->area_for_floor(c->version(), c->floor);
if (area == 0x0E) { if (area == 0x0E) {
// On Normal, Dark Falz does not have a third phase, so send the drop // On Normal, Dark Falz does not have a third phase, so send the drop request after the end of the second phase.
// request after the end of the second phase. On all other difficulty // On all other difficulty levels, send it after the third phase.
// levels, send it after the third phase.
if ((difficulty == Difficulty::NORMAL) && (flag_num == 0x0035)) { if ((difficulty == Difficulty::NORMAL) && (flag_num == 0x0035)) {
boss_enemy_type = EnemyType::DARK_FALZ_2; boss_enemy_type = EnemyType::DARK_FALZ_2;
} else if ((difficulty != Difficulty::NORMAL) && (flag_num == 0x0037)) { } else if ((difficulty != Difficulty::NORMAL) && (flag_num == 0x0037)) {
@@ -3212,8 +3164,7 @@ static asio::awaitable<void> on_sync_quest_register(shared_ptr<Client> c, Subcom
throw runtime_error("invalid register number"); throw runtime_error("invalid register number");
} }
// If the lock status register is being written, change the game's flags to // If the lock status register is being written, change the game's flags to allow or forbid joining
// allow or forbid joining
if (l->quest && if (l->quest &&
l->quest->meta.joinable && l->quest->meta.joinable &&
(l->quest->meta.lock_status_register >= 0) && (l->quest->meta.lock_status_register >= 0) &&
@@ -3230,8 +3181,7 @@ static asio::awaitable<void> on_sync_quest_register(shared_ptr<Client> c, Subcom
bool should_forward = true; bool should_forward = true;
if (l->quest->meta.enable_schtserv_commands) { if (l->quest->meta.enable_schtserv_commands) {
// We currently only implement one Schtserv server command here. There // We currently only implement one Schtserv server command here. There are likely many more which we don't support.
// are likely many more which we don't support.
if (cmd.register_number == 0xF0) { if (cmd.register_number == 0xF0) {
should_forward = false; should_forward = false;
@@ -3301,9 +3251,8 @@ static asio::awaitable<void> on_set_entity_set_flag(shared_ptr<Client> c, Subcom
} }
if ((room >= 0) && (wave_number >= 0)) { if ((room >= 0) && (wave_number >= 0)) {
// When all enemies in a wave event have (set_flags & 8), which means // When all enemies in a wave event have (set_flags & 8), which means they are defeated, set event_flags =
// they are defeated, set event_flags = (event_flags | 0x18) & (~4), // (event_flags | 0x18) & (~4), which means it is done and should not trigger again
// which means it is done and should not trigger
bool all_enemies_defeated = true; bool all_enemies_defeated = true;
l->log.info_f("Checking for defeated enemies with room={:04X} wave_number={:04X}", room, wave_number); l->log.info_f("Checking for defeated enemies with room={:04X} wave_number={:04X}", room, wave_number);
for (auto ene_st : l->map_state->enemy_states_for_floor_room_wave(c->version(), cmd.floor, room, wave_number)) { for (auto ene_st : l->map_state->enemy_states_for_floor_room_wave(c->version(), cmd.floor, room, wave_number)) {
@@ -3370,16 +3319,14 @@ static asio::awaitable<void> on_set_entity_set_flag(shared_ptr<Client> c, Subcom
} }
case 0x0A: // set_switch_flag case 0x0A: // set_switch_flag
case 0x0B: { // clear_switch_flag case 0x0B: { // clear_switch_flag
// These opcodes cause the client to send 6x05 commands, so // These opcodes cause the client to send 6x05 commands, so we don't have to do anything here.
// we don't have to do anything here.
uint16_t switch_flag_num = actions_r.get_u16l(); uint16_t switch_flag_num = actions_r.get_u16l();
l->log.info_f("(W-{:03X} script) {}able_switch_flag {:04X}", l->log.info_f("(W-{:03X} script) {}able_switch_flag {:04X}",
ev_st->w_id, (opcode & 1) ? "dis" : "en", switch_flag_num); ev_st->w_id, (opcode & 1) ? "dis" : "en", switch_flag_num);
break; break;
} }
case 0x0C: { // trigger_event case 0x0C: { // trigger_event
// This opcode causes the client to send a 6x67 command, so // This opcode causes the client to send a 6x67 command, so we don't have to do anything here.
// we don't have to do anything here.
uint32_t event_id = actions_r.get_u32l(); uint32_t event_id = actions_r.get_u32l();
l->log.info_f("(W-{:03X} script) trigger_event {:08X}", ev_st->w_id, event_id); l->log.info_f("(W-{:03X} script) trigger_event {:08X}", ev_st->w_id, event_id);
break; break;
@@ -3438,8 +3385,7 @@ static asio::awaitable<void> on_update_telepipe_state(shared_ptr<Client> c, Subc
c->telepipe_state = cmd.state; c->telepipe_state = cmd.state;
c->telepipe_lobby_id = l->lobby_id; c->telepipe_lobby_id = l->lobby_id;
// See the comments in G_SetTelepipeState_6x68 in CommandsFormats.hh for // See the comments in G_SetTelepipeState_6x68 in CommandsFormats.hh for why we have to do this
// why we have to do this
if (is_big_endian(c->version())) { if (is_big_endian(c->version())) {
c->telepipe_state.room_id = bswap32_high16(phosg::bswap32(c->telepipe_state.room_id)); c->telepipe_state.room_id = bswap32_high16(phosg::bswap32(c->telepipe_state.room_id));
} }
@@ -3490,10 +3436,9 @@ static asio::awaitable<void> on_update_enemy_state(shared_ptr<Client> c, Subcomm
ene_st->alias_target_ene_st->e_id, ene_st->alias_target_ene_st->total_damage, ene_st->alias_target_ene_st->game_flags); ene_st->alias_target_ene_st->e_id, ene_st->alias_target_ene_st->total_damage, ene_st->alias_target_ene_st->game_flags);
} }
// TODO: It'd be nice if this worked on bosses too, but it seems we have to // TODO: It'd be nice if this worked on bosses too, but it seems we have to use each boss' specific state-syncing
// use each boss' specific state-syncing command, or the cutscenes misbehave. // command, or the cutscenes misbehave. Just setting flag 0x800 does work on Vol Opt (and the various parts), but
// Just setting flag 0x800 does work on Vol Opt (and the various parts), but // doesn't work on other Episode 1 bosses. Other episodes' bosses are not yet tested.
// doesn't work on other Episode 1 bosses. Other episodes are not yet tested.
bool is_fast_kill = c->check_flag(Client::Flag::FAST_KILLS_ENABLED) && bool is_fast_kill = c->check_flag(Client::Flag::FAST_KILLS_ENABLED) &&
!type_definition_for_enemy(ene_st->super_ene->type).is_boss() && !type_definition_for_enemy(ene_st->super_ene->type).is_boss() &&
!(ene_st->game_flags & 0x00000800); !(ene_st->game_flags & 0x00000800);
@@ -3797,15 +3742,14 @@ static asio::awaitable<void> on_set_entity_pos_and_angle_6x17(shared_ptr<Client>
co_return; co_return;
} }
// 6x17 is used to transport players to the other part of the Vol Opt boss // 6x17 is used to transport players to the other part of the Vol Opt boss arena, so phase 2 can begin. We only allow
// arena, so phase 2 can begin. We only allow 6x17 in the Monitor Room (Vol // 6x17 in the Monitor Room (Vol Opt arena).
// Opt arena).
if (l->area_for_floor(c->version(), c->floor) != 0x0D) { if (l->area_for_floor(c->version(), c->floor) != 0x0D) {
throw runtime_error("client sent 6x17 command in area other than Vol Opt"); throw runtime_error("client sent 6x17 command in area other than Vol Opt");
} }
// If the target is on a different floor or does not exist, just drop the // If the target is on a different floor or does not exist, just drop the command instead of raising; this could have
// command instead of raising; this could have been due to a data race // been due to a data race
if (cmd.header.entity_id < 0x1000) { if (cmd.header.entity_id < 0x1000) {
auto target = l->clients.at(cmd.header.entity_id); auto target = l->clients.at(cmd.header.entity_id);
if (!target || target->floor != c->floor) { if (!target || target->floor != c->floor) {
@@ -3902,8 +3846,7 @@ static asio::awaitable<void> on_level_up(shared_ptr<Client> c, SubcommandMessage
co_return; co_return;
} }
// On the DC prototypes, this command doesn't include any stats - it just // On the DC prototypes, this command doesn't include any stats - it just increments the player's level by 1.
// increments the player's level by 1.
auto p = c->character_file(); auto p = c->character_file();
if (is_pre_v1(c->version())) { if (is_pre_v1(c->version())) {
msg.check_size_t<G_ChangePlayerLevel_DCNTE_6x30>(); msg.check_size_t<G_ChangePlayerLevel_DCNTE_6x30>();
@@ -3977,9 +3920,8 @@ static uint32_t base_exp_for_enemy_type(
} }
} }
// Always try the current episode first. If the current episode is Ep4, try // Always try the current episode first. If the current episode is Ep4, try Ep1 next if in Crater and Ep2 next if in
// Ep1 next if in Crater and Ep2 next if in Desert (this mirrors the logic in // Desert (this mirrors the logic in BB Patch Project's omnispawn patch).
// BB Patch Project's omnispawn patch).
array<Episode, 3> episode_order; array<Episode, 3> episode_order;
episode_order[0] = current_episode; episode_order[0] = current_episode;
if (current_episode == Episode::EP1) { if (current_episode == Episode::EP1) {
@@ -4069,9 +4011,8 @@ static asio::awaitable<void> on_steal_exp_bb(shared_ptr<Client> c, SubcommandMes
uint32_t enemy_exp = base_exp_for_enemy_type( uint32_t enemy_exp = base_exp_for_enemy_type(
s->battle_params, l->quest, type, episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO); s->battle_params, l->quest, type, episode, l->difficulty, ene_st->super_ene->floor, l->mode == GameMode::SOLO);
// Note: The original code checks if special.type is 9, 10, or 11, and skips // Note: The original code checks if special.type is 9, 10, or 11, and skips applying the android bonus if so. We
// applying the android bonus if so. We don't do anything for those special // don't do anything for those special types, so we don't check for that here.
// types, so we don't check for that here.
float percent = special.amount + ((l->difficulty == Difficulty::ULTIMATE) && char_class_is_android(p->disp.visual.char_class) ? 30 : 0); float percent = special.amount + ((l->difficulty == Difficulty::ULTIMATE) && char_class_is_android(p->disp.visual.char_class) ? 30 : 0);
float ep2_factor = (episode == Episode::EP2) ? 1.3 : 1.0; float ep2_factor = (episode == Episode::EP2) ? 1.3 : 1.0;
uint32_t stolen_exp = max<uint32_t>(min<uint32_t>((enemy_exp * percent * ep2_factor) / 100.0f, (static_cast<size_t>(l->difficulty) + 1) * 20), 1); uint32_t stolen_exp = max<uint32_t>(min<uint32_t>((enemy_exp * percent * ep2_factor) / 100.0f, (static_cast<size_t>(l->difficulty) + 1) * 20), 1);
@@ -4104,10 +4045,9 @@ static asio::awaitable<void> on_enemy_exp_request_bb(shared_ptr<Client> c, Subco
ene_st = ene_st->alias_target_ene_st; ene_st = ene_st->alias_target_ene_st;
} }
// If the requesting player never hit this enemy, they are probably cheating; // If the requesting player never hit this enemy, they are probably cheating; ignore the command. Also, each player
// ignore the command. Also, each player sends a 6xC8 if they ever hit the // sends a 6xC8 if they ever hit the enemy; we only react to the first 6xC8 for each enemy (and give all relevant
// enemy; we only react to the first 6xC8 for each enemy (and give all // players EXP then, if they deserve it).
// relevant players EXP then, if they deserve it).
if (!ene_st->ever_hit_by_client_id(c->lobby_client_id) || if (!ene_st->ever_hit_by_client_id(c->lobby_client_id) ||
(ene_st->server_flags & MapState::EnemyState::Flag::EXP_GIVEN)) { (ene_st->server_flags & MapState::EnemyState::Flag::EXP_GIVEN)) {
l->log.info_f("EXP already given for this enemy; ignoring request"); l->log.info_f("EXP already given for this enemy; ignoring request");
@@ -4135,11 +4075,9 @@ static asio::awaitable<void> on_enemy_exp_request_bb(shared_ptr<Client> c, Subco
} }
if (base_exp != 0.0) { if (base_exp != 0.0) {
// If this player killed the enemy, they get full EXP; if they tagged the // If this player killed the enemy, they get full EXP; if they tagged the enemy, they get 80% EXP; if auto EXP
// enemy, they get 80% EXP; if auto EXP share is enabled and they are // share is enabled and they are close enough to the monster, they get a smaller share; if none of these
// close enough to the monster, they get a smaller share; if none of // situations apply, they get no EXP. In Battle and Challenge modes, if a quest is loaded, EXP share is disabled.
// these situations apply, they get no EXP. In Battle and Challenge
// modes, if a quest is loaded, EXP share is disabled.
float exp_share_multiplier = (((l->mode == GameMode::BATTLE) || (l->mode == GameMode::CHALLENGE)) && l->quest) float exp_share_multiplier = (((l->mode == GameMode::BATTLE) || (l->mode == GameMode::CHALLENGE)) && l->quest)
? 0.0f ? 0.0f
: l->exp_share_multiplier; : l->exp_share_multiplier;
@@ -4162,12 +4100,10 @@ static asio::awaitable<void> on_enemy_exp_request_bb(shared_ptr<Client> c, Subco
} }
if (rate_factor > 0.0) { if (rate_factor > 0.0) {
// In PSOBB, Sega decided to add a 30% EXP boost for Episode 2. They // In PSOBB, Sega decided to add a 30% EXP boost for Episode 2. They could have done something reasonable, like
// could have done something reasonable, like edit the BattleParamEntry // edit the BattleParamEntry files so the monsters would all give more EXP, but they did something far lazier
// files so the monsters would all give more EXP, but they did // instead: they just stuck an if statement in the client's EXP request function. We, unfortunately, have to do
// something far lazier instead: they just stuck an if statement in the // the same thing here.
// client's EXP request function. We, unfortunately, have to do the
// same thing here.
uint32_t player_exp = base_exp * uint32_t player_exp = base_exp *
rate_factor * rate_factor *
l->base_exp_multiplier * l->base_exp_multiplier *
@@ -4181,8 +4117,7 @@ static asio::awaitable<void> on_enemy_exp_request_bb(shared_ptr<Client> c, Subco
} }
} }
// Update kill counts on unsealable items, but only for the player who // Update kill counts on unsealable items, but only for the player who actually killed the enemy
// actually killed the enemy
if (ene_st->last_hit_by_client_id(client_id)) { if (ene_st->last_hit_by_client_id(client_id)) {
auto& inventory = lc->character_file()->inventory; auto& inventory = lc->character_file()->inventory;
for (size_t z = 0; z < inventory.num_items; z++) { for (size_t z = 0; z < inventory.num_items; z++) {
@@ -4220,11 +4155,9 @@ static asio::awaitable<void> on_adjust_player_meseta_bb(shared_ptr<Client> c, Su
} }
static void assert_quest_item_create_allowed(shared_ptr<const Lobby> l, const ItemData& item) { static void assert_quest_item_create_allowed(shared_ptr<const Lobby> l, const ItemData& item) {
// We always enforce these restrictions if the quest has any restrictions // We always enforce these restrictions if the quest has any restrictions defined, even if the client has cheat mode
// defined, even if the client has cheat mode enabled or has debug enabled. // enabled or has debug enabled. If the client can cheat, there are much easier ways to create items (e.g. the $item
// If the client can cheat, there are much easier ways to create items (e.g. // chat command) than spoofing these quest item creation commands, so they should just do that instead.
// the $item chat command) than spoofing these quest item creation commands,
// so they should just do that instead.
if (!l->quest) { if (!l->quest) {
throw std::runtime_error("cannot create quest reward item with no quest loaded"); throw std::runtime_error("cannot create quest reward item with no quest loaded");
@@ -4256,23 +4189,19 @@ static asio::awaitable<void> on_quest_create_item_bb(shared_ptr<Client> c, Subco
ItemData item; ItemData item;
item = cmd.item_data; item = cmd.item_data;
// enforce_stack_size_limits must come after this assert since quests may // enforce_stack_size_limits must come after this assert since quests may attempt to create stackable items with a
// attempt to create stackable items with a count of zero // count of zero
assert_quest_item_create_allowed(l, item); assert_quest_item_create_allowed(l, item);
item.enforce_stack_size_limits(limits); item.enforce_stack_size_limits(limits);
item.id = l->generate_item_id(c->lobby_client_id); item.id = l->generate_item_id(c->lobby_client_id);
// The logic for the item_create and item_create2 quest opcodes (B3 and B4) // The logic for the item_create and item_create2 quest opcodes (B3 and B4) includes a precondition check to see if
// includes a precondition check to see if the player can actually add the // the player can actually add the item to their inventory or not, and the entire command is skipped if not. However,
// item to their inventory or not, and the entire command is skipped if not. // on BB, the implementation performs this check and sends a 6xCA command instead - the item is not immediately added
// However, on BB, the implementation performs this check and sends a 6xCA // to the inventory, and is instead added when the server sends back a 6xBE command. So if a quest creates multiple
// command instead - the item is not immediately added to the inventory, and // items in quick succession, there may be another 6xCA/6xBE sequence in flight, and the client's check if an item
// is instead added when the server sends back a 6xBE command. So if a quest // can be created may pass when a 6xBE command that would make it fail is already on the way from the server. To
// creates multiple items in quick succession, there may be another 6xCA/6xBE // handle this, we simply ignore any 6xCA command if the item can't be created.
// sequence in flight, and the client's check if an item can be created may
// pass when a 6xBE command that would make it fail is already on the way
// from the server. To handle this, we simply ignore any 6xCA command if the
// item can't be created.
try { try {
c->character_file()->add_item(item, limits); c->character_file()->add_item(item, limits);
send_create_inventory_item_to_lobby(c, c->lobby_client_id, item); send_create_inventory_item_to_lobby(c, c->lobby_client_id, item);
@@ -4322,9 +4251,8 @@ asio::awaitable<void> on_transfer_item_via_mail_message_bb(shared_ptr<Client> c,
c->print_inventory(); c->print_inventory();
} }
// To receive an item, the player must be online, using BB, have a character // To receive an item, the player must be online, using BB, have a character loaded (that is, be in a lobby or game),
// loaded (that is, be in a lobby or game), not be at the bank counter at the // not be at the bank counter at the moment, and there must be room in their bank to receive the item.
// moment, and there must be room in their bank to receive the item.
bool item_sent = false; bool item_sent = false;
auto target_c = s->find_client(nullptr, cmd.target_guild_card_number); auto target_c = s->find_client(nullptr, cmd.target_guild_card_number);
if (target_c && if (target_c &&
@@ -4339,10 +4267,8 @@ asio::awaitable<void> on_transfer_item_via_mail_message_bb(shared_ptr<Client> c,
} }
if (item_sent) { if (item_sent) {
// See the comment in the 6xCC handler about why we do this. Similar to // See the comment in the 6xCC handler about why we do this. Similar to that case, the 6xCB handler on the client
// that case, the 6xCB handler on the client side does exactly the same // side does exactly the same thing as 6x29, but 6x29 is backward-compatible with other versions and 6xCB is not.
// thing as 6x29, but 6x29 is backward-compatible with other PSO versions
// and 6xCB is not.
G_DeleteInventoryItem_6x29 cmd29 = {{0x29, 0x03, cmd.header.client_id}, cmd.item_id, cmd.amount}; G_DeleteInventoryItem_6x29 cmd29 = {{0x29, 0x03, cmd.header.client_id}, cmd.item_id, cmd.amount};
SubcommandMessage delete_item_msg{msg.command, msg.flag, &cmd29, sizeof(cmd29)}; SubcommandMessage delete_item_msg{msg.command, msg.flag, &cmd29, sizeof(cmd29)};
forward_subcommand(c, delete_item_msg); forward_subcommand(c, delete_item_msg);
@@ -4389,11 +4315,10 @@ static asio::awaitable<void> on_exchange_item_for_team_points_bb(shared_ptr<Clie
c->print_inventory(); c->print_inventory();
} }
// The original implementation forwarded the 6xCC command to all other // The original implementation forwarded the 6xCC command to all other clients. However, the handler does exactly the
// clients. However, the handler does exactly the same thing as 6x29 if the // same thing as 6x29 if the affected client isn't the local client. Since the sender has already processed the 6xCC
// affected client isn't the local client. Since the sender has already // that they sent by the time we receive this, we pretend that they sent 6x29 instead and send that to the others in
// processed the 6xCC that they sent by the time we receive this, we pretend // the game.
// that they sent 6x29 instead and send that to the others in the game.
G_DeleteInventoryItem_6x29 cmd29 = {{0x29, 0x03, cmd.header.client_id}, cmd.item_id, cmd.amount}; G_DeleteInventoryItem_6x29 cmd29 = {{0x29, 0x03, cmd.header.client_id}, cmd.item_id, cmd.amount};
SubcommandMessage delete_item_msg{msg.command, msg.flag, &cmd29, sizeof(cmd29)}; SubcommandMessage delete_item_msg{msg.command, msg.flag, &cmd29, sizeof(cmd29)};
forward_subcommand(c, delete_item_msg); forward_subcommand(c, delete_item_msg);
@@ -4452,18 +4377,15 @@ static asio::awaitable<void> on_destroy_floor_item(shared_ptr<Client> c, Subcomm
} }
if (!fi) { if (!fi) {
// There are generally two data races that could occur here. Either the // There are generally two data races that could occur here. Either the player attempted to evict the item at the
// player attempted to evict the item at the same time the server did (that // same time the server did (that is, the client's and server's 6x63 commands crossed paths on the network), or the
// is, the client's and server's 6x63 commands crossed paths on the // player attempted to evict an item that was already picked up. The former case is easy to handle; we can just
// network), or the player attempted to evict an item that was already // ignore the command. The latter case is more difficult - we have to know which player picked up the item and send
// picked up. The former case is easy to handle; we can just ignore the // a 6x2B command to the sender, to sync their item state with the server's again. We can't just look through the
// command. The latter case is more difficult - we have to know which // players' inventories to find the item ID, since item IDs can be destroyed when stackable items or Meseta are
// player picked up the item and send a 6x2B command to the sender, to sync // picked up.
// their item state with the server's again. We can't just look through the // TODO: We don't actually handle the evict/pickup conflict case. This case is probably quite rare, but we should
// players' inventories to find the item ID, since item IDs can be // eventually handle it.
// destroyed when stackable items or Meseta are picked up.
// TODO: We don't actually handle the evict/pickup conflict case. This case
// is probably quite rare, but we should eventually handle it.
l->log.info_f("Player {} attempted to destroy floor item {:08X}, but it is missing", l->log.info_f("Player {} attempted to destroy floor item {:08X}, but it is missing",
c->lobby_client_id, cmd.item_id); c->lobby_client_id, cmd.item_id);
@@ -4514,10 +4436,9 @@ static asio::awaitable<void> on_identify_item_bb(shared_ptr<Client> c, Subcomman
throw runtime_error("non-weapon items cannot be unidentified"); throw runtime_error("non-weapon items cannot be unidentified");
} }
// It seems the client expects an item ID to be consumed here, even though // It seems the client expects an item ID to be consumed here, even though the returned item has the same ID as the
// the returned item has the same ID as the original item. Perhaps this was // original item. Perhaps this was not the case on Sega's original server, and the returned item had a new ID
// not the case on Sega's original server, and the returned item had a new // instead.
// ID instead.
l->generate_item_id(c->lobby_client_id); l->generate_item_id(c->lobby_client_id);
p->disp.stats.meseta -= 100; p->disp.stats.meseta -= 100;
c->bb_identify_result = p->inventory.items[x].data; c->bb_identify_result = p->inventory.items[x].data;
@@ -4765,8 +4686,7 @@ static asio::awaitable<void> on_challenge_mode_retry_or_quit(shared_ptr<Client>
m.clear(); m.clear();
} }
// If the leader (c) is BB, they are expected to send 02DF later, which // If the leader (c) is BB, they are expected to send 02DF later, which will recreate the overlays.
// will recreate the overlays.
if (!is_v4(c->version())) { if (!is_v4(c->version())) {
for (auto lc : l->clients) { for (auto lc : l->clients) {
if (lc) { if (lc) {
@@ -5202,9 +5122,8 @@ static asio::awaitable<void> on_quest_F95E_result_bb(shared_ptr<Client> c, Subco
} }
ItemData item = (results.size() == 1) ? results[0] : results[l->rand_crypt->next() % results.size()]; ItemData item = (results.size() == 1) ? results[0] : results[l->rand_crypt->next() % results.size()];
if (item.data1[0] == 0x04) { // Meseta if (item.data1[0] == 0x04) { // Meseta
// TODO: What is the right amount of Meseta to use here? Presumably it // TODO: What is the right amount of Meseta to use here? Presumably it should be random within a certain range,
// should be random within a certain range, but it's not obvious what // but it's not obvious what that range should be.
// that range should be.
item.data2d = 100; item.data2d = 100;
} else if (item.data1[0] == 0x00) { } else if (item.data1[0] == 0x00) {
item.data1[4] |= 0x80; // Unidentified item.data1[4] |= 0x80; // Unidentified
@@ -5244,8 +5163,7 @@ static asio::awaitable<void> on_quest_F95F_result_bb(shared_ptr<Client> c, Subco
const auto& limits = *s->item_stack_limits(c->version()); const auto& limits = *s->item_stack_limits(c->version());
size_t index = p->inventory.find_item_by_primary_identifier(0x03100400); // Photon Ticket size_t index = p->inventory.find_item_by_primary_identifier(0x03100400); // Photon Ticket
auto ticket_item = p->remove_item(p->inventory.items[index].data.id, result.first, limits); auto ticket_item = p->remove_item(p->inventory.items[index].data.id, result.first, limits);
// TODO: Shouldn't we send a 6x29 here? Check if this causes desync in an // TODO: Shouldn't we send a 6x29 here? Check if this causes desync in an actual game
// actual game
G_ExchangeItemInQuest_BB_6xDB cmd_6xDB; G_ExchangeItemInQuest_BB_6xDB cmd_6xDB;
cmd_6xDB.header = {0xDB, 0x04, c->lobby_client_id}; cmd_6xDB.header = {0xDB, 0x04, c->lobby_client_id};
@@ -5315,8 +5233,7 @@ static asio::awaitable<void> on_quest_F960_result_bb(shared_ptr<Client> c, Subco
throw runtime_error("no item produced, even from failure tier"); throw runtime_error("no item produced, even from failure tier");
} }
// The client sends a 6xC9 to remove Meseta before sending 6xE2, so we don't // The client sends a 6xC9 to remove Meseta before sending 6xE2, so we don't have to deal with Meseta here.
// have to deal with Meseta here.
item.id = l->generate_item_id(c->lobby_client_id); item.id = l->generate_item_id(c->lobby_client_id);
// If it's a weapon, make it unidentified // If it's a weapon, make it unidentified
@@ -5325,21 +5242,20 @@ static asio::awaitable<void> on_quest_F960_result_bb(shared_ptr<Client> c, Subco
item.data1[4] |= 0x80; item.data1[4] |= 0x80;
} }
// The 6xE3 handler on the client fails if the item already exists, so we // The 6xE3 handler on the client fails if the item already exists, so we need to send 6xE3 before we call
// need to send 6xE3 before we call send_create_inventory_item_to_lobby. // send_create_inventory_item_to_lobby.
G_SetMesetaSlotPrizeResult_BB_6xE3 cmd_6xE3 = { G_SetMesetaSlotPrizeResult_BB_6xE3 cmd_6xE3 = {
{0xE3, sizeof(G_SetMesetaSlotPrizeResult_BB_6xE3) >> 2, c->lobby_client_id}, item}; {0xE3, sizeof(G_SetMesetaSlotPrizeResult_BB_6xE3) >> 2, c->lobby_client_id}, item};
send_command_t(c, 0x60, 0x00, cmd_6xE3); send_command_t(c, 0x60, 0x00, cmd_6xE3);
// Add the item to the player's inventory if possible; if not, drop it on the // Add the item to the player's inventory if possible; if not, drop it on the floor where the player is standing
// floor where the player is standing
bool added_to_inventory; bool added_to_inventory;
try { try {
p->add_item(item, *s->item_stack_limits(c->version())); p->add_item(item, *s->item_stack_limits(c->version()));
added_to_inventory = true; added_to_inventory = true;
} catch (const out_of_range&) { } catch (const out_of_range&) {
// If the game's drop mode is private or duplicate, make the item visible // If the game's drop mode is private or duplicate, make the item visible only to this player; in other modes, make
// only to this player; in other modes, make it visible to everyone // it visible to everyone
uint16_t flags = ((l->drop_mode == ServerDropMode::SERVER_PRIVATE) || (l->drop_mode == ServerDropMode::SERVER_DUPLICATE)) uint16_t flags = ((l->drop_mode == ServerDropMode::SERVER_PRIVATE) || (l->drop_mode == ServerDropMode::SERVER_DUPLICATE))
? (1 << c->lobby_client_id) ? (1 << c->lobby_client_id)
: 0x000F; : 0x000F;
@@ -5474,10 +5390,9 @@ static asio::awaitable<void> on_write_quest_counter_bb(shared_ptr<Client> c, Sub
co_return; co_return;
} }
//////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This makes it easier to see which handlers exist on which prototypes via // This makes it easier to see which handlers exist on which prototypes via syntax highlighting
// syntax highlighting
constexpr uint8_t NONE = 0x00; constexpr uint8_t NONE = 0x00;
const vector<SubcommandDefinition> subcommand_definitions{ const vector<SubcommandDefinition> subcommand_definitions{
+1 -4
View File
@@ -119,10 +119,7 @@ public:
protected: protected:
Parsed6x70Data( Parsed6x70Data(
const G_6x70_Base_V1& base, const G_6x70_Base_V1& base, uint32_t guild_card_number, Version from_version, bool from_client_customization);
uint32_t guild_card_number,
Version from_version,
bool from_client_customization);
G_6x70_Base_V1 base_v1(bool is_v3) const; G_6x70_Base_V1 base_v1(bool is_v3) const;
static uint32_t convert_game_flags(uint32_t game_flags, bool to_v3); static uint32_t convert_game_flags(uint32_t game_flags, bool to_v3);
uint32_t get_game_flags(bool is_v3) const; uint32_t get_game_flags(bool is_v3) const;
+8 -12
View File
@@ -111,8 +111,7 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
case 0x17: case 0x17:
case 0x91: case 0x91:
case 0x9B: { case 0x9B: {
auto& mask = check_size_t<S_ServerInitDefault_DC_PC_V3_02_17_91_9B>( auto& mask = check_size_t<S_ServerInitDefault_DC_PC_V3_02_17_91_9B>(mask_data, mask_size, 0xFFFF);
mask_data, mask_size, 0xFFFF);
mask.server_key = 0; mask.server_key = 0;
mask.client_key = 0; mask.client_key = 0;
break; break;
@@ -370,7 +369,8 @@ ReplaySession::ReplaySession(shared_ptr<ServerState> state, FILE* input_log, boo
// ### cc $<chat command> // ### cc $<chat command>
if (this->clients.size() != 1) { if (this->clients.size() != 1) {
throw runtime_error(std::format( throw runtime_error(std::format(
"(ev-line {}) cc shortcut cannot be used with multiple clients connected; use on C-X cc instead", line_num)); "(ev-line {}) cc shortcut cannot be used with multiple clients connected; use on C-X cc instead",
line_num));
} }
shared_ptr<Event> event; shared_ptr<Event> event;
try { try {
@@ -468,9 +468,7 @@ ReplaySession::ReplaySession(shared_ptr<ServerState> state, FILE* input_log, boo
uint64_t client_id = stoull(tokens[8].substr(2), nullptr, 16); uint64_t client_id = stoull(tokens[8].substr(2), nullptr, 16);
try { try {
parsing_command = this->create_event( parsing_command = this->create_event(
from_client ? Event::Type::SEND : Event::Type::RECEIVE, from_client ? Event::Type::SEND : Event::Type::RECEIVE, this->clients.at(client_id), line_num);
this->clients.at(client_id),
line_num);
num_events++; num_events++;
} catch (const out_of_range&) { } catch (const out_of_range&) {
throw runtime_error(std::format("(ev-line {}) input log contains command for missing client", line_num)); throw runtime_error(std::format("(ev-line {}) input log contains command for missing client", line_num));
@@ -554,8 +552,7 @@ asio::awaitable<void> ReplaySession::run() {
this->reschedule_idle_timeout(); this->reschedule_idle_timeout();
auto msg = co_await c->channel->recv(); auto msg = co_await c->channel->recv();
// TODO: Use the iovec form of phosg::print_data here instead of // TODO: Use the iovec form of phosg::print_data here instead of prepend_command_header (which copies data)
// prepend_command_header (which copies the string)
string full_command = prepend_command_header( string full_command = prepend_command_header(
c->version, (c->channel->crypt_in.get() != nullptr), msg.command, msg.flag, msg.data); c->version, (c->channel->crypt_in.get() != nullptr), msg.command, msg.flag, msg.data);
this->commands_received++; this->commands_received++;
@@ -622,8 +619,8 @@ asio::awaitable<void> ReplaySession::run() {
case Version::BB_V4: case Version::BB_V4:
if (msg.command == 0x03 || msg.command == 0x9B) { if (msg.command == 0x03 || msg.command == 0x9B) {
auto& cmd = msg.check_size_t<S_ServerInitDefault_BB_03_9B>(0xFFFF); auto& cmd = msg.check_size_t<S_ServerInitDefault_BB_03_9B>(0xFFFF);
// TODO: At some point it may matter which BB private key file we use. // TODO: At some point it may matter which BB private key file we use. Don't just blindly use the
// Don't just blindly use the first one here. // first one here.
c->channel->crypt_in = make_shared<PSOBBEncryption>( c->channel->crypt_in = make_shared<PSOBBEncryption>(
*this->state->bb_private_keys[0], cmd.server_key.data(), cmd.server_key.size()); *this->state->bb_private_keys[0], cmd.server_key.data(), cmd.server_key.size());
c->channel->crypt_out = make_shared<PSOBBEncryption>( c->channel->crypt_out = make_shared<PSOBBEncryption>(
@@ -664,8 +661,7 @@ asio::awaitable<void> ReplaySession::run() {
this->state->use_psov2_rand_crypt = this->prev_psov2_crypt_enabled; this->state->use_psov2_rand_crypt = this->prev_psov2_crypt_enabled;
if (!this->run_failed) { if (!this->run_failed) {
// Wait a bit longer to ensure that any command sent at the end of the replay // Wait a bit longer to ensure that any command sent at the end of the replay session don't crash the server
// session don't crash the server
co_await async_sleep(std::chrono::seconds(2)); co_await async_sleep(std::chrono::seconds(2));
replay_log.info_f("Replay complete: {} commands sent ({} bytes), {} commands received ({} bytes)", replay_log.info_f("Replay complete: {} commands sent ({} bytes), {} commands received ({} bytes)",
this->commands_sent, this->bytes_sent, this->commands_received, this->bytes_received); this->commands_sent, this->bytes_sent, this->commands_received, this->bytes_received);
+47 -71
View File
@@ -74,9 +74,8 @@ static const array<uint8_t, 0x0038> DEFAULT_JOYSTICK_CONFIG = {
0x08, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00}; 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00};
// Originally there was going to be a language-based header for .nsc files, but // Originally there was going to be a language-based header for .nsc files, but then I decided against it. This string
// then I decided against it. This string was already in use for that parser, // was already in use for that parser, so I didn't bother changing it.
// so I didn't bother changing it.
const char* LegacySavedAccountDataBB::SIGNATURE = "newserv account file format; 7 sections present; sequential;"; const char* LegacySavedAccountDataBB::SIGNATURE = "newserv account file format; 7 sections present; sequential;";
ShuffleTables::ShuffleTables(PSOV2Encryption& crypt) { ShuffleTables::ShuffleTables(PSOV2Encryption& crypt) {
@@ -195,14 +194,8 @@ bool PSOGCIFileHeader::is_nte() const {
} }
uint32_t compute_psogc_timestamp( uint32_t compute_psogc_timestamp(
uint16_t year, uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second) {
uint8_t month, static uint16_t month_start_day[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
uint8_t day,
uint8_t hour,
uint8_t minute,
uint8_t second) {
static uint16_t month_start_day[12] = {
0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
uint32_t year_start_day = ((year - 1998) >> 2) + (year - 2000) * 365; uint32_t year_start_day = ((year - 1998) >> 2) + (year - 2000) * 365;
if ((((year - 1998) & 3) == 0) && (month < 3)) { if ((((year - 1998) & 3) == 0) && (month < 3)) {
@@ -740,10 +733,9 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSOGCN
src.disp.visual.name.decode(Language::JAPANESE), src.disp.visual.name.decode(Language::JAPANESE),
nullptr); nullptr);
ret->inventory = src.inventory; ret->inventory = src.inventory;
// Note: We intentionally do not call ret->inventory.decode_from_client here. // Note: We intentionally do not call ret->inventory.decode_from_client here. This is because the GC client byteswaps
// This is because the GC client byteswaps data2 in each item before sending // data2 in each item before sending it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does not
// it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does // do this, so the data2 fields are already in the correct order here.
// not do this, so the data2 fields are already in the correct order here.
Language language = ret->inventory.language; Language language = ret->inventory.language;
ret->disp = src.disp.to_bb(language, language); ret->disp = src.disp.to_bb(language, language);
ret->validation_flags = src.validation_flags; ret->validation_flags = src.validation_flags;
@@ -781,10 +773,9 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSOGCC
src.disp.visual.name.decode(Language::JAPANESE), src.disp.visual.name.decode(Language::JAPANESE),
nullptr); nullptr);
ret->inventory = src.inventory; ret->inventory = src.inventory;
// Note: We intentionally do not call ret->inventory.decode_from_client here. // Note: We intentionally do not call ret->inventory.decode_from_client here. This is because the GC client byteswaps
// This is because the GC client byteswaps data2 in each item before sending // data2 in each item before sending it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does not
// it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does // do this, so the data2 fields are already in the correct order here.
// not do this, so the data2 fields are already in the correct order here.
Language language = ret->inventory.language; Language language = ret->inventory.language;
ret->disp = src.disp.to_bb(language, language); ret->disp = src.disp.to_bb(language, language);
ret->validation_flags = src.validation_flags; ret->validation_flags = src.validation_flags;
@@ -926,8 +917,8 @@ PSODCNTECharacterFile::Character PSOBBCharacterFile::as_dc_nte(uint64_t hardware
PSODCNTECharacterFile::Character ret; PSODCNTECharacterFile::Character ret;
ret.inventory = this->inventory; ret.inventory = this->inventory;
// We don't need to do the v1-compatible encoding (hence it is OK to pass // We don't need to do the v1-compatible encoding (hence it is OK to pass nullptr here) but we do need to encode mag
// nullptr here) but we do need to encode mag stats in the v2 format // stats in the v2 format
ret.inventory.encode_for_client(Version::DC_NTE, nullptr); ret.inventory.encode_for_client(Version::DC_NTE, nullptr);
ret.disp = this->disp.to_dcpcv3<false>(language, language); ret.disp = this->disp.to_dcpcv3<false>(language, language);
ret.disp.visual.enforce_lobby_join_limits_for_version(Version::DC_V2); ret.disp.visual.enforce_lobby_join_limits_for_version(Version::DC_V2);
@@ -952,8 +943,8 @@ PSODC112000CharacterFile::Character PSOBBCharacterFile::as_11_2000(uint64_t hard
PSODC112000CharacterFile::Character ret; PSODC112000CharacterFile::Character ret;
ret.inventory = this->inventory; ret.inventory = this->inventory;
// We don't need to do the v1-compatible encoding (hence it is OK to pass // We don't need to do the v1-compatible encoding (hence it is OK to pass nullptr here) but we do need to encode mag
// nullptr here) but we do need to encode mag stats in the v2 format // stats in the v2 format
ret.inventory.encode_for_client(Version::DC_11_2000, nullptr); ret.inventory.encode_for_client(Version::DC_11_2000, nullptr);
ret.disp = this->disp.to_dcpcv3<false>(language, language); ret.disp = this->disp.to_dcpcv3<false>(language, language);
ret.disp.visual.enforce_lobby_join_limits_for_version(Version::DC_V2); ret.disp.visual.enforce_lobby_join_limits_for_version(Version::DC_V2);
@@ -989,8 +980,8 @@ PSOBBCharacterFile::operator PSODCV1CharacterFile::Character() const {
PSODCV1CharacterFile::Character ret; PSODCV1CharacterFile::Character ret;
ret.inventory = this->inventory; ret.inventory = this->inventory;
// We don't need to do the v1-compatible encoding (hence it is OK to pass // We don't need to do the v1-compatible encoding (hence it is OK to pass nullptr here) but we do need to encode mag
// nullptr here) but we do need to encode mag stats in the v2 format // stats in the v2 format
ret.inventory.encode_for_client(Version::DC_V1, nullptr); ret.inventory.encode_for_client(Version::DC_V1, nullptr);
ret.disp = this->disp.to_dcpcv3<false>(language, language); ret.disp = this->disp.to_dcpcv3<false>(language, language);
ret.disp.visual.enforce_lobby_join_limits_for_version(Version::DC_V2); ret.disp.visual.enforce_lobby_join_limits_for_version(Version::DC_V2);
@@ -1021,8 +1012,8 @@ PSOBBCharacterFile::operator PSODCV2CharacterFile::Character() const {
PSODCV2CharacterFile::Character ret; PSODCV2CharacterFile::Character ret;
ret.inventory = this->inventory; ret.inventory = this->inventory;
// We don't need to do the v1-compatible encoding (hence it is OK to pass // We don't need to do the v1-compatible encoding (hence it is OK to pass nullptr here) but we do need to encode mag
// nullptr here) but we do need to encode mag stats in the v2 format // stats in the v2 format
ret.inventory.encode_for_client(Version::DC_V2, nullptr); ret.inventory.encode_for_client(Version::DC_V2, nullptr);
ret.disp = this->disp.to_dcpcv3<false>(language, language); ret.disp = this->disp.to_dcpcv3<false>(language, language);
ret.disp.visual.enforce_lobby_join_limits_for_version(Version::DC_V2); ret.disp.visual.enforce_lobby_join_limits_for_version(Version::DC_V2);
@@ -1062,10 +1053,9 @@ PSOBBCharacterFile::operator PSOGCNTECharacterFileCharacter() const {
PSOGCNTECharacterFileCharacter ret; PSOGCNTECharacterFileCharacter ret;
ret.inventory = this->inventory; ret.inventory = this->inventory;
// Note: We intentionally do not call ret.inventory.encode_for_client here. // Note: We intentionally do not call ret.inventory.encode_for_client here. This is because the GC client byteswaps
// This is because the GC client byteswaps data2 in each item before sending // data2 in each item before sending it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does not
// it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does // do this, so the data2 fields are already in the correct order here.
// not do this, so the data2 fields are already in the correct order here.
ret.disp = this->disp.to_dcpcv3<true>(language, language); ret.disp = this->disp.to_dcpcv3<true>(language, language);
ret.disp.visual.enforce_lobby_join_limits_for_version(Version::GC_V3); ret.disp.visual.enforce_lobby_join_limits_for_version(Version::GC_V3);
ret.validation_flags = this->validation_flags; ret.validation_flags = this->validation_flags;
@@ -1100,10 +1090,9 @@ PSOBBCharacterFile::operator PSOGCCharacterFile::Character() const {
PSOGCCharacterFile::Character ret; PSOGCCharacterFile::Character ret;
ret.inventory = this->inventory; ret.inventory = this->inventory;
// Note: We intentionally do not call ret.inventory.encode_for_client here. // Note: We intentionally do not call ret.inventory.encode_for_client here. This is because the GC client byteswaps
// This is because the GC client byteswaps data2 in each item before sending // data2 in each item before sending it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does not
// it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does // do this, so the data2 fields are already in the correct order here.
// not do this, so the data2 fields are already in the correct order here.
ret.disp = this->disp.to_dcpcv3<true>(language, language); ret.disp = this->disp.to_dcpcv3<true>(language, language);
ret.disp.visual.enforce_lobby_join_limits_for_version(Version::GC_V3); ret.disp.visual.enforce_lobby_join_limits_for_version(Version::GC_V3);
ret.validation_flags = this->validation_flags; ret.validation_flags = this->validation_flags;
@@ -1220,27 +1209,22 @@ void PSOCHARFile::save(
phosg::fwritex(f.get(), header); phosg::fwritex(f.get(), header);
phosg::fwritex(f.get(), *character); phosg::fwritex(f.get(), *character);
phosg::fwritex(f.get(), *system); phosg::fwritex(f.get(), *system);
// TODO: Technically, we should write the actual team membership struct to // TODO: Technically, we should write the actual team membership struct to the file here, but that would cause Client
// the file here, but that would cause Client to depend on Account, which it // to depend on Account, which it currently does not. This data doesn't matter at all for correctness within newserv,
// currently does not. This data doesn't matter at all for correctness within // since it ignores this data entirely and instead generates the membership struct from the team ID in the Account
// newserv, since it ignores this data entirely and instead generates the // and the team's state. So, writing correct data here would mostly be for compatibility with other PSO servers. But
// membership struct from the team ID in the Account and the team's state. // if the other server is newserv, then this data wouldn't be used anyway, and if it's not, then it would presumably
// So, writing correct data here would mostly be for compatibility with other // have a different set of teams with a different set of team IDs anyway, so the membership struct here would be
// PSO servers. But if the other server is newserv, then this data wouldn't // useless either way.
// be used anyway, and if it's not, then it would presumably have a different
// set of teams with a different set of team IDs anyway, so the membership
// struct here would be useless either way.
static const PSOBBFullTeamMembership empty_membership; static const PSOBBFullTeamMembership empty_membership;
phosg::fwritex(f.get(), empty_membership); phosg::fwritex(f.get(), empty_membership);
} }
// TODO: Eliminate duplication between this function and the parallel function // TODO: Eliminate duplication between this function and the parallel function in PlayerBankT
// in PlayerBankT
void PSOBBCharacterFile::add_item(const ItemData& item, const ItemData::StackLimits& limits) { void PSOBBCharacterFile::add_item(const ItemData& item, const ItemData::StackLimits& limits) {
uint32_t primary_identifier = item.primary_identifier(); uint32_t primary_identifier = item.primary_identifier();
// Annoyingly, meseta is in the disp data, not in the inventory struct. If the // Meseta is in the disp data, not in the inventory struct. If the item is meseta, we have to modify disp instead.
// item is meseta, we have to modify disp instead.
if (primary_identifier == 0x04000000) { if (primary_identifier == 0x04000000) {
this->add_meseta(item.data2d); this->add_meseta(item.data2d);
return; return;
@@ -1249,8 +1233,7 @@ void PSOBBCharacterFile::add_item(const ItemData& item, const ItemData::StackLim
// Handle combinable items // Handle combinable items
size_t combine_max = item.max_stack_size(limits); size_t combine_max = item.max_stack_size(limits);
if (combine_max > 1) { if (combine_max > 1) {
// Get the item index if there's already a stack of the same item in the // Get the item index if there's already a stack of the same item in the player's inventory
// player's inventory
size_t y; size_t y;
for (y = 0; y < this->inventory.num_items; y++) { for (y = 0; y < this->inventory.num_items; y++) {
if (this->inventory.items[y].data.primary_identifier() == primary_identifier) { if (this->inventory.items[y].data.primary_identifier() == primary_identifier) {
@@ -1269,8 +1252,7 @@ void PSOBBCharacterFile::add_item(const ItemData& item, const ItemData::StackLim
} }
} }
// If we get here, then it's not meseta and not a combine item, so it needs to // If we get here, then it's not meseta and not a combine item, so it needs to go into an empty inventory slot
// go into an empty inventory slot
if (this->inventory.num_items >= 30) { if (this->inventory.num_items >= 30) {
throw out_of_range("inventory is full"); throw out_of_range("inventory is full");
} }
@@ -1282,13 +1264,11 @@ void PSOBBCharacterFile::add_item(const ItemData& item, const ItemData::StackLim
this->inventory.num_items++; this->inventory.num_items++;
} }
// TODO: Eliminate code duplication between this function and the parallel // TODO: Eliminate code duplication between this function and the parallel function in PlayerBankT
// function in PlayerBankT
ItemData PSOBBCharacterFile::remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits) { ItemData PSOBBCharacterFile::remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits) {
ItemData ret; ItemData ret;
// If we're removing meseta (signaled by an invalid item ID), then create a // If we're removing meseta (signaled by an invalid item ID), then create a meseta item.
// meseta item.
if (item_id == 0xFFFFFFFF) { if (item_id == 0xFFFFFFFF) {
this->remove_meseta(amount, !is_v4(limits.version)); this->remove_meseta(amount, !is_v4(limits.version));
ret.data1[0] = 0x04; ret.data1[0] = 0x04;
@@ -1300,10 +1280,9 @@ ItemData PSOBBCharacterFile::remove_item(uint32_t item_id, uint32_t amount, cons
auto& inventory_item = this->inventory.items[index]; auto& inventory_item = this->inventory.items[index];
bool is_equipped = (inventory_item.flags & 0x00000008); bool is_equipped = (inventory_item.flags & 0x00000008);
// If the item is a combine item and are we removing less than we have of it, // If the item is a combine item and are we removing less than we have of it, then create a new item and reduce the
// then create a new item and reduce the amount of the existing stack. Note // amount of the existing stack. Note that passing amount == 0 means to remove the entire stack, so this only applies
// that passing amount == 0 means to remove the entire stack, so this only // if amount is nonzero.
// applies if amount is nonzero.
if (amount && (inventory_item.data.stack_size(limits) > 1) && (amount < inventory_item.data.data1[5])) { if (amount && (inventory_item.data.stack_size(limits) > 1) && (amount < inventory_item.data.data1[5])) {
if (is_equipped) { if (is_equipped) {
throw runtime_error("character has a combine item equipped"); throw runtime_error("character has a combine item equipped");
@@ -1315,9 +1294,8 @@ ItemData PSOBBCharacterFile::remove_item(uint32_t item_id, uint32_t amount, cons
return ret; return ret;
} }
// If we get here, then it's not meseta, and either it's not a combine item or // If we get here, then it's not meseta, and either it's not a combine item or we're removing the entire stack.
// we're removing the entire stack. Delete the item from the inventory slot // Delete the item from the inventory slot and return the deleted item.
// and return the deleted item.
if (is_equipped) { if (is_equipped) {
this->inventory.unequip_item_index(index); this->inventory.unequip_item_index(index);
} }
@@ -1418,10 +1396,9 @@ void PSOBBCharacterFile::clear_all_material_usage() {
} }
void PSOBBCharacterFile::import_tethealla_material_usage(std::shared_ptr<const LevelTable> level_table) { void PSOBBCharacterFile::import_tethealla_material_usage(std::shared_ptr<const LevelTable> level_table) {
// Tethealla (Ephinea) doesn't store material counts anywhere in the file, // Tethealla (Ephinea) doesn't store material counts anywhere in the file, so if the material counts in the inventory
// so if the material counts in the inventory extension data are all zero, // extension data are all zero, check the current stats against the expected stats for the character's current level
// check the current stats against the expected stats for the character's // and set the material counts if they make sense.
// current level and set the material counts if they make sense.
if (this->get_material_usage(PSOBBCharacterFile::MaterialType::POWER) | if (this->get_material_usage(PSOBBCharacterFile::MaterialType::POWER) |
this->get_material_usage(PSOBBCharacterFile::MaterialType::MIND) | this->get_material_usage(PSOBBCharacterFile::MaterialType::MIND) |
this->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE) | this->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE) |
@@ -1440,9 +1417,8 @@ void PSOBBCharacterFile::import_tethealla_material_usage(std::shared_ptr<const L
uint64_t def = (this->disp.stats.char_stats.dfp - level_base_stats.char_stats.dfp) / 2; uint64_t def = (this->disp.stats.char_stats.dfp - level_base_stats.char_stats.dfp) / 2;
uint64_t luck = (this->disp.stats.char_stats.lck - level_base_stats.char_stats.lck) / 2; uint64_t luck = (this->disp.stats.char_stats.lck - level_base_stats.char_stats.lck) / 2;
// We intentionally do not check any limits here. This is because on pre-v3, // We intentionally do not check any limits here. This is because on pre-v3, there are no limits, and we don't want
// there are no limits, and we don't want to reject legitimate characters // to reject legitimate characters that have used more than 250 materials.
// that have used more than 250 materials.
this->set_material_usage(MaterialType::POWER, pow); this->set_material_usage(MaterialType::POWER, pow);
this->set_material_usage(MaterialType::MIND, mind); this->set_material_usage(MaterialType::MIND, mind);
+94 -145
View File
@@ -18,7 +18,7 @@
#include "PlayerSubordinates.hh" #include "PlayerSubordinates.hh"
#include "Text.hh" #include "Text.hh"
//////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Memory card / VMU structures // Memory card / VMU structures
struct PSOVMSFileHeader { struct PSOVMSFileHeader {
@@ -45,12 +45,10 @@ struct PSOVMSFileHeader {
} __packed_ws__(PSOVMSFileHeader, 0x80); } __packed_ws__(PSOVMSFileHeader, 0x80);
struct PSOGCIFileHeader { struct PSOGCIFileHeader {
// Every PSOGC save file begins with a PSOGCIFileHeader. The first 0x40 bytes // Every PSOGC save file begins with a PSOGCIFileHeader. The first 0x40 bytes of this structure are the .gci file
// of this structure are the .gci file header; the remaining bytes after that // header; the remaining bytes after that are the actual data from the memory card. For save files (system /
// are the actual data from the memory card. For save files (system / // character / Guild Card), one of the structures below immediately follows the PSOGCIFileHeader. The system file is
// character / Guild Card), one of the structures below immediately follows // not encrypted, but the character and Guild Card files are encrypted using a seed stored in the system file.
// the PSOGCIFileHeader. The system file is not encrypted, but the character
// and Guild Card files are encrypted using a seed stored in the system file.
/* 0000 */ parray<char, 4> game_id; // 'GPOE', 'GPSP', etc. /* 0000 */ parray<char, 4> game_id; // 'GPOE', 'GPSP', etc.
/* 0004 */ parray<char, 2> developer_id; // '8P' for Sega /* 0004 */ parray<char, 2> developer_id; // '8P' for Sega
// There is a structure for this part of the header, but we don't use it. // There is a structure for this part of the header, but we don't use it.
@@ -67,19 +65,17 @@ struct PSOGCIFileHeader {
/* 0038 */ be_uint16_t num_blocks = 0; /* 0038 */ be_uint16_t num_blocks = 0;
/* 003A */ parray<uint8_t, 2> unused2; /* 003A */ parray<uint8_t, 2> unused2;
/* 003C */ be_uint32_t comment_offset = 0; /* 003C */ be_uint32_t comment_offset = 0;
// GCI header ends here (and memcard file data begins here) // GCI header ends here (and memcard data begins here)
// game_name is e.g. "PSO EPISODE I & II" or "PSO EPISODE III" /* 0040 */ pstring<TextEncoding::MARKED, 0x1C> game_name; // e.g. "PSO EPISODE I & II" or "PSO EPISODE III"
/* 0040 */ pstring<TextEncoding::MARKED, 0x1C> game_name;
/* 005C */ be_uint32_t embedded_seed = 0; // Used in some of Ralf's quest packs /* 005C */ be_uint32_t embedded_seed = 0; // Used in some of Ralf's quest packs
/* 0060 */ pstring<TextEncoding::MARKED, 0x20> file_name; /* 0060 */ pstring<TextEncoding::MARKED, 0x20> file_name;
/* 0080 */ parray<uint8_t, 0x1800> banner; /* 0080 */ parray<uint8_t, 0x1800> banner;
/* 1880 */ parray<uint8_t, 0x800> icon; /* 1880 */ parray<uint8_t, 0x800> icon;
// data_size specifies the number of bytes remaining in the file. In all cases // data_size specifies the number of bytes remaining in the file. In all cases except for the system file, this data
// except for the system file, this data is encrypted. // is encrypted.
/* 2080 */ be_uint32_t data_size = 0; /* 2080 */ be_uint32_t data_size = 0;
// To compute checksum, set checksum to zero, then compute the CRC32 of all // To compute checksum, set checksum to zero, then compute the CRC32 of all fields in this struct starting with
// fields in this struct starting with gci_header.game_name. (Yes, including // gci_header.game_name. (Yes, including the checksum field, which is temporarily zero.) See checksum_correct below.
// the checksum field, which is temporarily zero.) See checksum_correct below.
/* 2084 */ be_uint32_t checksum = 0; /* 2084 */ be_uint32_t checksum = 0;
/* 2088 */ /* 2088 */
@@ -93,13 +89,9 @@ struct PSOGCIFileHeader {
struct PSOXBFileHeader { struct PSOXBFileHeader {
// The signature is computed by doing the following: // The signature is computed by doing the following:
// // TODO: Should flags be 0 or 1? It looks like it should be 0 for // // TODO: Should flags be 0 or 1? It looks like it should be 0 for character files, but not sure about this
// // character files, but not sure about this
// auto handle = XCalculateSignatureBegin(flags); // auto handle = XCalculateSignatureBegin(flags);
// XCalculateSignatureUpdate( // XCalculateSignatureUpdate(handle, &header.source_size, total_size - offsetof(PSOXBFileHeader, source_size));
// handle,
// &header.source_size,
// total_size - offsetof(PSOXBFileHeader, source_size));
// XCalculateSignatureEnd(handle, header.signature); // XCalculateSignatureEnd(handle, header.signature);
/* 0000 */ parray<uint8_t, 0x14> signature; /* 0000 */ parray<uint8_t, 0x14> signature;
/* 0014 */ le_uint32_t source_size = 0; // == total file size - 0x4000 /* 0014 */ le_uint32_t source_size = 0; // == total file size - 0x4000
@@ -116,7 +108,7 @@ struct PSOXBFileHeader {
void check() const; void check() const;
} __packed_ws__(PSOXBFileHeader, 0x6048); } __packed_ws__(PSOXBFileHeader, 0x6048);
//////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Subordinate structures // Subordinate structures
struct ShuffleTables { struct ShuffleTables {
@@ -247,15 +239,13 @@ struct PSOBBFullTeamMembership {
PSOBBFullTeamMembership() = default; PSOBBFullTeamMembership() = default;
} __packed_ws__(PSOBBFullTeamMembership, 0x838); } __packed_ws__(PSOBBFullTeamMembership, 0x838);
//////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// System files // System files
struct PSOPCCreationTimeFile { // PSO______FLS struct PSOPCCreationTimeFile { // PSO______FLS
// The game creates this file if necessary and fills it with random data. // The game creates this file if necessary and fills it with random data. Most of the random data appears to be a
// Most of the random data appears to be a decoy; only one field is used. // decoy; only one field is used. As in other PSO versions, creation_timestamp is used as an encryption key for the
// As in other PSO versions, creation_timestamp is used as an encryption key // other save files, but only if the serial number isn't set in the Windows registry.
// for the other save files, but only if the serial number isn't set in the
// Windows registry.
/* 0000 */ parray<uint8_t, 0x624> unused1; /* 0000 */ parray<uint8_t, 0x624> unused1;
/* 0624 */ le_uint32_t creation_timestamp = 0; /* 0624 */ le_uint32_t creation_timestamp = 0;
/* 0628 */ parray<uint8_t, 0xDD8> unused2; /* 0628 */ parray<uint8_t, 0xDD8> unused2;
@@ -264,8 +254,8 @@ struct PSOPCCreationTimeFile { // PSO______FLS
struct PSOPCSystemFile { // PSO______COM struct PSOPCSystemFile { // PSO______COM
/* 0000 */ le_uint32_t checksum = 0; /* 0000 */ le_uint32_t checksum = 0;
// Most of these fields are guesses based on the format used in GC and the // Most of these fields are guesses based on the format used in GC and the assumption that Sega didn't change much
// assumption that Sega didn't change much between versions. // between versions.
/* 0004 */ le_int16_t music_volume = 0; /* 0004 */ le_int16_t music_volume = 0;
/* 0006 */ int8_t sound_volume = 0; /* 0006 */ int8_t sound_volume = 0;
/* 0007 */ Language language = Language::ENGLISH; /* 0007 */ Language language = Language::ENGLISH;
@@ -282,17 +272,15 @@ struct PSOGCSystemFile {
/* 0004 */ be_int16_t music_volume = 0; // 0 = full volume; -250 = min volume /* 0004 */ be_int16_t music_volume = 0; // 0 = full volume; -250 = min volume
/* 0006 */ int8_t sound_volume = 0; // 0 = full volume; -100 = min volume /* 0006 */ int8_t sound_volume = 0; // 0 = full volume; -100 = min volume
/* 0007 */ Language language = Language::ENGLISH; /* 0007 */ Language language = Language::ENGLISH;
// This field stores the effective time zone offset between the server and // This field stores the effective time zone offset between the server and client, in frames. The default value is
// client, in frames. The default value is 1728000, which corresponds to 16 // 1728000, which corresponds to 16 hours. This is recomputed when the client receives a B1 command.
// hours. This is recomputed when the client receives a B1 command.
/* 0008 */ be_int32_t server_time_delta_frames = 1728000; /* 0008 */ be_int32_t server_time_delta_frames = 1728000;
/* 000C */ be_uint16_t udp_behavior = 0; // 0 = auto, 1 = on, 2 = off /* 000C */ be_uint16_t udp_behavior = 0; // 0 = auto, 1 = on, 2 = off
/* 000E */ be_uint16_t surround_sound_enabled = 0; /* 000E */ be_uint16_t surround_sound_enabled = 0;
/* 0010 */ parray<uint8_t, 0x100> event_flags; // Can be set by quest opcode D8 or E8 /* 0010 */ parray<uint8_t, 0x100> event_flags; // Can be set by quest opcode D8 or E8
/* 0110 */ parray<uint8_t, 8> unknown_a7; /* 0110 */ parray<uint8_t, 8> unknown_a7;
// This timestamp is the number of seconds since 12:00AM on 1 January 2000. // This timestamp is the number of seconds since 12:00AM on 1 January 2000. This field is also used as the round1
// This field is also used as the round1 seed for encrypting the character and // seed for encrypting the character and Guild Card files.
// Guild Card files.
/* 0118 */ be_uint32_t creation_timestamp = 0; /* 0118 */ be_uint32_t creation_timestamp = 0;
/* 011C */ /* 011C */
} __packed_ws__(PSOGCSystemFile, 0x11C); } __packed_ws__(PSOGCSystemFile, 0x11C);
@@ -355,7 +343,7 @@ struct PSOBBBaseSystemFile : PSOBBMinimalSystemFile {
PSOBBBaseSystemFile(); PSOBBBaseSystemFile();
} __packed_ws__(PSOBBBaseSystemFile, 0x2B8); } __packed_ws__(PSOBBBaseSystemFile, 0x2B8);
//////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Character files // Character files
struct PSODCNTECharacterFile { struct PSODCNTECharacterFile {
@@ -364,14 +352,12 @@ struct PSODCNTECharacterFile {
// See PSOGCCharacterFile::Character for descriptions of fields' meanings. // See PSOGCCharacterFile::Character for descriptions of fields' meanings.
/* 0000:---- */ PlayerInventory inventory; /* 0000:---- */ PlayerInventory inventory;
/* 034C:---- */ PlayerDispDataDCPCV3 disp; /* 034C:---- */ PlayerDispDataDCPCV3 disp;
// masked_creation_timestamp is expected to contain the value // masked_creation_timestamp is expected to contain the value (creation_timestamp ^ hardware_id_mid), where
// (creation_timestamp ^ hardware_id_mid), where hardware_id_mid contains // hardware_id_mid contains the middle 32 bits of the 64-bit hardware ID returned by the SYSINFO_ID syscall (the
// the middle 32 bits of the 64-bit hardware ID returned by the SYSINFO_ID // top and bottom 16 bits are ignored for this purpose).
// syscall (the top and bottom 16 bits are ignored for this purpose).
/* 041C:0000 */ le_uint32_t masked_creation_timestamp = 0; /* 041C:0000 */ le_uint32_t masked_creation_timestamp = 0;
/* 0420:0004 */ le_uint32_t creation_timestamp = 0; /* 0420:0004 */ le_uint32_t creation_timestamp = 0;
// The value of signature is approximately pi * 1e9, but they got a couple // The value of signature is approximately pi * 1e9, but they got a couple of digits wrong (3141562653)
// of digits wrong (3141562653)
/* 0424:0008 */ le_uint32_t signature = 0xBB40711D; /* 0424:0008 */ le_uint32_t signature = 0xBB40711D;
/* 0428:000C */ le_uint32_t play_time_seconds = 0; /* 0428:000C */ le_uint32_t play_time_seconds = 0;
/* 042C:0010 */ le_uint32_t option_flags = 0x00040058; /* 042C:0010 */ le_uint32_t option_flags = 0x00040058;
@@ -379,9 +365,8 @@ struct PSODCNTECharacterFile {
/* 0432:0016 */ le_uint16_t inventory_erasure_count = 0; /* 0432:0016 */ le_uint16_t inventory_erasure_count = 0;
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username; /* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password; /* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
// TODO: Figure out how quest flags work; it's obviously different from 0x80 // TODO: Figure out how quest flags work; it's obviously different from 0x80 bytes per difficulty like in v1. Is it
// bytes per difficulty like in v1. Is it just 2048 flags shared across all // just 2048 flags shared across all difficulties, instead of 1024 in each difficulty?
// difficulties, instead of 1024 in each difficulty?
/* 0460:0044 */ parray<uint8_t, 0x100> quest_flags; /* 0460:0044 */ parray<uint8_t, 0x100> quest_flags;
/* 0560:0144 */ le_uint16_t bank_meseta; /* 0560:0144 */ le_uint16_t bank_meseta;
/* 0562:0146 */ le_uint16_t num_bank_items; /* 0562:0146 */ le_uint16_t num_bank_items;
@@ -482,11 +467,9 @@ struct PSODCV2CharacterFile {
/* 15C8:11AC */ PlayerRecordsBattle battle_records; /* 15C8:11AC */ PlayerRecordsBattle battle_records;
/* 15E0:11C4 */ PlayerRecordsChallengeDC challenge_records; /* 15E0:11C4 */ PlayerRecordsChallengeDC challenge_records;
/* 1680:1264 */ parray<le_uint16_t, 20> tech_menu_shortcut_entries; /* 1680:1264 */ parray<le_uint16_t, 20> tech_menu_shortcut_entries;
// The Choice Search config is stored here as 32-bit integers, even though // The Choice Search config is stored here as 32-bit integers, even though it's represented with 16-bit integers in
// it's represented with 16-bit integers in the various commands that send it // the various commands that send it to and from the server. The order of the entries here is the same (that is,
// to and from the server. The order of the entries here is the same (that // the first two of these ints are entries[0], the second two are entries[1], etc.).
// is, the first two of these ints are entries[0], the second two are
// entries[1], etc.).
/* 16A8:128C */ parray<le_uint32_t, 10> choice_search_config; /* 16A8:128C */ parray<le_uint32_t, 10> choice_search_config;
/* 16D0:12B4 */ parray<uint8_t, 4> unknown_a2; /* 16D0:12B4 */ parray<uint8_t, 4> unknown_a2;
/* 16D4:12B8 */ pstring<TextEncoding::ASCII, 0x10> v2_serial_number; /* 16D4:12B8 */ pstring<TextEncoding::ASCII, 0x10> v2_serial_number;
@@ -565,9 +548,8 @@ struct PSOGCNTECharacterFileCharacter {
/* 2494:2078 */ parray<uint8_t, 4> unknown_a4; /* 2494:2078 */ parray<uint8_t, 4> unknown_a4;
/* 2498:207C */ PlayerRecordsChallengeDC challenge_records; /* 2498:207C */ PlayerRecordsChallengeDC challenge_records;
/* 2538:211C */ parray<be_uint16_t, 20> tech_menu_shortcut_entries; /* 2538:211C */ parray<be_uint16_t, 20> tech_menu_shortcut_entries;
// TODO: choice_search_config and offline_battle_records may be in here // TODO: choice_search_config and offline_battle_records may be in here somewhere. When they are found, don't forget
// somewhere. When they are found, don't forget to update the conversion // to update the conversion functions in PSOBBCharacterFile.
// functions in PSOBBCharacterFile.
/* 2560:2144 */ parray<uint8_t, 0x130> unknown_n2; /* 2560:2144 */ parray<uint8_t, 0x130> unknown_n2;
/* 2690:2274 */ /* 2690:2274 */
} __packed_ws__(PSOGCNTECharacterFileCharacter, 0x2690); } __packed_ws__(PSOGCNTECharacterFileCharacter, 0x2690);
@@ -575,58 +557,45 @@ struct PSOGCNTECharacterFileCharacter {
struct PSOGCCharacterFile { struct PSOGCCharacterFile {
/* 00000 */ be_uint32_t checksum = 0; /* 00000 */ be_uint32_t checksum = 0;
struct Character { struct Character {
// This structure is internally split into two by the game. The offsets // This structure is internally split into two by the game. The offsets here are relative to the start of this
// here are relative to the start of this structure (first column), and // structure (first column), and relative to the start of the second internal structure (second column).
// relative to the start of the second internal structure (second column).
/* 0000:---- */ PlayerInventoryBE inventory; /* 0000:---- */ PlayerInventoryBE inventory;
/* 034C:---- */ PlayerDispDataDCPCV3BE disp; /* 034C:---- */ PlayerDispDataDCPCV3BE disp;
// Known bits in the validation_flags field: // Known bits in the validation_flags field:
// 00000001: Character was not saved after disconnecting (and the message // 00000001: Character was not saved after disconnecting (and the message about items being deleted is shown in
// about items being deleted is shown in the select menu) // the select menu)
// 00000002: Character has level out of range (< 0 or > max) // 00000002: Character has level out of range (< 0 or > max)
// 00000004: Character has EXP out of range for their current level // 00000004: Character has EXP out of range for their current level
// 00000008: Character has one or more stats out of range (< 0 or > max) // 00000008: Character has one or more stats out of range (< 0 or > max)
// 00000010: Character has ever possessed a hacked item, according to the // 00000010: Character has ever possessed a hacked item, according to the check_for_hacked_item function in DCv2.
// check_for_hacked_item function in DCv2. It appears this logic was // It appears this logic was removed in v3, so this flag is unused on v3+.
// removed in v3, so this flag is unused on v3+.
// 00000020: Character has meseta out of range (< 0 or > 999999) // 00000020: Character has meseta out of range (< 0 or > 999999)
// 00000040: Character was loaded on a client that has "important" files // 00000040: Character was loaded on a client that has "important" files modified (on GC, these files are
// modified (on GC, these files are ending_normal.sfd, psogc_j.sfd, // ending_normal.sfd, psogc_j.sfd, psogc_j2.sfd, ult01.sfd, ult02.sfd, ult03.sfd, ult04.sfd, ItemPMT.prs,
// psogc_j2.sfd, ult01.sfd, ult02.sfd, ult03.sfd, ult04.sfd, // itemrt.gsl, itempt.gsl, and PlyLevelTbl.cpt). For files larger than 1000000 bytes (decimal), the game only
// ItemPMT.prs, itemrt.gsl, itempt.gsl, and PlyLevelTbl.cpt). For files // checks the file's size and skips checksumming its contents.
// larger than 1000000 bytes (decimal), the game only checks the file's // PSO v3 and later only use flag 00000001; all logic that checks or sets the other flags was removed in v3.
// size and skips checksumming its contents. // Curiously, there is logic in v3 that clears 00000001 and 00000002 at the same time, but 00000002 is never set.
// PSO v3 and later only use flag 00000001; all logic that checks or sets
// the other flags was removed in v3. Curiously, there is logic in v3 that
// clears flags 00000001 and 00000002 at the same time, but 00000002 is
// never set.
/* 041C:0000 */ be_uint32_t validation_flags = 0; /* 041C:0000 */ be_uint32_t validation_flags = 0;
// The creation timestamp is measured in seconds since midnight on 1 // The creation timestamp is measured in seconds since midnight on 1 January 2000.
// January 2000.
/* 0420:0004 */ be_uint32_t creation_timestamp = 0; /* 0420:0004 */ be_uint32_t creation_timestamp = 0;
// The signature field holds the value 0xA205B064, which is 2718281828 in // The signature field holds the value 0xA205B064, which is 2718281828 in decimal - approximately e * 10^9. It's
// decimal - approximately e * 10^9. It's unknown why Sega chose this // unknown why Sega chose this value. On some other versions, this field has a different value; see the defaults in
// value. On some other versions, this field has a different value; see the // the other versions' structures.
// defaults in the other versions' structures.
/* 0424:0008 */ be_uint32_t signature = 0xA205B064; /* 0424:0008 */ be_uint32_t signature = 0xA205B064;
/* 0428:000C */ be_uint32_t play_time_seconds = 0; /* 0428:000C */ be_uint32_t play_time_seconds = 0;
// This field is a collection of several flags and small values. The known // This field is a collection of several flags and small values. The known fields are:
// fields are:
// ------AB -----CDD EEEFFFGG HIJKLMNO // ------AB -----CDD EEEFFFGG HIJKLMNO
// A = Function key setting (BB; 0 = menu shortcuts; 1 = chat shortcuts). // A = Function key setting (BB; 0 = menu shortcuts; 1 = chat shortcuts). This bit is unused by PSO GC.
// This bit is unused by PSO GC. // B = Keyboard controls (BB; 0 = on; 1 = off). PSO GC uses this field, but its function is currently unknown.
// B = Keyboard controls (BB; 0 = on; 1 = off). This field is also used
// by PSO GC, but its function is currently unknown.
// C = Choice search setting (0 = enabled; 1 = disabled) // C = Choice search setting (0 = enabled; 1 = disabled)
// D = Which pane of the shortcut menu was last used // D = Which pane of the shortcut menu was last used
// E = Player lobby labels (0 = name; 1 = name, language, and level; // E = Player labels (0 = name; 1 = name, language, and level; 2 = W/D counts; 3 = challenge rank; 4 = nothing)
// 2 = W/D counts; 3 = challenge rank; 4 = nothing) // F = Idle disconnect time (0 = 15 mins; 1 = 30 mins; 2 = 45 mins; 3 = 60 mins; 4: never; 5-7: undefined
// F = Idle disconnect time (0 = 15 mins; 1 = 30 mins; 2 = 45 mins; // behavior due to a missing bounds check).
// 3 = 60 mins; 4: never; 5-7: undefined behavior due to a missing
// bounds check).
// G = Message speed (0 = slow; 1 = normal; 2 = fast; 3 = very fast) // G = Message speed (0 = slow; 1 = normal; 2 = fast; 3 = very fast)
// H, I, J, K = unknown; these appear to be used only during Japanese // H, I, J, K = unknown; these appear to be used only during Japanese text input.
// text input. See TWindowKeyBoardBase_read_option_flags // See TWindowKeyBoardBase_read_option_flags
// L = Rumble enabled // L = Rumble enabled
// M = Cursor position (0 = saved; 1 = non-saved) // M = Cursor position (0 = saved; 1 = non-saved)
// N = Button config (0 = normal; 1 = L/R reversed) // N = Button config (0 = normal; 1 = L/R reversed)
@@ -668,9 +637,8 @@ struct PSOGCCharacterFile {
} __packed_ws__(PSOGCCharacterFile, 0x1156C); } __packed_ws__(PSOGCCharacterFile, 0x1156C);
struct PSOGCEp3NTECharacter { struct PSOGCEp3NTECharacter {
// This structure is internally split into two by the game. The offsets here // This structure is internally split into two by the game. The offsets here are relative to the start of this
// are relative to the start of this structure (first column), and relative // structure (first column), and relative to the start of the second internal structure (second column).
// to the start of the second internal structure (second column).
/* 0000:---- */ PlayerInventoryBE inventory; /* 0000:---- */ PlayerInventoryBE inventory;
/* 034C:---- */ PlayerDispDataDCPCV3BE disp; /* 034C:---- */ PlayerDispDataDCPCV3BE disp;
/* 041C:0000 */ be_uint32_t validation_flags = 0; /* 041C:0000 */ be_uint32_t validation_flags = 0;
@@ -704,9 +672,8 @@ struct PSOGCEp3NTECharacter {
struct PSOGCEp3CharacterFile { struct PSOGCEp3CharacterFile {
/* 00000 */ be_uint32_t checksum = 0; // crc32 of this field (as 0) through end of struct /* 00000 */ be_uint32_t checksum = 0; // crc32 of this field (as 0) through end of struct
struct Character { struct Character {
// This structure is internally split into two by the game. The offsets here // This structure is internally split into two by the game. The offsets here are relative to the start of this
// are relative to the start of this structure (first column), and relative // structure (first column), and relative to the start of the second internal structure (second column).
// to the start of the second internal structure (second column).
/* 0000:---- */ PlayerInventoryBE inventory; /* 0000:---- */ PlayerInventoryBE inventory;
/* 034C:---- */ PlayerDispDataDCPCV3BE disp; /* 034C:---- */ PlayerDispDataDCPCV3BE disp;
/* 041C:0000 */ be_uint32_t validation_flags = 0; /* 041C:0000 */ be_uint32_t validation_flags = 0;
@@ -717,16 +684,13 @@ struct PSOGCEp3CharacterFile {
/* 0430:0014 */ be_uint32_t save_count = 1; /* 0430:0014 */ be_uint32_t save_count = 1;
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username; /* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password; /* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
// seq_vars is an array of 8192 bits, which contain all the Episode 3 quest // seq_vars is an array of 8192 bits, which contain all the Episode 3 quest progress flags. This includes things
// progress flags. This includes things like which maps are unlocked, which // like which maps are unlocked, which NPC decks are unlocked, and whether the player has a VIP card or not.
// NPC decks are unlocked, and whether the player has a VIP card or not. // Logically, this structure maps to quest_flags in other versions, but is a different size.
// Logically, this structure maps to quest_flags in other versions, but is
// a different size.
/* 0460:0044 */ Ep3SeqVars seq_vars; /* 0460:0044 */ Ep3SeqVars seq_vars;
/* 0860:0444 */ be_uint32_t death_count = 0; /* 0860:0444 */ be_uint32_t death_count = 0;
// Curiously, Episode 3 characters do have item banks, but there are only 4 // Curiously, Episode 3 characters do have item banks, but there are only 4 item slots. Presumably Sega didn't
// item slots. Presumably Sega didn't completely remove the bank in Ep3 // completely remove the bank in Ep3 because they would have had to change too much code.
// because they would have had to change too much code.
/* 0864:0448 */ PlayerBankT<4, true> bank; /* 0864:0448 */ PlayerBankT<4, true> bank;
/* 08CC:04B0 */ GuildCardGCBE guild_card; /* 08CC:04B0 */ GuildCardGCBE guild_card;
/* 095C:0540 */ parray<SaveFileSymbolChatEntryGC, 12> symbol_chats; /* 095C:0540 */ parray<SaveFileSymbolChatEntryGC, 12> symbol_chats;
@@ -751,21 +715,17 @@ struct PSOGCEp3CharacterFile {
/* 193F0 */ pstring<TextEncoding::ASCII, 0x10> serial_number; // As {:08X} (not decimal) /* 193F0 */ pstring<TextEncoding::ASCII, 0x10> serial_number; // As {:08X} (not decimal)
/* 19400 */ pstring<TextEncoding::ASCII, 0x10> access_key; // As 12 ASCII characters (decimal) /* 19400 */ pstring<TextEncoding::ASCII, 0x10> access_key; // As 12 ASCII characters (decimal)
/* 19410 */ pstring<TextEncoding::ASCII, 0x10> password; /* 19410 */ pstring<TextEncoding::ASCII, 0x10> password;
// In Episode 3, this field still exists, but is unused since BGM test was // In Episode 3, this field still exists, but is unused since BGM test was removed from the options menu in favor of
// removed from the options menu in favor of the jukebox. The jukebox is // the jukebox. The jukebox is accessible online only, and which songs are available there is controlled by the B7
// accessible online only, and which songs are available there is controlled // command sent by the server instead.
// by the B7 command sent by the server instead.
/* 19420 */ be_uint64_t bgm_test_songs_unlocked = 0; /* 19420 */ be_uint64_t bgm_test_songs_unlocked = 0;
/* 19428 */ be_uint32_t save_count = 1; /* 19428 */ be_uint32_t save_count = 1;
// This is an array of 999 bits, represented here as 128 bytes (the last 25 // This is an array of 999 bits, represented here as 128 bytes (the last 25 bits are not used). Each bit corresponds
// bits are not used). Each bit corresponds to a card ID with the bit's index; // to a card ID with the bit's index; if the bit is set, then during offline play, the card's rank is replaced with
// if the bit is set, then during offline play, the card's rank is replaced // D2 if its original rank is S, SS, E, or D2, or with D1 if the original rank is any other value. Upon receiving a
// with D2 if its original rank is S, SS, E, or D2, or with D1 if the original // B8 command (server card definitions), the game clears this array, and sets all bits whose corresponding cards from
// rank is any other value. Upon receiving a B8 command (server card // the server have the D1 or D2 ranks. This could have been used by Sega to prevent broken cards from being used
// definitions), the game clears this array, and sets all bits whose // offline, but there's no indication that they ever used this functionality.
// corresponding cards from the server have the D1 or D2 ranks. This could
// have been used by Sega to prevent broken cards from being used offline, but
// there's no indication that they ever used this functionality.
/* 1942C */ parray<uint8_t, 0x80> card_rank_override_flags; /* 1942C */ parray<uint8_t, 0x80> card_rank_override_flags;
/* 194AC */ be_uint32_t round2_seed = 0; /* 194AC */ be_uint32_t round2_seed = 0;
/* 194B0 */ /* 194B0 */
@@ -773,10 +733,9 @@ struct PSOGCEp3CharacterFile {
struct PSOXBCharacterFile { struct PSOXBCharacterFile {
struct Character { struct Character {
// This structure is internally split into two by the game. The offsets here // This structure is internally split into two by the game. The offsets here are relative to the start of this
// are relative to the start of this structure (first column), and relative // structure (first column), and relative to the start of the second internal structure (second column). Most
// to the start of the second internal structure (second column). // fields have the same meanings as in PSOGCCharacterFile::Character.
// Most fields have the same meanings as in PSOGCCharacterFile::Character.
/* 0000:---- */ PlayerInventory inventory; /* 0000:---- */ PlayerInventory inventory;
/* 034C:---- */ PlayerDispDataDCPCV3 disp; /* 034C:---- */ PlayerDispDataDCPCV3 disp;
/* 041C:0000 */ le_uint32_t validation_flags = 0; /* 041C:0000 */ le_uint32_t validation_flags = 0;
@@ -832,10 +791,9 @@ struct PSOXBCharacterFile {
} __packed_ws__(PSOXBCharacterFile, 0x26564); } __packed_ws__(PSOXBCharacterFile, 0x26564);
struct PSOBBCharacterFile { struct PSOBBCharacterFile {
// Most fields have the same meanings as in PSOGCCharacterFile::Character. // Most fields have the same meanings as in PSOGCCharacterFile::Character. This is part of the .psochar file format,
// This is part of the .psochar file format, but it is not the first member // but it is not the first member of that structure, so add 8 to all the offsets here if you're working with a
// of that structure, so add 8 to all the offsets here if you're working with // .psochar file. See PSOCHARFile below for the full file format.
// a .psochar file. See PSOCHARFile below for the full file format.
/* 0000 */ PlayerInventory inventory; /* 0000 */ PlayerInventory inventory;
/* 034C */ PlayerDispDataBB disp; /* 034C */ PlayerDispDataBB disp;
@@ -924,9 +882,8 @@ struct PSOBBCharacterFile {
} __packed_ws__(PSOBBCharacterFile, 0x2EA4); } __packed_ws__(PSOBBCharacterFile, 0x2EA4);
struct PSOCHARFile { struct PSOCHARFile {
// This is the format of .psochar files used by newserv and Ephinea (and // This is the format of .psochar files used by newserv and Ephinea (and perhaps other servers as well). newserv
// perhaps other servers as well). newserv doesn't actually use this // doesn't actually use this structure in its logic, so it's here primarily for documentation.
// structure in its logic, so it's here primarily for documentation.
/* 0000 */ PSOCommandHeaderBB header; // command = 0x00E7, size = 0x399C, flag = 0 /* 0000 */ PSOCommandHeaderBB header; // command = 0x00E7, size = 0x399C, flag = 0
/* 0008 */ PSOBBCharacterFile character; /* 0008 */ PSOBBCharacterFile character;
@@ -946,7 +903,7 @@ struct PSOCHARFile {
std::shared_ptr<const PSOBBCharacterFile> character); std::shared_ptr<const PSOBBCharacterFile> character);
} __packed_ws__(PSOCHARFile, 0x399C); } __packed_ws__(PSOCHARFile, 0x399C);
//////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Guild Card files // Guild Card files
struct PSODCNTEGuildCardFile { struct PSODCNTEGuildCardFile {
@@ -1048,15 +1005,14 @@ struct PSOBBGuildCardFile {
void delete_duplicates(); void delete_duplicates();
} __packed_ws__(PSOBBGuildCardFile, 0xD590); } __packed_ws__(PSOBBGuildCardFile, 0xD590);
//////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Snapshot files // Snapshot files
struct PSOGCSnapshotFile { struct PSOGCSnapshotFile {
/* 00000 */ be_uint32_t checksum = 0; /* 00000 */ be_uint32_t checksum = 0;
/* 00004 */ be_uint16_t width = 0x100; /* 00004 */ be_uint16_t width = 0x100;
/* 00006 */ be_uint16_t height = 0xC0; /* 00006 */ be_uint16_t height = 0xC0;
// Pixels are stored as 4x4 blocks of RGB565 values. See the implementation // Pixels are stored as 4x4 blocks of RGB565 values. See the implementation of decode_image for details.
// of decode_image for details.
/* 00008 */ parray<be_uint16_t, 0xC000> pixels; /* 00008 */ parray<be_uint16_t, 0xC000> pixels;
/* 18008 */ uint8_t unknown_a1 = 0x18; // Always 0x18? /* 18008 */ uint8_t unknown_a1 = 0x18; // Always 0x18?
/* 18009 */ uint8_t unknown_a2 = 0; /* 18009 */ uint8_t unknown_a2 = 0;
@@ -1070,7 +1026,7 @@ struct PSOGCSnapshotFile {
phosg::ImageRGB888 decode_image() const; phosg::ImageRGB888 decode_image() const;
} __packed_ws__(PSOGCSnapshotFile, 0x1818C); } __packed_ws__(PSOGCSnapshotFile, 0x1818C);
//////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Obsolete newserv-specific formats (for backward compatibility only) // Obsolete newserv-specific formats (for backward compatibility only)
struct LegacySavedPlayerDataBB { // .nsc file format struct LegacySavedPlayerDataBB { // .nsc file format
@@ -1112,7 +1068,7 @@ struct LegacySavedAccountDataBB { // .nsa file format
/* F080 */ /* F080 */
} __packed_ws__(LegacySavedAccountDataBB, 0xF080); } __packed_ws__(LegacySavedAccountDataBB, 0xF080);
//////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Encoding/decoding functions // Encoding/decoding functions
template <bool BE> template <bool BE>
@@ -1184,8 +1140,7 @@ std::string decrypt_fixed_size_data_section_s(
checksum = expected_crc; checksum = expected_crc;
if (expected_crc != actual_crc) { if (expected_crc != actual_crc) {
throw std::runtime_error(std::format( throw std::runtime_error(std::format(
"incorrect decrypted data section checksum: expected {:08X}; received {:08X}", "incorrect decrypted data section checksum: expected {:08X}; received {:08X}", expected_crc, actual_crc));
expected_crc, actual_crc));
} }
} }
@@ -1220,8 +1175,7 @@ StructT decrypt_fixed_size_data_section_t(
ret.checksum = expected_crc; ret.checksum = expected_crc;
if (expected_crc != actual_crc) { if (expected_crc != actual_crc) {
throw std::runtime_error(std::format( throw std::runtime_error(std::format(
"incorrect decrypted data section checksum: expected {:08X}; received {:08X}", "incorrect decrypted data section checksum: expected {:08X}; received {:08X}", expected_crc, actual_crc));
expected_crc, actual_crc));
} }
} }
@@ -1269,12 +1223,7 @@ std::string encrypt_fixed_size_data_section_t(const StructT& s, uint32_t round1_
} }
uint32_t compute_psogc_timestamp( uint32_t compute_psogc_timestamp(
uint16_t year, uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second);
uint8_t month,
uint8_t day,
uint8_t hour,
uint8_t minute,
uint8_t second);
std::string encode_psobb_hangame_credentials( std::string encode_psobb_hangame_credentials(
const std::string& user_id, const std::string& token, const std::string& unused = ""); const std::string& user_id, const std::string& token, const std::string& unused = "");
+99 -185
View File
@@ -53,9 +53,8 @@ const unordered_set<uint32_t> v2_crypt_initial_client_commands({
0x00CC019D, // (02) DCv2/GCNTE login (UDP off) 0x00CC019D, // (02) DCv2/GCNTE login (UDP off)
0x0130009D, // (02) DCv2/GCNTE extended login 0x0130009D, // (02) DCv2/GCNTE extended login
0x0130019D, // (02) DCv2/GCNTE extended login (UDP off) 0x0130019D, // (02) DCv2/GCNTE extended login (UDP off)
// Note: PSO PC initial commands are not listed here because we don't use a // Note: PSO PC initial commands are not listed here because we don't use a detector encryption for PSO PC
// detector encryption for PSO PC (instead, we use the split reconnect command // (instead, we use the split reconnect command to send PC to a different port).
// to send PC to a different port).
}); });
const unordered_set<uint32_t> v3_crypt_initial_client_commands({ const unordered_set<uint32_t> v3_crypt_initial_client_commands({
0x00E000DB, // (17) GC/XB license check 0x00E000DB, // (17) GC/XB license check
@@ -79,8 +78,8 @@ void send_command(shared_ptr<Client> c, uint16_t command, uint32_t flag, const v
c->channel->send(command, flag, data, size); c->channel->send(command, flag, data, size);
} }
void send_command_excluding_client(shared_ptr<Lobby> l, shared_ptr<Client> c, void send_command_excluding_client(
uint16_t command, uint32_t flag, const void* data, size_t size) { shared_ptr<Lobby> l, shared_ptr<Client> c, uint16_t command, uint32_t flag, const void* data, size_t size) {
for (auto& client : l->clients) { for (auto& client : l->clients) {
if (!client || (client == c)) { if (!client || (client == c)) {
continue; continue;
@@ -89,8 +88,8 @@ void send_command_excluding_client(shared_ptr<Lobby> l, shared_ptr<Client> c,
} }
} }
void send_command_if_not_loading(shared_ptr<Lobby> l, void send_command_if_not_loading(
uint16_t command, uint32_t flag, const void* data, size_t size) { shared_ptr<Lobby> l, uint16_t command, uint32_t flag, const void* data, size_t size) {
for (auto& client : l->clients) { for (auto& client : l->clients) {
if (!client || client->check_flag(Client::Flag::LOADING)) { if (!client || client->check_flag(Client::Flag::LOADING)) {
continue; continue;
@@ -99,13 +98,11 @@ void send_command_if_not_loading(shared_ptr<Lobby> l,
} }
} }
void send_command(shared_ptr<Lobby> l, uint16_t command, uint32_t flag, void send_command(shared_ptr<Lobby> l, uint16_t command, uint32_t flag, const void* data, size_t size) {
const void* data, size_t size) {
send_command_excluding_client(l, nullptr, command, flag, data, size); send_command_excluding_client(l, nullptr, command, flag, data, size);
} }
void send_command(shared_ptr<ServerState> s, uint16_t command, uint32_t flag, void send_command(shared_ptr<ServerState> s, uint16_t command, uint32_t flag, const void* data, size_t size) {
const void* data, size_t size) {
for (auto& l : s->all_lobbies()) { for (auto& l : s->all_lobbies()) {
send_command(l, command, flag, data, size); send_command(l, command, flag, data, size);
} }
@@ -150,8 +147,7 @@ static const char* dc_lobby_server_copyright = "DreamCast Lobby Server. Copyrigh
static const char* bb_game_server_copyright = "Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM."; static const char* bb_game_server_copyright = "Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM.";
static const char* bb_pm_server_copyright = "PSO NEW PM Server. Copyright 1999-2002 SONICTEAM."; static const char* bb_pm_server_copyright = "PSO NEW PM Server. Copyright 1999-2002 SONICTEAM.";
S_ServerInitWithAfterMessageT_DC_PC_V3_02_17_91_9B<0xB4> S_ServerInitWithAfterMessageT_DC_PC_V3_02_17_91_9B<0xB4> prepare_server_init_contents_console(
prepare_server_init_contents_console(
uint32_t server_key, uint32_t client_key, uint8_t flags) { uint32_t server_key, uint32_t client_key, uint8_t flags) {
bool initial_connection = (flags & SendServerInitFlag::IS_INITIAL_CONNECTION); bool initial_connection = (flags & SendServerInitFlag::IS_INITIAL_CONNECTION);
S_ServerInitWithAfterMessageT_DC_PC_V3_02_17_91_9B<0xB4> cmd; S_ServerInitWithAfterMessageT_DC_PC_V3_02_17_91_9B<0xB4> cmd;
@@ -214,11 +210,8 @@ void send_server_init_dc_pc_v3(shared_ptr<Client> c, uint8_t flags) {
} }
} }
S_ServerInitWithAfterMessageT_BB_03_9B<0xB4> S_ServerInitWithAfterMessageT_BB_03_9B<0xB4> prepare_server_init_contents_bb(
prepare_server_init_contents_bb( const parray<uint8_t, 0x30>& server_key, const parray<uint8_t, 0x30>& client_key, uint8_t flags) {
const parray<uint8_t, 0x30>& server_key,
const parray<uint8_t, 0x30>& client_key,
uint8_t flags) {
bool use_secondary_message = (flags & SendServerInitFlag::USE_SECONDARY_MESSAGE); bool use_secondary_message = (flags & SendServerInitFlag::USE_SECONDARY_MESSAGE);
S_ServerInitWithAfterMessageT_BB_03_9B<0xB4> cmd; S_ServerInitWithAfterMessageT_BB_03_9B<0xB4> cmd;
cmd.basic_cmd.copyright.encode(use_secondary_message ? bb_pm_server_copyright : bb_game_server_copyright); cmd.basic_cmd.copyright.encode(use_secondary_message ? bb_pm_server_copyright : bb_game_server_copyright);
@@ -311,9 +304,7 @@ void send_patch_enter_directory(shared_ptr<Client> c, const string& dir) {
} }
void send_patch_change_to_directory( void send_patch_change_to_directory(
shared_ptr<Client> c, shared_ptr<Client> c, vector<string>& client_path_directories, const vector<string>& file_path_directories) {
vector<string>& client_path_directories,
const vector<string>& file_path_directories) {
// First, exit all leaf directories that don't match the desired path // First, exit all leaf directories that don't match the desired path
while (!client_path_directories.empty() && while (!client_path_directories.empty() &&
((client_path_directories.size() > file_path_directories.size()) || ((client_path_directories.size() > file_path_directories.size()) ||
@@ -322,8 +313,7 @@ void send_patch_change_to_directory(
client_path_directories.pop_back(); client_path_directories.pop_back();
} }
// At this point, client_path_directories should be a prefix of // At this point, client_path_directories should be a prefix of file_path_directories (or should match exactly)
// file_path_directories (or should match exactly)
if (client_path_directories.size() > file_path_directories.size()) { if (client_path_directories.size() > file_path_directories.size()) {
throw logic_error("did not exit all necessary directories"); throw logic_error("did not exit all necessary directories");
} }
@@ -350,8 +340,7 @@ asio::awaitable<void> prepare_client_for_patches(shared_ptr<Client> c) {
auto call1_res = co_await send_function_call(c, fn, label_writes, nullptr, 0, 0x80000000, 8, 0x7F2734EC); auto call1_res = co_await send_function_call(c, fn, label_writes, nullptr, 0, 0x80000000, 8, 0x7F2734EC);
try { try {
c->specific_version = specific_version_for_gc_header_checksum(call1_res.checksum); c->specific_version = specific_version_for_gc_header_checksum(call1_res.checksum);
c->log.info_f("Version detected as {:08X} from header checksum {:08X}", c->log.info_f("Version detected as {:08X} from header checksum {:08X}", c->specific_version, call1_res.checksum);
c->specific_version, call1_res.checksum);
} catch (const out_of_range&) { } catch (const out_of_range&) {
c->log.info_f("Could not detect specific version from header checksum {:08X}", call1_res.checksum); c->log.info_f("Could not detect specific version from header checksum {:08X}", call1_res.checksum);
} }
@@ -369,7 +358,8 @@ asio::awaitable<void> prepare_client_for_patches(shared_ptr<Client> c) {
version_detect_name = "VersionDetectXB"; version_detect_name = "VersionDetectXB";
} }
if (version_detect_name && specific_version_is_indeterminate(c->specific_version)) { if (version_detect_name && specific_version_is_indeterminate(c->specific_version)) {
auto vers_detect_res = co_await send_function_call(c, s->function_code_index->name_to_function.at(version_detect_name)); auto vers_detect_res = co_await send_function_call(
c, s->function_code_index->name_to_function.at(version_detect_name));
c->specific_version = vers_detect_res.return_value; c->specific_version = vers_detect_res.return_value;
c->log.info_f("Version detected as {:08X}", c->specific_version); c->log.info_f("Version detected as {:08X}", c->specific_version);
} }
@@ -391,8 +381,8 @@ string prepare_send_function_call_data(
if (use_encrypted_format) { if (use_encrypted_format) {
uint32_t key = phosg::random_object<uint32_t>(); uint32_t key = phosg::random_object<uint32_t>();
// This format was probably never used on any little-endian system, but we // This format was probably never used on any little-endian system, but we implement the way it would probably
// implement the way it would probably work there if it was used. // work there if it was used.
phosg::StringWriter w; phosg::StringWriter w;
if (code->is_big_endian()) { if (code->is_big_endian()) {
w.put_u32b(data.size()); w.put_u32b(data.size());
@@ -564,10 +554,9 @@ asio::awaitable<void> send_dol_file(shared_ptr<Client> c, shared_ptr<DOLFileInde
// Write the file in multiple chunks // Write the file in multiple chunks
for (size_t offset = 0; offset < dol->data.size();) { for (size_t offset = 0; offset < dol->data.size();) {
// Note: The protocol allows commands to be up to 0x7C00 bytes in size, but // Note: The protocol allows commands to be up to 0x7C00 bytes in size, but sending large B2 commands can cause the
// sending large B2 commands can cause the client to crash or softlock. To // client to crash or softlock. To avoid this, we limit the payload to 4KB, which results in a B2 command 0x10D0
// avoid this, we limit the payload to 4KB, which results in a B2 command // bytes in size.
// 0x10D0 bytes in size.
size_t bytes_to_send = min<size_t>(0x1000, dol->data.size() - offset); size_t bytes_to_send = min<size_t>(0x1000, dol->data.size() - offset);
string data_to_send = dol->data.substr(offset, bytes_to_send); string data_to_send = dol->data.substr(offset, bytes_to_send);
@@ -582,8 +571,7 @@ asio::awaitable<void> send_dol_file(shared_ptr<Client> c, shared_ptr<DOLFileInde
offset += bytes_to_send; offset += bytes_to_send;
} }
// Send the final function, which moves the DOL's sections into place and // Send the final function, which moves the DOL's sections into place and calls the entrypoint
// calls the entrypoint
auto fn = s->function_code_index->name_to_function.at("RunDOL"); auto fn = s->function_code_index->name_to_function.at("RunDOL");
label_writes = {{"dol_base_ptr", dol_base_addr}}; label_writes = {{"dol_base_ptr", dol_base_addr}};
co_await send_function_call(c, fn, label_writes); co_await send_function_call(c, fn, label_writes);
@@ -596,8 +584,7 @@ void send_reconnect(shared_ptr<Client> c, uint32_t address, uint16_t port) {
send_command_t(c, is_patch(c->version()) ? 0x14 : 0x19, 0x00, cmd); send_command_t(c, is_patch(c->version()) ? 0x14 : 0x19, 0x00, cmd);
} }
void send_pc_console_split_reconnect(shared_ptr<Client> c, uint32_t address, void send_pc_console_split_reconnect(shared_ptr<Client> c, uint32_t address, uint16_t pc_port, uint16_t console_port) {
uint16_t pc_port, uint16_t console_port) {
S_ReconnectSplit_19 cmd; S_ReconnectSplit_19 cmd;
cmd.pc_address = address; cmd.pc_address = address;
cmd.pc_port = pc_port; cmd.pc_port = pc_port;
@@ -651,10 +638,9 @@ void send_client_init_bb(shared_ptr<Client> c, uint32_t error_code) {
cmd.can_create_team = 1; cmd.can_create_team = 1;
cmd.episode_4_unlocked = 1; cmd.episode_4_unlocked = 1;
// If security_token is zero, the game scrambles the client config data based // If security_token is zero, the game scrambles the client config data based on the first character in the username.
// on the first character in the username. We undo the scramble here, so when // We undo the scramble here, so when the client scrambles the data upon receipt, it will be correct when it next is
// the client scrambles the data upon receipt, it will be correct when it next // sent back to the server.
// is sent back to the server.
if (cmd.security_token == 0 && c->login && c->login->bb_license) { if (cmd.security_token == 0 && c->login && c->login->bb_license) {
scramble_bb_security_data(cmd.client_config, c->login->bb_license->username.at(0), true); scramble_bb_security_data(cmd.client_config, c->login->bb_license->username.at(0), true);
} }
@@ -674,11 +660,9 @@ void send_system_file_bb(shared_ptr<Client> c) {
} }
void send_player_preview_bb(shared_ptr<Client> c, int8_t character_index, const PlayerDispDataBBPreview* preview) { void send_player_preview_bb(shared_ptr<Client> c, int8_t character_index, const PlayerDispDataBBPreview* preview) {
if (!preview) { if (!preview) { // No player exists
// no player exists
S_PlayerPreview_NoPlayer_BB_00E4 cmd = {character_index, 0x00000002}; S_PlayerPreview_NoPlayer_BB_00E4 cmd = {character_index, 0x00000002};
send_command_t(c, 0x00E4, 0x00000000, cmd); send_command_t(c, 0x00E4, 0x00000000, cmd);
} else { } else {
SC_PlayerPreview_CreateCharacter_BB_00E5 cmd = {character_index, *preview}; SC_PlayerPreview_CreateCharacter_BB_00E5 cmd = {character_index, *preview};
send_command_t(c, 0x00E5, 0x00000000, cmd); send_command_t(c, 0x00E5, 0x00000000, cmd);
@@ -702,10 +686,7 @@ void send_guild_card_chunk_bb(shared_ptr<Client> c, size_t chunk_index) {
size_t data_size = min<size_t>(sizeof(PSOBBGuildCardFile) - chunk_offset, sizeof(cmd.data)); size_t data_size = min<size_t>(sizeof(PSOBBGuildCardFile) - chunk_offset, sizeof(cmd.data));
cmd.unknown_a1 = 0; cmd.unknown_a1 = 0;
cmd.chunk_index = chunk_index; cmd.chunk_index = chunk_index;
cmd.data.assign_range( cmd.data.assign_range(reinterpret_cast<const uint8_t*>(c->guild_card_file().get()) + chunk_offset, data_size, 0);
reinterpret_cast<const uint8_t*>(c->guild_card_file().get()) + chunk_offset,
data_size, 0);
send_command(c, 0x02DC, 0x00000000, &cmd, sizeof(cmd) - sizeof(cmd.data) + data_size); send_command(c, 0x02DC, 0x00000000, &cmd, sizeof(cmd) - sizeof(cmd.data) + data_size);
} }
@@ -731,9 +712,8 @@ void send_stream_file_index_bb(shared_ptr<Client> c) {
auto cache_res = s->bb_stream_files_cache->get_or_load(key); auto cache_res = s->bb_stream_files_cache->get_or_load(key);
auto& e = entries.emplace_back(); auto& e = entries.emplace_back();
e.size = cache_res.file->data->size(); e.size = cache_res.file->data->size();
// Computing the checksum can be slow, so we cache it along with the file // Computing the checksum can be slow, so we cache it along with the file data. If the cache result was just
// data. If the cache result was just populated, then it may be different, // populated, then it may be different, so we always recompute the checksum in that case.
// so we always recompute the checksum in that case.
if (cache_res.generate_called) { if (cache_res.generate_called) {
e.checksum = crc32(cache_res.file->data->data(), e.size); e.checksum = crc32(cache_res.file->data->data(), e.size);
s->bb_stream_files_cache->replace_obj<uint32_t>(key + ".crc32", e.checksum); s->bb_stream_files_cache->replace_obj<uint32_t>(key + ".crc32", e.checksum);
@@ -813,9 +793,6 @@ void send_complete_player_bb(shared_ptr<Client> c) {
c->login->account->last_player_name = p->disp.name.decode(p->inventory.language); c->login->account->last_player_name = p->disp.name.decode(p->inventory.language);
} }
////////////////////////////////////////////////////////////////////////////////
// message functions
enum class ColorMode { enum class ColorMode {
NONE, NONE,
ADD, ADD,
@@ -863,12 +840,14 @@ static void send_text(
ch->send(command, flag, w.str()); ch->send(command, flag, w.str());
} }
static void send_text(std::shared_ptr<Channel> ch, uint16_t command, uint32_t flag, const string& text, ColorMode color_mode) { static void send_text(
std::shared_ptr<Channel> ch, uint16_t command, uint32_t flag, const string& text, ColorMode color_mode) {
phosg::StringWriter w; phosg::StringWriter w;
send_text(ch, w, command, flag, text, color_mode); send_text(ch, w, command, flag, text, color_mode);
} }
static void send_header_text(std::shared_ptr<Channel> ch, uint16_t command, uint32_t guild_card_number, const string& text, ColorMode color_mode) { static void send_header_text(
std::shared_ptr<Channel> ch, uint16_t command, uint32_t guild_card_number, const string& text, ColorMode color_mode) {
phosg::StringWriter w; phosg::StringWriter w;
w.put(SC_TextHeader_01_06_11_B0_EE({0, guild_card_number})); w.put(SC_TextHeader_01_06_11_B0_EE({0, guild_card_number}));
send_text(ch, w, command, 0x00, text, color_mode); send_text(ch, w, command, 0x00, text, color_mode);
@@ -1098,12 +1077,7 @@ void send_chat_message(
const string& text, const string& text,
char private_flags) { char private_flags) {
string prepared_data = prepare_chat_data( string prepared_data = prepare_chat_data(
c->version(), c->version(), c->language(), c->lobby_client_id, from_name, text, private_flags);
c->language(),
c->lobby_client_id,
from_name,
text,
private_flags);
send_prepared_chat_message(c, from_guild_card_number, prepared_data); send_prepared_chat_message(c, from_guild_card_number, prepared_data);
} }
@@ -1162,9 +1136,6 @@ void send_simple_mail(shared_ptr<ServerState> s, uint32_t from_guild_card_number
} }
} }
////////////////////////////////////////////////////////////////////////////////
// info board
template <TextEncoding NameEncoding, TextEncoding MessageEncoding> template <TextEncoding NameEncoding, TextEncoding MessageEncoding>
void send_info_board_t(shared_ptr<Client> c) { void send_info_board_t(shared_ptr<Client> c) {
vector<S_InfoBoardEntryT_D8<NameEncoding, MessageEncoding>> entries; vector<S_InfoBoardEntryT_D8<NameEncoding, MessageEncoding>> entries;
@@ -1229,10 +1200,7 @@ void send_choice_search_choices(shared_ptr<Client> c) {
} }
template <typename CommandHeaderT, TextEncoding Encoding> template <typename CommandHeaderT, TextEncoding Encoding>
void send_card_search_result_t( void send_card_search_result_t(shared_ptr<Client> c, shared_ptr<Client> result, shared_ptr<Lobby> result_lobby) {
shared_ptr<Client> c,
shared_ptr<Client> result,
shared_ptr<Lobby> result_lobby) {
auto s = c->require_server_state(); auto s = c->require_server_state();
S_GuildCardSearchResultT<CommandHeaderT, Encoding> cmd; S_GuildCardSearchResultT<CommandHeaderT, Encoding> cmd;
@@ -1263,10 +1231,7 @@ void send_card_search_result_t(
send_command_t(c, 0x41, 0x00, cmd); send_command_t(c, 0x41, 0x00, cmd);
} }
void send_card_search_result( void send_card_search_result(shared_ptr<Client> c, shared_ptr<Client> result, shared_ptr<Lobby> result_lobby) {
shared_ptr<Client> c,
shared_ptr<Client> result,
shared_ptr<Lobby> result_lobby) {
switch (c->version()) { switch (c->version()) {
case Version::DC_NTE: case Version::DC_NTE:
case Version::DC_11_2000: case Version::DC_11_2000:
@@ -1399,8 +1364,7 @@ void send_guild_card(
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 Version::XB_V3: case Version::XB_V3:
send_guild_card_xb( send_guild_card_xb(ch, guild_card_number, xb_user_id, name, description, language, section_id, char_class);
ch, guild_card_number, xb_user_id, name, description, language, section_id, char_class);
break; break;
case Version::BB_V4: case Version::BB_V4:
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);
@@ -1434,9 +1398,6 @@ void send_guild_card(shared_ptr<Client> c, shared_ptr<Client> source) {
source_p->disp.visual.char_class); source_p->disp.visual.char_class);
} }
////////////////////////////////////////////////////////////////////////////////
// menus
template <typename EntryT> template <typename EntryT>
void send_menu_t(shared_ptr<Client> c, shared_ptr<const Menu> menu, bool is_info_menu) { void send_menu_t(shared_ptr<Client> c, shared_ptr<const Menu> menu, bool is_info_menu) {
vector<EntryT> entries; vector<EntryT> entries;
@@ -1505,8 +1466,7 @@ void send_menu_t(shared_ptr<Client> c, shared_ptr<const Menu> menu, bool is_info
} }
} }
// See the description of the 07 command in CommandFormats.hh for details on // See the description of the 07 command in CommandFormats.hh for details on why we do this.
// why we do this.
if (is_pre_v1(c->version())) { if (is_pre_v1(c->version())) {
send_set_guild_card_number(c); send_set_guild_card_number(c);
} }
@@ -1524,10 +1484,7 @@ void send_menu(shared_ptr<Client> c, shared_ptr<const Menu> menu, bool is_info_m
} }
template <TextEncoding Encoding> template <TextEncoding Encoding>
void send_game_menu_t( void send_game_menu_t(shared_ptr<Client> c, bool is_spectator_team_list, bool show_tournaments_only) {
shared_ptr<Client> c,
bool is_spectator_team_list,
bool show_tournaments_only) {
auto s = c->require_server_state(); auto s = c->require_server_state();
vector<S_MenuItemT<Encoding>> entries; vector<S_MenuItemT<Encoding>> entries;
@@ -1623,10 +1580,7 @@ void send_game_menu_t(
send_command_vt(c, is_spectator_team_list ? 0xE6 : 0x08, entries.size() - 1, entries); send_command_vt(c, is_spectator_team_list ? 0xE6 : 0x08, entries.size() - 1, entries);
} }
void send_game_menu( void send_game_menu(shared_ptr<Client> c, bool is_spectator_team_list, bool show_tournaments_only) {
shared_ptr<Client> c,
bool is_spectator_team_list,
bool show_tournaments_only) {
if (is_v4(c->version())) { if (is_v4(c->version())) {
send_game_menu_t<TextEncoding::UTF16_ALWAYS_MARKED>(c, is_spectator_team_list, show_tournaments_only); send_game_menu_t<TextEncoding::UTF16_ALWAYS_MARKED>(c, is_spectator_team_list, show_tournaments_only);
} else if (uses_utf16(c->version())) { } else if (uses_utf16(c->version())) {
@@ -1824,12 +1778,10 @@ void send_quest_categories_menu(shared_ptr<Client> c, QuestMenuType menu_type, E
} }
void send_lobby_list(shared_ptr<Client> c) { void send_lobby_list(shared_ptr<Client> c) {
// DC v1 expects 10 lobbies in this list; DC v2 and later accept a variable // DC v1 expects 10 lobbies in this list; DC v2 and later accept a variable number, but other parts of the code
// number, but other parts of the code expect there to always be 15 lobbies. // expect there to always be 15 lobbies. Furthermore, there are only 16 entries in the array in TProtocol and the
// Furthermore, there are only 16 entries in the array in TProtocol and the // writes aren't bounds-checked, so the 83 command could overwrite later parts of TProtocol if more than 16 entries
// writes aren't bounds-checked, so the 83 command could overwrite later // are sent. (On Episode 3, there are 21 entries instead.)
// parts of TProtocol if more than 16 entries are sent. (On Episode 3, there
// are 21 entries instead.)
auto s = c->require_server_state(); auto s = c->require_server_state();
vector<S_LobbyListEntry_83> entries; vector<S_LobbyListEntry_83> entries;
@@ -1849,9 +1801,6 @@ void send_lobby_list(shared_ptr<Client> c) {
send_command_vt(c, 0x83, entries.size(), entries); send_command_vt(c, 0x83, entries.size(), entries);
} }
////////////////////////////////////////////////////////////////////////////////
// lobby joining
template <typename EntryT> template <typename EntryT>
void send_player_records_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Client> joining_client) { void send_player_records_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Client> joining_client) {
vector<EntryT> entries; vector<EntryT> entries;
@@ -1886,7 +1835,8 @@ void populate_lobby_data_for_client(LobbyDataT& ret, shared_ptr<const Client> c,
} }
template <> template <>
void populate_lobby_data_for_client(PlayerLobbyDataXB& ret, shared_ptr<const Client> c, shared_ptr<const Client> viewer_c) { void populate_lobby_data_for_client(
PlayerLobbyDataXB& ret, shared_ptr<const Client> c, shared_ptr<const Client> viewer_c) {
ret.player_tag = 0x00010000; ret.player_tag = 0x00010000;
ret.guild_card_number = c->login->account->account_id; ret.guild_card_number = c->login->account->account_id;
if (c->version() == Version::XB_V3) { if (c->version() == Version::XB_V3) {
@@ -1900,7 +1850,8 @@ void populate_lobby_data_for_client(PlayerLobbyDataXB& ret, shared_ptr<const Cli
} }
template <> template <>
void populate_lobby_data_for_client<PlayerLobbyDataBB>(PlayerLobbyDataBB& ret, shared_ptr<const Client> c, shared_ptr<const Client> viewer_c) { void populate_lobby_data_for_client<PlayerLobbyDataBB>(
PlayerLobbyDataBB& ret, shared_ptr<const Client> c, shared_ptr<const Client> viewer_c) {
ret.player_tag = 0x00010000; ret.player_tag = 0x00010000;
ret.guild_card_number = c->login->account->account_id; ret.guild_card_number = c->login->account->account_id;
ret.client_id = c->lobby_client_id; ret.client_id = c->lobby_client_id;
@@ -2146,7 +2097,8 @@ void send_join_game(shared_ptr<Client> c, shared_ptr<Lobby> l) {
auto& cmd_p = cmd.players_ep3[x]; auto& cmd_p = cmd.players_ep3[x];
cmd_p.inventory = other_p->inventory; cmd_p.inventory = other_p->inventory;
cmd_p.inventory.encode_for_client(c->version(), s->item_parameter_table_for_encode(c->version())); cmd_p.inventory.encode_for_client(c->version(), s->item_parameter_table_for_encode(c->version()));
cmd_p.disp = convert_player_disp_data<PlayerDispDataDCPCV3>(other_p->disp, c->language(), other_p->inventory.language); cmd_p.disp = convert_player_disp_data<PlayerDispDataDCPCV3>(
other_p->disp, c->language(), other_p->inventory.language);
cmd_p.disp.enforce_lobby_join_limits_for_version(c->version()); cmd_p.disp.enforce_lobby_join_limits_for_version(c->version());
uint32_t name_color = s->name_color_for_client(lc); uint32_t name_color = s->name_color_for_client(lc);
if (name_color) { if (name_color) {
@@ -2211,8 +2163,8 @@ void send_join_lobby_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cli
} else { } else {
lobby_type = l->block - 1; lobby_type = l->block - 1;
} }
// Allow non-canonical lobby types on GC. They may work on other versions too, // Allow non-canonical lobby types on GC. They may work on other versions too, but I haven't verified which values
// but I haven't verified which values don't crash on each version. // don't crash on each version.
switch (c->version()) { switch (c->version()) {
case Version::GC_EP3_NTE: case Version::GC_EP3_NTE:
case Version::GC_EP3: case Version::GC_EP3:
@@ -2343,8 +2295,7 @@ void send_join_lobby_xb(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cl
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_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;
if (l->is_game()) { if (l->is_game()) {
if (joining_client) { if (joining_client) {
@@ -2431,8 +2382,8 @@ void send_join_lobby(shared_ptr<Client> c, shared_ptr<Lobby> l) {
} }
} }
// If the client will stop sending message box close confirmations after // If the client will stop sending message box close confirmations after joining any lobby, set the appropriate flag
// joining any lobby, set the appropriate flag and update the client config // and update the client config
if (c->check_flag(Client::Flag::NO_D6_AFTER_LOBBY) && !c->check_flag(Client::Flag::NO_D6)) { if (c->check_flag(Client::Flag::NO_D6_AFTER_LOBBY) && !c->check_flag(Client::Flag::NO_D6)) {
c->set_flag(Client::Flag::NO_D6); c->set_flag(Client::Flag::NO_D6);
} }
@@ -2556,9 +2507,6 @@ asio::awaitable<GetPlayerInfoResult> send_get_player_info(shared_ptr<Client> c,
co_return co_await promise->get(); co_return co_await promise->get();
} }
////////////////////////////////////////////////////////////////////////////////
// Trade window
void send_execute_item_trade(shared_ptr<Client> c, const vector<ItemData>& items) { void send_execute_item_trade(shared_ptr<Client> c, const vector<ItemData>& items) {
auto s = c->require_server_state(); auto s = c->require_server_state();
@@ -2601,9 +2549,6 @@ void send_execute_card_trade(shared_ptr<Client> c, const vector<pair<uint32_t, u
send_command_t(c, 0xEE, 0xD3, cmd); send_command_t(c, 0xEE, 0xD3, cmd);
} }
////////////////////////////////////////////////////////////////////////////////
// arrows
void send_arrow_update(shared_ptr<Lobby> l) { void send_arrow_update(shared_ptr<Lobby> l) {
vector<S_ArrowUpdateEntry_88> entries; vector<S_ArrowUpdateEntry_88> entries;
@@ -2643,9 +2588,6 @@ void send_resume_game(shared_ptr<Lobby> l, shared_ptr<Client> ready_client) {
} }
} }
////////////////////////////////////////////////////////////////////////////////
// Game/cheat commands
static vector<G_UpdateEntityStat_6x9A> generate_stats_change_subcommands( static vector<G_UpdateEntityStat_6x9A> generate_stats_change_subcommands(
uint16_t client_id, PlayerStatsChange stat, uint32_t amount) { uint16_t client_id, PlayerStatsChange stat, uint32_t amount) {
if (amount > (0x7BF8 * 0xFF) / sizeof(G_UpdateEntityStat_6x9A)) { if (amount > (0x7BF8 * 0xFF) / sizeof(G_UpdateEntityStat_6x9A)) {
@@ -2688,8 +2630,7 @@ static G_ChangePlayerHP_6x2F generate_hp_restore_command(
static_cast<uint32_t>(what), amount, client_id}; static_cast<uint32_t>(what), amount, client_id};
} }
void send_change_player_hp( void send_change_player_hp(std::shared_ptr<Channel> ch, uint16_t client_id, PlayerHPChange what, int16_t amount) {
std::shared_ptr<Channel> ch, uint16_t client_id, PlayerHPChange what, int16_t amount) {
send_command_t(ch, 0x60, 0x00, generate_hp_restore_command(ch->version, client_id, what, amount)); send_command_t(ch, 0x60, 0x00, generate_hp_restore_command(ch->version, client_id, what, amount));
} }
@@ -2703,7 +2644,8 @@ asio::awaitable<void> send_change_player_hp(
} }
} }
asio::awaitable<void> send_change_player_hp(std::shared_ptr<Lobby> l, uint16_t client_id, PlayerHPChange what, int16_t amount) { asio::awaitable<void> send_change_player_hp(
std::shared_ptr<Lobby> l, uint16_t client_id, PlayerHPChange what, int16_t amount) {
for (const auto& lc : l->clients) { for (const auto& lc : l->clients) {
if (lc) { if (lc) {
co_await send_change_player_hp(lc, client_id, what, amount); co_await send_change_player_hp(lc, client_id, what, amount);
@@ -2756,10 +2698,12 @@ void send_game_join_sync_command(
c->log.info_f("Compressed sync data from ({:X} -> {:X} bytes):", size, compressed_data.size()); c->log.info_f("Compressed sync data from ({:X} -> {:X} bytes):", size, compressed_data.size());
phosg::print_data(stderr, data, size); phosg::print_data(stderr, data, size);
} }
send_game_join_sync_command_compressed(c, compressed_data.data(), compressed_data.size(), size, dc_nte_sc, dc_11_2000_sc, sc); send_game_join_sync_command_compressed(
c, compressed_data.data(), compressed_data.size(), size, dc_nte_sc, dc_11_2000_sc, sc);
} }
void send_game_join_sync_command(shared_ptr<Client> c, const string& data, uint8_t dc_nte_sc, uint8_t dc_11_2000_sc, uint8_t sc) { void send_game_join_sync_command(
shared_ptr<Client> c, const string& data, uint8_t dc_nte_sc, uint8_t dc_11_2000_sc, uint8_t sc) {
send_game_join_sync_command(c, data.data(), data.size(), dc_nte_sc, dc_11_2000_sc, sc); send_game_join_sync_command(c, data.data(), data.size(), dc_nte_sc, dc_11_2000_sc, sc);
} }
@@ -2826,11 +2770,10 @@ void send_game_item_state(shared_ptr<Client> c) {
for (size_t floor = 0; floor < 0x0F; floor++) { for (size_t floor = 0; floor < 0x0F; floor++) {
const auto& m = l->floor_item_managers.at(floor); const auto& m = l->floor_item_managers.at(floor);
// It's important that these are added in increasing order of item_id (hence // It's important that these are added in increasing order of item_id (hence why items is a map and not an
// why items is a map and not an unordered_map), since the game uses binary // unordered_map), since the game uses binary search to find floor items when picking them up. If items aren't in
// search to find floor items when picking them up. If items aren't in the // the correct order, the game may fail to find an item when attempting to pick it up, causing "ghost items" which
// correct order, the game may fail to find an item when attempting to pick // are visible but can't be picked up.
// it up, causing "ghost items" which are visible but can't be picked up.
for (const auto& it : m.items) { for (const auto& it : m.items) {
const auto& item = it.second; const auto& item = it.second;
if (!item->visible_to_client(c->lobby_client_id)) { if (!item->visible_to_client(c->lobby_client_id)) {
@@ -2858,8 +2801,8 @@ void send_game_item_state(shared_ptr<Client> c) {
const auto& data = decompressed_w.str(); const auto& data = decompressed_w.str();
send_game_join_sync_command(c, data.data(), data.size(), 0x5E, 0x65, 0x6D); send_game_join_sync_command(c, data.data(), data.size(), 0x5E, 0x65, 0x6D);
// Items on floors 0x0F and above can't be sent in the 6x6D command, so we // Items on floors 0x0F and above can't be sent in the 6x6D command, so we manually send 6x5D commands to create them
// manually send 6x5D commands to create them if needed // if needed
phosg::StringWriter w; phosg::StringWriter w;
for (size_t floor = 0x0F; floor < l->floor_item_managers.size(); floor++) { for (size_t floor = 0x0F; floor < l->floor_item_managers.size(); floor++) {
const auto& m = l->floor_item_managers[floor]; const auto& m = l->floor_item_managers[floor];
@@ -3061,8 +3004,7 @@ void send_game_flag_state_t(shared_ptr<Client> c) {
} }
void send_game_flag_state(shared_ptr<Client> c) { void send_game_flag_state(shared_ptr<Client> c) {
// DC NTE and 11/2000 don't have this command at all; v1 has it but it doesn't // DC NTE and 11/2000 don't have this command at all; v1 has it but it doesn't include flags for Ultimate.
// include flags for Ultimate.
if (is_pre_v1(c->version())) { if (is_pre_v1(c->version())) {
return; return;
} else if (is_v1(c->version())) { } else if (is_v1(c->version())) {
@@ -3102,8 +3044,8 @@ void send_game_player_state(shared_ptr<Client> to_c, shared_ptr<Client> from_c,
to_send.bonus_hp_from_materials = from_p->inventory.hp_from_materials; to_send.bonus_hp_from_materials = from_p->inventory.hp_from_materials;
to_send.bonus_tp_from_materials = from_p->inventory.tp_from_materials; to_send.bonus_tp_from_materials = from_p->inventory.tp_from_materials;
to_send.language = from_c->language(); to_send.language = from_c->language();
// TODO: Deal with telepipes. Probably we should track their state via the // TODO: Deal with telepipes. Probably we should track their state via the subcommands sent when they're
// subcommands sent when they're created/destroyed, but currently we don't. // created/destroyed, but currently we don't.
to_send.area = from_c->floor; to_send.area = from_c->floor;
to_send.technique_levels_v1 = from_p->disp.technique_levels_v1; to_send.technique_levels_v1 = from_p->disp.technique_levels_v1;
to_send.visual = from_p->disp.visual; to_send.visual = from_p->disp.visual;
@@ -3365,9 +3307,6 @@ void send_quest_function_call(shared_ptr<Client> c, uint16_t label) {
send_quest_function_call(c->channel, label); send_quest_function_call(c->channel, label);
} }
////////////////////////////////////////////////////////////////////////////////
// ep3 only commands
void send_ep3_card_list_update(shared_ptr<Client> c) { void send_ep3_card_list_update(shared_ptr<Client> c) {
if (!c->check_flag(Client::Flag::HAS_EP3_CARD_DEFS)) { if (!c->check_flag(Client::Flag::HAS_EP3_CARD_DEFS)) {
auto s = c->require_server_state(); auto s = c->require_server_state();
@@ -3485,16 +3424,11 @@ void send_ep3_tournament_list_t(shared_ptr<Client> c, bool is_for_spectator_team
? MenuID::TOURNAMENTS_FOR_SPEC ? MenuID::TOURNAMENTS_FOR_SPEC
: MenuID::TOURNAMENTS; : MenuID::TOURNAMENTS;
entry.item_id = tourn->get_menu_item_id(); entry.item_id = tourn->get_menu_item_id();
// TODO: What does it mean for a tournament to be locked? Should we support // TODO: What does it mean for a tournament to be locked? Should we support that?
// that? // TODO: Write appropriate round text (1st, 2nd, 3rd) here. This is nontrivial because unlike Sega's
// TODO: Write appropriate round text (1st, 2nd, 3rd) here. This is // implementation, newserv does not require a round to completely finish before starting matches in the next round,
// nontrivial because unlike Sega's implementation, newserv does not require
// a round to completely finish before starting matches in the next round,
// as long as the winners of the preceding matches have been determined. // as long as the winners of the preceding matches have been determined.
entry.state = entry.state = (tourn->get_state() == Episode3::Tournament::State::REGISTRATION) ? 0x00 : 0x05;
(tourn->get_state() == Episode3::Tournament::State::REGISTRATION)
? 0x00
: 0x05;
// TODO: Fill in cmd.start_time here when we implement scheduled starts. // TODO: Fill in cmd.start_time here when we implement scheduled starts.
entry.name.encode(tourn->get_name(), c->language()); entry.name.encode(tourn->get_name(), c->language());
const auto& teams = tourn->all_teams(); const auto& teams = tourn->all_teams();
@@ -3518,9 +3452,7 @@ void send_ep3_tournament_list(shared_ptr<Client> c, bool is_for_spectator_team_c
} }
void send_ep3_tournament_entry_list( void send_ep3_tournament_entry_list(
shared_ptr<Client> c, shared_ptr<Client> c, shared_ptr<const Episode3::Tournament> tourn, bool is_for_spectator_team_create) {
shared_ptr<const Episode3::Tournament> tourn,
bool is_for_spectator_team_create) {
S_TournamentEntryList_Ep3_E2 cmd; S_TournamentEntryList_Ep3_E2 cmd;
cmd.players_per_team = (tourn->get_flags() & Episode3::Tournament::Flag::IS_2V2) ? 2 : 1; cmd.players_per_team = (tourn->get_flags() & Episode3::Tournament::Flag::IS_2V2) ? 2 : 1;
size_t z = 0; size_t z = 0;
@@ -3549,9 +3481,7 @@ void send_ep3_tournament_entry_list(
} }
template <typename RulesT> template <typename RulesT>
void send_ep3_tournament_details_t( void send_ep3_tournament_details_t(shared_ptr<Client> c, shared_ptr<const Episode3::Tournament> tourn) {
shared_ptr<Client> c,
shared_ptr<const Episode3::Tournament> tourn) {
S_TournamentGameDetailsBaseT_Ep3_E3<RulesT> cmd; S_TournamentGameDetailsBaseT_Ep3_E3<RulesT> cmd;
auto vm = tourn->get_map()->version(c->language()); auto vm = tourn->get_map()->version(c->language());
cmd.tournament_name.encode(tourn->get_name(), c->language()); cmd.tournament_name.encode(tourn->get_name(), c->language());
@@ -3570,9 +3500,7 @@ void send_ep3_tournament_details_t(
send_command_t(c, 0xE3, 0x02, cmd); send_command_t(c, 0xE3, 0x02, cmd);
} }
void send_ep3_tournament_details( void send_ep3_tournament_details(shared_ptr<Client> c, shared_ptr<const Episode3::Tournament> tourn) {
shared_ptr<Client> c,
shared_ptr<const Episode3::Tournament> tourn) {
if (c->version() == Version::GC_EP3_NTE) { if (c->version() == Version::GC_EP3_NTE) {
send_ep3_tournament_details_t<Episode3::RulesTrial>(c, tourn); send_ep3_tournament_details_t<Episode3::RulesTrial>(c, tourn);
} else { } else {
@@ -3690,11 +3618,9 @@ void send_ep3_game_details_t(shared_ptr<Client> c, shared_ptr<Lobby> l) {
} }
} }
// There is a client bug that causes the spectators list to always be // There is a client bug that causes the spectators list to always be empty when sent with E1, because there's no
// empty when sent with E1, because there's no way for E1 to set the // way for E1 to set the spectator count in the info window object. To account for this, we send a mostly-blank
// spectator count in the info window object. To account for this, we send // E3 to set the spectator count, followed by an E1 with the correct data.
// a mostly-blank E3 to set the spectator count, followed by an E1 with
// the correct data.
S_TournamentGameDetailsBaseT_Ep3_E3<RulesT> cmd_E3; S_TournamentGameDetailsBaseT_Ep3_E3<RulesT> cmd_E3;
cmd_E3.num_spectators = num_spectators; cmd_E3.num_spectators = num_spectators;
send_command_t(c, 0xE3, 0x04, cmd_E3); send_command_t(c, 0xE3, 0x04, cmd_E3);
@@ -3825,15 +3751,13 @@ void send_ep3_tournament_match_result(shared_ptr<Lobby> l, uint32_t meseta_rewar
write_player_names(cmd.names_entries[0], match->preceding_a->winner_team); write_player_names(cmd.names_entries[0], match->preceding_a->winner_team);
cmd.names_entries[1].team_name.encode(match->preceding_b->winner_team->name, lc->language()); cmd.names_entries[1].team_name.encode(match->preceding_b->winner_team->name, lc->language());
write_player_names(cmd.names_entries[1], match->preceding_b->winner_team); write_player_names(cmd.names_entries[1], match->preceding_b->winner_team);
// The value 6 here causes the client to show the "Congratulations" text // The value 6 here causes the client to show the "Congratulations" text instead of "On to the next round"
// instead of "On to the next round"
cmd.round_num = (match == tourn->get_final_match()) ? 6 : match->round_num; cmd.round_num = (match == tourn->get_final_match()) ? 6 : match->round_num;
cmd.num_players_per_team = match->preceding_a->winner_team->max_players; cmd.num_players_per_team = match->preceding_a->winner_team->max_players;
cmd.winner_team_id = (match->preceding_b->winner_team == match->winner_team); cmd.winner_team_id = (match->preceding_b->winner_team == match->winner_team);
cmd.meseta_amount = meseta_reward; cmd.meseta_amount = meseta_reward;
cmd.meseta_reward_text.encode("You got %s meseta!", Language::ENGLISH); cmd.meseta_reward_text.encode("You got %s meseta!", Language::ENGLISH);
if ((lc->version() != Version::GC_EP3_NTE) && if ((lc->version() != Version::GC_EP3_NTE) && !(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
!(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
uint8_t mask_key = (phosg::random_object<uint32_t>() % 0xFF) + 1; uint8_t mask_key = (phosg::random_object<uint32_t>() % 0xFF) + 1;
set_mask_for_ep3_game_command(&cmd, sizeof(cmd), mask_key); set_mask_for_ep3_game_command(&cmd, sizeof(cmd), mask_key);
} }
@@ -3935,15 +3859,14 @@ void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key) {
return; return;
} }
// If header->mask_key isn't zero when we get here, then the command is // If header->mask_key isn't zero when we get here, then the command is already masked with a different mask_key, so
// already masked with a different mask_key, so unmask it first // unmask it first
if ((mask_key != 0) && (header->mask_key != 0)) { if ((mask_key != 0) && (header->mask_key != 0)) {
set_mask_for_ep3_game_command(vdata, size, 0); set_mask_for_ep3_game_command(vdata, size, 0);
} }
// Now, exactly one of header->mask_key and mask_key should be nonzero, and we // Now, exactly one of header->mask_key and mask_key should be nonzero, and we are either directly masking or
// are either directly masking or unmasking the command. Since this operation // unmasking the command. Since this operation is symmetric, we don't need to split it into two cases.
// is symmetric, we don't need to split it into two cases.
if ((header->mask_key == 0) == (mask_key == 0)) { if ((header->mask_key == 0) == (mask_key == 0)) {
throw logic_error("only one of header->mask_key and mask_key may be nonzero"); throw logic_error("only one of header->mask_key and mask_key may be nonzero");
} }
@@ -4069,11 +3992,9 @@ void send_open_quest_file(
throw logic_error("cannot send quest files to this version of client"); throw logic_error("cannot send quest files to this version of client");
} }
// On most versions, we can trust the TCP stack to do the right thing when we // On most versions, we can trust the TCP stack to do the right thing when we send a lot of data at once, but on GC,
// send a lot of data at once, but on GC, the client will crash if too much // the client will crash if too much quest data is sent at once. This is likely a bug in the TCP stack, since the
// quest data is sent at once. This is likely a bug in the TCP stack, since // client should apply backpressure to avoid bad situations, but we have to deal with it here instead.
// the client should apply backpressure to avoid bad situations, but we have
// to deal with it here instead.
size_t total_chunks = (contents->size() + 0x3FF) / 0x400; size_t total_chunks = (contents->size() + 0x3FF) / 0x400;
size_t chunks_to_send = is_v1_or_v2(c->version()) ? total_chunks : min<size_t>(V3_V4_QUEST_LOAD_MAX_CHUNKS_IN_FLIGHT, total_chunks); size_t chunks_to_send = is_v1_or_v2(c->version()) ? total_chunks : min<size_t>(V3_V4_QUEST_LOAD_MAX_CHUNKS_IN_FLIGHT, total_chunks);
@@ -4086,8 +4007,8 @@ void send_open_quest_file(
send_quest_file_chunk(c, filename, offset / 0x400, contents->data() + offset, chunk_bytes, (type != QuestFileType::ONLINE)); send_quest_file_chunk(c, filename, offset / 0x400, contents->data() + offset, chunk_bytes, (type != QuestFileType::ONLINE));
} }
// If there are still chunks to send, track the file so the chunk // If there are still chunks to send, track the file so the chunk acknowledgement handler (13 or A7) can know what to
// acknowledgement handler (13 or A7) can know what to send next // send next
if (chunks_to_send < total_chunks) { if (chunks_to_send < total_chunks) {
c->sending_files.emplace(filename, contents); c->sending_files.emplace(filename, contents);
c->log.info_f("Opened file {}", filename); c->log.info_f("Opened file {}", filename);
@@ -4232,7 +4153,7 @@ void send_server_time(shared_ptr<Client> c) {
string time_str(128, 0); string time_str(128, 0);
size_t len = strftime(time_str.data(), time_str.size(), "%Y:%m:%d: %H:%M:%S.000", &t_parsed); size_t len = strftime(time_str.data(), time_str.size(), "%Y:%m:%d: %H:%M:%S.000", &t_parsed);
if (len == 0) { if (len == 0) { // 128 should always be long enough
throw logic_error("strftime buffer too short"); throw logic_error("strftime buffer too short");
} }
time_str.resize(len); time_str.resize(len);
@@ -4262,16 +4183,12 @@ void send_change_event(shared_ptr<Lobby> l, uint8_t new_event) {
} }
void send_change_event(shared_ptr<ServerState> s, uint8_t new_event) { void send_change_event(shared_ptr<ServerState> s, uint8_t new_event) {
// TODO: Create a collection of all clients on the server (including those not // TODO: Create a collection of all clients on the server (including those not in lobbies) and use that here instead
// in lobbies) and use that here instead
for (auto& l : s->all_lobbies()) { for (auto& l : s->all_lobbies()) {
send_change_event(l, new_event); send_change_event(l, new_event);
} }
} }
////////////////////////////////////////////////////////////////////////////////
// BB teams
void send_update_team_membership(shared_ptr<Client> c) { void send_update_team_membership(shared_ptr<Client> c) {
auto team = c->team(); auto team = c->team();
S_UpdateTeamMembership_BB_12EA cmd; S_UpdateTeamMembership_BB_12EA cmd;
@@ -4362,8 +4279,7 @@ void send_intra_team_ranking(shared_ptr<Client> c) {
throw runtime_error("client is not in a team"); throw runtime_error("client is not in a team");
} }
// TODO: At some point we should maintain a sorted index instead of sorting // TODO: At some point we should maintain a sorted index instead of sorting these on-demand.
// these on-demand.
vector<const TeamIndex::Team::Member*> members; vector<const TeamIndex::Team::Member*> members;
for (const auto& it : team->members) { for (const auto& it : team->members) {
members.emplace_back(&it.second); members.emplace_back(&it.second);
@@ -4395,8 +4311,7 @@ void send_intra_team_ranking(shared_ptr<Client> c) {
void send_cross_team_ranking(shared_ptr<Client> c) { void send_cross_team_ranking(shared_ptr<Client> c) {
auto s = c->require_server_state(); auto s = c->require_server_state();
// TODO: At some point we should maintain a sorted index instead of sorting // TODO: At some point we should maintain a sorted index instead of sorting these on-demand.
// these on-demand.
auto teams = s->team_index->all(); auto teams = s->team_index->all();
auto rank_fn = +[](const shared_ptr<const TeamIndex::Team>& a, const shared_ptr<const TeamIndex::Team>& b) { auto rank_fn = +[](const shared_ptr<const TeamIndex::Team>& a, const shared_ptr<const TeamIndex::Team>& b) {
return a->points > b->points; return a->points > b->points;
@@ -4433,9 +4348,8 @@ void send_team_reward_list(shared_ptr<Client> c, bool show_purchased) {
vector<S_TeamRewardList_BB_19EA_1AEA::Entry> entries; vector<S_TeamRewardList_BB_19EA_1AEA::Entry> entries;
for (const auto& reward : s->team_index->reward_definitions()) { for (const auto& reward : s->team_index->reward_definitions()) {
// In the buy menu, hide rewards that can't be bought again (that is, unique // In the buy menu, hide rewards that can't be bought again (that is, unique rewards that the team already has). In
// rewards that the team already has). In the bought menu, hide rewards that // the bought menu, hide rewards that the team does not have or that can be bought again.
// the team does not have or that can be bought again.
if (show_purchased != (team->has_reward(reward.key) && reward.is_unique)) { if (show_purchased != (team->has_reward(reward.key) && reward.is_unique)) {
continue; continue;
} }
+33 -73
View File
@@ -24,27 +24,25 @@ extern const std::unordered_set<std::string> bb_crypt_initial_client_commands;
constexpr size_t V3_V4_QUEST_LOAD_MAX_CHUNKS_IN_FLIGHT = 4; constexpr size_t V3_V4_QUEST_LOAD_MAX_CHUNKS_IN_FLIGHT = 4;
// TODO: Many of these functions should take a shared_ptr<Channel> instead of a // TODO: Many of these functions should take a shared_ptr<Channel> instead of a shared_ptr<Client>. Refactor functions
// shared_ptr<Client>. Refactor functions appropriately. // appropriately.
// Note: There are so many versions of this function for a few reasons: // Note: There are so many versions of this function for a few reasons:
// - There are a lot of different target types (sometimes we want to send a // - There are a lot of different target types (sometimes we want to send a command to one client, sometimes to
// command to one client, sometimes to everyone in a lobby, etc.) // everyone in a lobby, etc.)
// - For the const void* versions, the data and size arguments should not be // - For the const void* versions, the data and size arguments should not be independently optional - this can lead to
// independently optional - this can lead to bugs where a non-null data // bugs where a non-null data pointer is given but size is accidentally not given (e.g. if the type of data in the
// pointer is given but size is accidentally not given (e.g. if the type of // calling function is changed from string to void*).
// data in the calling function is changed from string to void*).
template <typename CmdT> template <typename CmdT>
void send_or_enqueue_command( void send_or_enqueue_command(std::shared_ptr<Client> c, uint16_t command, uint32_t flag, const CmdT& cmd) {
std::shared_ptr<Client> c, uint16_t command, uint32_t flag, const CmdT& cmd) {
if (c->game_join_command_queue) { if (c->game_join_command_queue) {
c->log.info_f("Client not ready to receive game commands; adding to queue"); c->log.info_f("Client not ready to receive game commands; adding to queue");
auto& q_cmd = c->game_join_command_queue->emplace_back(); auto& q_cmd = c->game_join_command_queue->emplace_back();
q_cmd.command = command; q_cmd.command = command;
q_cmd.flag = flag; q_cmd.flag = flag;
// TODO: It'd be nice to avoid this copy. Maybe take in a pointer to cmd // TODO: It'd be nice to avoid this copy. Maybe take in a pointer to cmd and move it into q_cmd somehow, so q_cmd
// and move it into q_cmd somehow, so q_cmd can free it when needed? // can free it when needed?
q_cmd.data.assign(reinterpret_cast<const char*>(&cmd), sizeof(cmd)); q_cmd.data.assign(reinterpret_cast<const char*>(&cmd), sizeof(cmd));
} else { } else {
send_command(c, command, flag, &cmd, sizeof(cmd)); send_command(c, command, flag, &cmd, sizeof(cmd));
@@ -125,8 +123,7 @@ void send_command_t_vt(
const StructT& data, const StructT& data,
const std::vector<EntryT>& array_data) { const std::vector<EntryT>& array_data) {
std::string all_data(reinterpret_cast<const char*>(&data), sizeof(StructT)); std::string all_data(reinterpret_cast<const char*>(&data), sizeof(StructT));
all_data.append(reinterpret_cast<const char*>(array_data.data()), all_data.append(reinterpret_cast<const char*>(array_data.data()), array_data.size() * sizeof(EntryT));
array_data.size() * sizeof(EntryT));
send_command(c, command, flag, all_data.data(), all_data.size()); send_command(c, command, flag, all_data.data(), all_data.size());
} }
@@ -138,13 +135,10 @@ enum SendServerInitFlag {
}; };
S_ServerInitWithAfterMessageT_DC_PC_V3_02_17_91_9B<0xB4> S_ServerInitWithAfterMessageT_DC_PC_V3_02_17_91_9B<0xB4>
prepare_server_init_contents_console( prepare_server_init_contents_console(uint32_t server_key, uint32_t client_key, uint8_t flags);
uint32_t server_key, uint32_t client_key, uint8_t flags);
S_ServerInitWithAfterMessageT_BB_03_9B<0xB4> S_ServerInitWithAfterMessageT_BB_03_9B<0xB4>
prepare_server_init_contents_bb( prepare_server_init_contents_bb(
const parray<uint8_t, 0x30>& server_key, const parray<uint8_t, 0x30>& server_key, const parray<uint8_t, 0x30>& client_key, uint8_t flags);
const parray<uint8_t, 0x30>& client_key,
uint8_t flags);
void send_server_init(std::shared_ptr<Client> c, uint8_t flags); void send_server_init(std::shared_ptr<Client> c, uint8_t flags);
void send_set_guild_card_number(std::shared_ptr<Client> c); void send_set_guild_card_number(std::shared_ptr<Client> c);
@@ -164,9 +158,8 @@ std::string prepare_send_function_call_data(
uint32_t checksum_size, uint32_t checksum_size,
uint32_t override_relocations_offset, uint32_t override_relocations_offset,
bool use_encrypted_format); bool use_encrypted_format);
// NOTE: The two versions of send_function_call behave differently. The version // NOTE: The two versions of send_function_call behave differently. The version that takes a Channel returns
// that takes a Channel returns immediately; the version that takes a Client // immediately; the version that takes a Client does not return until the client has sent the response.
// does not return until the client has sent the response.
void send_function_call( void send_function_call(
std::shared_ptr<Channel> ch, std::shared_ptr<Channel> ch,
uint64_t client_enabled_flags, uint64_t client_enabled_flags,
@@ -189,17 +182,13 @@ asio::awaitable<C_ExecuteCodeResult_B3> send_function_call(
uint32_t override_relocations_offset = 0, uint32_t override_relocations_offset = 0,
bool ignore_actually_runs_code_flag = false); bool ignore_actually_runs_code_flag = false);
asio::awaitable<void> send_function_call_multi( asio::awaitable<void> send_function_call_multi(
std::shared_ptr<Client> c, std::shared_ptr<Client> c, std::unordered_set<std::shared_ptr<const CompiledFunctionCode>> codes);
std::unordered_set<std::shared_ptr<const CompiledFunctionCode>> codes);
asio::awaitable<bool> send_protected_command(std::shared_ptr<Client> c, const void* data, size_t size, bool echo_to_lobby); asio::awaitable<bool> send_protected_command(std::shared_ptr<Client> c, const void* data, size_t size, bool echo_to_lobby);
asio::awaitable<void> send_dol_file(std::shared_ptr<Client> c, std::shared_ptr<DOLFileIndex::File> dol); asio::awaitable<void> send_dol_file(std::shared_ptr<Client> c, std::shared_ptr<DOLFileIndex::File> dol);
void send_reconnect(std::shared_ptr<Client> c, uint32_t address, uint16_t port); void send_reconnect(std::shared_ptr<Client> c, uint32_t address, uint16_t port);
void send_pc_console_split_reconnect( void send_pc_console_split_reconnect(
std::shared_ptr<Client> c, std::shared_ptr<Client> c, uint32_t address, uint16_t pc_port, uint16_t console_port);
uint32_t address,
uint16_t pc_port,
uint16_t console_port);
void send_client_init_bb(std::shared_ptr<Client> c, uint32_t error); void send_client_init_bb(std::shared_ptr<Client> c, uint32_t error);
void send_system_file_bb(std::shared_ptr<Client> c); void send_system_file_bb(std::shared_ptr<Client> c);
@@ -227,7 +216,8 @@ void send_scrolling_message_bb(std::shared_ptr<Client> c, const std::string& tex
void send_text_or_scrolling_message(std::shared_ptr<Client> c, const std::string& text, const std::string& scrolling); void send_text_or_scrolling_message(std::shared_ptr<Client> c, const std::string& text, const std::string& scrolling);
void send_text_or_scrolling_message( void send_text_or_scrolling_message(
std::shared_ptr<Lobby> l, std::shared_ptr<Client> exclude_c, const std::string& text, const std::string& scrolling); std::shared_ptr<Lobby> l, std::shared_ptr<Client> exclude_c, const std::string& text, const std::string& scrolling);
void send_text_or_scrolling_message(std::shared_ptr<ServerState> s, const std::string& text, const std::string& scrolling); void send_text_or_scrolling_message(
std::shared_ptr<ServerState> s, const std::string& text, const std::string& scrolling);
std::string prepare_chat_data( std::string prepare_chat_data(
Version version, Version version,
@@ -236,18 +226,11 @@ std::string prepare_chat_data(
const std::string& from_name, const std::string& from_name,
const std::string& text, const std::string& text,
char private_flags); char private_flags);
void send_chat_message_from_client( void send_chat_message_from_client(std::shared_ptr<Channel> ch, const std::string& text, char private_flags);
std::shared_ptr<Channel> ch,
const std::string& text,
char private_flags);
void send_prepared_chat_message( void send_prepared_chat_message(
std::shared_ptr<Client> c, std::shared_ptr<Client> c, uint32_t from_guild_card_number, const std::string& prepared_data);
uint32_t from_guild_card_number,
const std::string& prepared_data);
void send_prepared_chat_message( void send_prepared_chat_message(
std::shared_ptr<Lobby> l, std::shared_ptr<Lobby> l, uint32_t from_guild_card_number, const std::string& prepared_data);
uint32_t from_guild_card_number,
const std::string& prepared_data);
void send_chat_message( void send_chat_message(
std::shared_ptr<Client> c, std::shared_ptr<Client> c,
uint32_t from_guild_card_number, uint32_t from_guild_card_number,
@@ -255,10 +238,7 @@ void send_chat_message(
const std::string& text, const std::string& text,
char private_flags); char private_flags);
void send_simple_mail( void send_simple_mail(
std::shared_ptr<Client> c, std::shared_ptr<Client> c, uint32_t from_guild_card_number, const std::string& from_name, const std::string& text);
uint32_t from_guild_card_number,
const std::string& from_name,
const std::string& text);
void send_simple_mail( void send_simple_mail(
std::shared_ptr<ServerState> s, std::shared_ptr<ServerState> s,
uint32_t from_guild_card_number, uint32_t from_guild_card_number,
@@ -287,9 +267,7 @@ void send_info_board(std::shared_ptr<Client> c);
void send_choice_search_choices(std::shared_ptr<Client> c); void send_choice_search_choices(std::shared_ptr<Client> c);
void send_card_search_result( void send_card_search_result(
std::shared_ptr<Client> c, std::shared_ptr<Client> c, std::shared_ptr<Client> result, std::shared_ptr<Lobby> result_lobby);
std::shared_ptr<Client> result,
std::shared_ptr<Lobby> result_lobby);
void send_guild_card( void send_guild_card(
std::shared_ptr<Channel> ch, std::shared_ptr<Channel> ch,
@@ -302,12 +280,8 @@ void send_guild_card(
uint8_t section_id, uint8_t section_id,
uint8_t char_class); uint8_t char_class);
void send_guild_card(std::shared_ptr<Client> c, std::shared_ptr<Client> source); void send_guild_card(std::shared_ptr<Client> c, std::shared_ptr<Client> source);
void send_menu( void send_menu(std::shared_ptr<Client> c, std::shared_ptr<const Menu> menu, bool is_info_menu = false);
std::shared_ptr<Client> c, std::shared_ptr<const Menu> menu, bool is_info_menu = false); void send_game_menu(std::shared_ptr<Client> c, bool is_spectator_team_list, bool is_tournament_game_list);
void send_game_menu(
std::shared_ptr<Client> c,
bool is_spectator_team_list,
bool is_tournament_game_list);
void send_quest_menu( void send_quest_menu(
std::shared_ptr<Client> c, std::shared_ptr<Client> c,
const std::vector<std::pair<QuestIndex::IncludeState, std::shared_ptr<const Quest>>>& quests, const std::vector<std::pair<QuestIndex::IncludeState, std::shared_ptr<const Quest>>>& quests,
@@ -408,35 +382,21 @@ void send_quest_function_call(std::shared_ptr<Client> c, uint16_t label);
void send_ep3_card_list_update(std::shared_ptr<Client> c); void send_ep3_card_list_update(std::shared_ptr<Client> c);
void send_ep3_media_update( void send_ep3_media_update(
std::shared_ptr<Client> c, std::shared_ptr<Client> c, uint32_t type, uint32_t which, const std::string& compressed_data);
uint32_t type,
uint32_t which,
const std::string& compressed_data);
void send_ep3_rank_update(std::shared_ptr<Client> c); void send_ep3_rank_update(std::shared_ptr<Client> c);
void send_ep3_card_battle_table_state(std::shared_ptr<Lobby> l, uint16_t table_number); void send_ep3_card_battle_table_state(std::shared_ptr<Lobby> l, uint16_t table_number);
void send_ep3_set_context_token(std::shared_ptr<Client> c, uint32_t context_token); void send_ep3_set_context_token(std::shared_ptr<Client> c, uint32_t context_token);
void send_ep3_confirm_tournament_entry( void send_ep3_confirm_tournament_entry(std::shared_ptr<Client> c, std::shared_ptr<const Episode3::Tournament> t);
std::shared_ptr<Client> c, void send_ep3_tournament_list(std::shared_ptr<Client> c, bool is_for_spectator_team_create);
std::shared_ptr<const Episode3::Tournament> t);
void send_ep3_tournament_list(
std::shared_ptr<Client> c,
bool is_for_spectator_team_create);
void send_ep3_tournament_entry_list( void send_ep3_tournament_entry_list(
std::shared_ptr<Client> c, std::shared_ptr<Client> c, std::shared_ptr<const Episode3::Tournament> t, bool is_for_spectator_team_create);
std::shared_ptr<const Episode3::Tournament> t, void send_ep3_tournament_info(std::shared_ptr<Client> c, std::shared_ptr<const Episode3::Tournament> t);
bool is_for_spectator_team_create);
void send_ep3_tournament_info(
std::shared_ptr<Client> c,
std::shared_ptr<const Episode3::Tournament> t);
void send_ep3_set_tournament_player_decks(std::shared_ptr<Client> c); void send_ep3_set_tournament_player_decks(std::shared_ptr<Client> c);
void send_ep3_tournament_match_result(std::shared_ptr<Lobby> l, uint32_t meseta_reward); void send_ep3_tournament_match_result(std::shared_ptr<Lobby> l, uint32_t meseta_reward);
void send_ep3_tournament_details( void send_ep3_tournament_details(std::shared_ptr<Client> c, std::shared_ptr<const Episode3::Tournament> t);
std::shared_ptr<Client> c, void send_ep3_game_details(std::shared_ptr<Client> c, std::shared_ptr<Lobby> l);
std::shared_ptr<const Episode3::Tournament> t);
void send_ep3_game_details(
std::shared_ptr<Client> c, std::shared_ptr<Lobby> l);
void send_ep3_update_game_metadata(std::shared_ptr<Lobby> l); void send_ep3_update_game_metadata(std::shared_ptr<Lobby> l);
void send_ep3_card_auction(std::shared_ptr<Lobby> l); void send_ep3_card_auction(std::shared_ptr<Lobby> l);
void send_ep3_disband_watcher_lobbies(std::shared_ptr<Lobby> primary_l); void send_ep3_disband_watcher_lobbies(std::shared_ptr<Lobby> primary_l);
+8 -12
View File
@@ -29,9 +29,8 @@ public:
Server& operator=(Server&&) = delete; Server& operator=(Server&&) = delete;
virtual ~Server() = default; virtual ~Server() = default;
// Generally subclasses will implement listen(), which should create a // Generally subclasses will implement listen(), which should create a SocketT object (of their desired type) with a
// SocketT object (of their desired type) with a valid endpoint and call // valid endpoint and call add_socket to actually listen on that endpoint
// add_socket to actually listen on that endpoint
void add_socket(std::shared_ptr<SocketT> sock) { void add_socket(std::shared_ptr<SocketT> sock) {
sock->acceptor = std::make_unique<asio::ip::tcp::acceptor>(*this->io_context, sock->endpoint); sock->acceptor = std::make_unique<asio::ip::tcp::acceptor>(*this->io_context, sock->endpoint);
asio::co_spawn(*this->io_context, this->accept_connections(sock), asio::detached); asio::co_spawn(*this->io_context, this->accept_connections(sock), asio::detached);
@@ -56,19 +55,16 @@ protected:
std::unordered_set<std::shared_ptr<SocketT>> sockets; std::unordered_set<std::shared_ptr<SocketT>> sockets;
std::unordered_set<std::shared_ptr<ClientT>> clients; std::unordered_set<std::shared_ptr<ClientT>> clients;
// create_client is called when a new socket is opened. It should create (and // create_client is called when a new socket is opened. It should create (and return) the ClientT object, or may
// return) the ClientT object, or may close client_sock and return nullptr if // close client_sock and return nullptr if it decides to reject the connection. create_client should NOT send or
// it decides to reject the connection. create_client should NOT send or
// receive any data, hence it is not a coroutine. // receive any data, hence it is not a coroutine.
[[nodiscard]] virtual std::shared_ptr<ClientT> create_client( [[nodiscard]] virtual std::shared_ptr<ClientT> create_client(
std::shared_ptr<SocketT> sock, asio::ip::tcp::socket&& client_sock) = 0; std::shared_ptr<SocketT> sock, asio::ip::tcp::socket&& client_sock) = 0;
// handle_client is called immediately after create_client if create_client // handle_client is called immediately after create_client if create_client did not return nullptr. It should handle
// did not return nullptr. It should handle all sending and receiving of data // all sending and receiving of data on the client's connection.
// on the client's connection.
virtual asio::awaitable<void> handle_client(std::shared_ptr<ClientT> c) = 0; virtual asio::awaitable<void> handle_client(std::shared_ptr<ClientT> c) = 0;
// destroy_client is called when the client is about to be destroyed, often // destroy_client is called when the client is about to be destroyed, often after it has disconnected (hence, it
// after it has disconnected (hence, it cannot assume that it can send or // cannot assume that it can send or receive any data). Additionally, the client has already been removed from
// receive any data). Additionally, the client has already been removed from
// this->clients at the time this is called. // this->clients at the time this is called.
virtual asio::awaitable<void> destroy_client(std::shared_ptr<ClientT> c) { virtual asio::awaitable<void> destroy_client(std::shared_ptr<ClientT> c) {
(void)c; (void)c;
+3 -6
View File
@@ -16,8 +16,7 @@
using namespace std; using namespace std;
ServerShell::ServerShell(shared_ptr<ServerState> state) ServerShell::ServerShell(shared_ptr<ServerState> state)
: state(state), : state(state), th(&ServerShell::thread_fn, this) {}
th(&ServerShell::thread_fn, this) {}
ServerShell::~ServerShell() { ServerShell::~ServerShell() {
if (this->th.joinable()) { if (this->th.joinable()) {
@@ -34,10 +33,8 @@ void ServerShell::thread_fn() {
try { try {
command = phosg::fgets(stdin); command = phosg::fgets(stdin);
} catch (const phosg::io_error& e) { } catch (const phosg::io_error& e) {
// Cygwin sometimes causes fgets() to fail with errno -1 when the // Cygwin sometimes causes fgets() to fail with errno -1 when the terminal window is resized. We ignore these
// terminal window is resized. We ignore these events unless the read // events unless the read failed immediately (which probably means it would fail again if we retried immediately)
// failed immediately (which probably means it would fail again if we
// retried immediately).
if (phosg::now() - read_start_usecs < 1000000 || e.error != -1) { if (phosg::now() - read_start_usecs < 1000000 || e.error != -1) {
throw; throw;
} }
+36 -47
View File
@@ -51,7 +51,8 @@ CheatFlags::CheatFlags(const phosg::JSON& json) : CheatFlags() {
} }
ServerState::QuestF960Result::QuestF960Result(const phosg::JSON& json, shared_ptr<const ItemNameIndex> name_index) { ServerState::QuestF960Result::QuestF960Result(const phosg::JSON& json, shared_ptr<const ItemNameIndex> name_index) {
static const array<string, 7> day_names = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; static const array<string, 7> day_names = {
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
this->meseta_cost = json.get_int("MesetaCost", 0); this->meseta_cost = json.get_int("MesetaCost", 0);
this->base_probability = json.get_int("BaseProbability", 0); this->base_probability = json.get_int("BaseProbability", 0);
this->probability_upgrade = json.get_int("ProbabilityUpgrade", 0); this->probability_upgrade = json.get_int("ProbabilityUpgrade", 0);
@@ -60,7 +61,8 @@ ServerState::QuestF960Result::QuestF960Result(const phosg::JSON& json, shared_pt
try { try {
this->results[day].emplace_back(name_index->parse_item_description(item_it->as_string())); this->results[day].emplace_back(name_index->parse_item_description(item_it->as_string()));
} catch (const exception& e) { } catch (const exception& e) {
config_log.warning_f("Cannot parse item description \"{}\": {} (skipping entry)", item_it->as_string(), e.what()); config_log.warning_f(
"Cannot parse item description \"{}\": {} (skipping entry)", item_it->as_string(), e.what());
} }
} }
} }
@@ -81,10 +83,7 @@ void ServerState::add_client_to_available_lobby(shared_ptr<Client> c) {
if (c->preferred_lobby_id >= 0) { if (c->preferred_lobby_id >= 0) {
try { try {
auto l = this->find_lobby(c->preferred_lobby_id); auto l = this->find_lobby(c->preferred_lobby_id);
if (l && if (l && !l->is_game() && l->check_flag(Lobby::Flag::PUBLIC) && l->version_is_allowed(c->version())) {
!l->is_game() &&
l->check_flag(Lobby::Flag::PUBLIC) &&
l->version_is_allowed(c->version())) {
l->add_client(c); l->add_client(c);
added_to_lobby = l; added_to_lobby = l;
} }
@@ -96,10 +95,7 @@ void ServerState::add_client_to_available_lobby(shared_ptr<Client> c) {
for (const auto& lobby_id : this->public_lobby_search_order(c)) { for (const auto& lobby_id : this->public_lobby_search_order(c)) {
try { try {
auto l = this->find_lobby(lobby_id); auto l = this->find_lobby(lobby_id);
if (l && if (l && !l->is_game() && l->check_flag(Lobby::Flag::PUBLIC) && l->version_is_allowed(c->version())) {
!l->is_game() &&
l->check_flag(Lobby::Flag::PUBLIC) &&
l->version_is_allowed(c->version())) {
l->add_client(c); l->add_client(c);
added_to_lobby = l; added_to_lobby = l;
break; break;
@@ -135,10 +131,7 @@ void ServerState::remove_client_from_lobby(shared_ptr<Client> c) {
} }
bool ServerState::change_client_lobby( bool ServerState::change_client_lobby(
shared_ptr<Client> c, shared_ptr<Client> c, shared_ptr<Lobby> new_lobby, bool send_join_notification, ssize_t required_client_id) {
shared_ptr<Lobby> new_lobby,
bool send_join_notification,
ssize_t required_client_id) {
uint8_t old_lobby_client_id = c->lobby_client_id; uint8_t old_lobby_client_id = c->lobby_client_id;
auto current_lobby = c->lobby.lock(); auto current_lobby = c->lobby.lock();
@@ -161,8 +154,7 @@ bool ServerState::change_client_lobby(
return true; return true;
} }
void ServerState::send_lobby_join_notifications(shared_ptr<Lobby> l, void ServerState::send_lobby_join_notifications(shared_ptr<Lobby> l, shared_ptr<Client> joining_client) {
shared_ptr<Client> joining_client) {
for (auto& other_client : l->clients) { for (auto& other_client : l->clients) {
if (!other_client) { if (!other_client) {
continue; continue;
@@ -243,9 +235,8 @@ void ServerState::on_player_left_lobby(shared_ptr<Lobby> l, uint8_t leaving_clie
} }
shared_ptr<Client> ServerState::find_client(const string* identifier, uint64_t account_id, shared_ptr<Lobby> l) { shared_ptr<Client> ServerState::find_client(const string* identifier, uint64_t account_id, shared_ptr<Lobby> l) {
// WARNING: There are multiple callsites where we assume this function never // WARNING: There are multiple callsites where we assume this function never returns a client that isn't in any
// returns a client that isn't in any lobby. If this behavior changes, we will // lobby. If this behavior changes, we will need to audit all callsites to ensure correctness.
// need to audit all callsites to ensure correctness.
if ((account_id == 0) && identifier) { if ((account_id == 0) && identifier) {
try { try {
@@ -298,8 +289,7 @@ uint32_t ServerState::connect_address_for_client(shared_ptr<Client> c) const {
{ {
auto peer_channel = dynamic_pointer_cast<PeerChannel>(c->channel); auto peer_channel = dynamic_pointer_cast<PeerChannel>(c->channel);
if (peer_channel) { if (peer_channel) {
// This is used during replays; the "client" will ignore this and // This is used during replays; the "client" will ignore this and reconnect via another PeerChannel
// reconnect via another PeerChannel
return 0xEEEEEEEE; return 0xEEEEEEEE;
} }
} }
@@ -517,7 +507,8 @@ shared_ptr<const CommonItemSet> ServerState::common_item_set(Version logic_versi
} else if ((logic_version == Version::GC_NTE) || is_v3(logic_version) || is_v4(logic_version)) { } else if ((logic_version == Version::GC_NTE) || is_v3(logic_version) || is_v4(logic_version)) {
return this->common_item_sets.at("common-table-v3-v4"); return this->common_item_sets.at("common-table-v3-v4");
} else { } else {
throw runtime_error(std::format("no default common item set is available for {}", phosg::name_for_enum(logic_version))); throw runtime_error(std::format(
"no default common item set is available for {}", phosg::name_for_enum(logic_version)));
} }
} }
@@ -545,11 +536,9 @@ void ServerState::set_port_configuration(const vector<PortConfiguration>& port_c
for (const auto& pc : port_configs) { for (const auto& pc : port_configs) {
auto spc = make_shared<PortConfiguration>(pc); auto spc = make_shared<PortConfiguration>(pc);
if (!this->name_to_port_config.emplace(spc->name, spc).second) { if (!this->name_to_port_config.emplace(spc->name, spc).second) {
// Note: This is a logic_error instead of a runtime_error because // Note: This is a logic_error instead of a runtime_error because port_configs comes from a JSON map, so the
// port_configs comes from a JSON map, so the names should already all be // names should already all be unique. In contrast, the user can define port configurations with the same number
// unique. In contrast, the user can define port configurations with the // while still writing valid JSON, so only one of these cases can reasonably occur as a result of user behavior.
// same number while still writing valid JSON, so only one of these cases
// can reasonably occur as a result of user behavior.
throw logic_error("duplicate name in port configuration"); throw logic_error("duplicate name in port configuration");
} }
if (!this->number_to_port_config.emplace(spc->port, spc).second) { if (!this->number_to_port_config.emplace(spc->port, spc).second) {
@@ -571,9 +560,7 @@ void ServerState::set_port_configuration(const vector<PortConfiguration>& port_c
} }
shared_ptr<const string> ServerState::load_bb_file( shared_ptr<const string> ServerState::load_bb_file(
const string& patch_index_filename, const string& patch_index_filename, const string& gsl_filename, const string& bb_directory_filename) const {
const string& gsl_filename,
const string& bb_directory_filename) const {
if (this->bb_patch_file_index) { if (this->bb_patch_file_index) {
// First, look in the patch tree's data directory // First, look in the patch tree's data directory
@@ -588,8 +575,7 @@ shared_ptr<const string> ServerState::load_bb_file(
// Second, look in the patch tree's data.gsl file // Second, look in the patch tree's data.gsl file
const string& effective_gsl_filename = gsl_filename.empty() ? patch_index_filename : gsl_filename; const string& effective_gsl_filename = gsl_filename.empty() ? patch_index_filename : gsl_filename;
try { try {
// TODO: It's kinda not great that we copy the data here; find a way to // TODO: It's kinda not great that we copy the data here; find a way to avoid doing this (also in the below case)
// avoid doing this (also in the below case)
return make_shared<string>(this->bb_data_gsl->get_copy(effective_gsl_filename)); return make_shared<string>(this->bb_data_gsl->get_copy(effective_gsl_filename));
} catch (const out_of_range&) { } catch (const out_of_range&) {
} }
@@ -782,8 +768,7 @@ void ServerState::load_config_early() {
try { try {
this->local_address = this->all_addresses.at(local_address_str); this->local_address = this->all_addresses.at(local_address_str);
string addr_str = string_for_address(this->local_address); string addr_str = string_for_address(this->local_address);
config_log.info_f("Added local address: {} ({})", addr_str, config_log.info_f("Added local address: {} ({})", addr_str, local_address_str);
local_address_str);
} catch (const out_of_range&) { } catch (const out_of_range&) {
this->local_address = address_for_string(local_address_str.c_str()); this->local_address = address_for_string(local_address_str.c_str());
config_log.info_f("Added local address: {}", local_address_str); config_log.info_f("Added local address: {}", local_address_str);
@@ -810,8 +795,7 @@ void ServerState::load_config_early() {
try { try {
this->external_address = this->all_addresses.at(external_address_str); this->external_address = this->all_addresses.at(external_address_str);
string addr_str = string_for_address(this->external_address); string addr_str = string_for_address(this->external_address);
config_log.info_f("Added external address: {} ({})", addr_str, config_log.info_f("Added external address: {} ({})", addr_str, external_address_str);
external_address_str);
} catch (const out_of_range&) { } catch (const out_of_range&) {
this->external_address = address_for_string(external_address_str.c_str()); this->external_address = address_for_string(external_address_str.c_str());
config_log.info_f("Added external address: {}", external_address_str); config_log.info_f("Added external address: {}", external_address_str);
@@ -830,7 +814,8 @@ void ServerState::load_config_early() {
string addr_str = string_for_address(this->external_address); string addr_str = string_for_address(this->external_address);
config_log.warning_f("External address not specified; using {} as default", addr_str); config_log.warning_f("External address not specified; using {} as default", addr_str);
} else { } else {
config_log.warning_f("External address not specified and no default is available; only local clients will be able to connect"); config_log.warning_f(
"External address not specified and no default is available; only local clients will be able to connect");
} }
} }
@@ -1003,16 +988,21 @@ void ServerState::load_config_early() {
? prs_decompress_size(compressed_gvm_data) ? prs_decompress_size(compressed_gvm_data)
: decompressed_gvm_data.size(); : decompressed_gvm_data.size();
if (decompressed_size > 0x37000) { if (decompressed_size > 0x37000) {
throw runtime_error(std::format("banner {} is too large (0x{:X} bytes; maximum size is 0x37000 bytes)", path, decompressed_size)); throw runtime_error(std::format(
"banner {} is too large (0x{:X} bytes; maximum size is 0x37000 bytes)", path, decompressed_size));
} }
if (compressed_gvm_data.empty()) { if (compressed_gvm_data.empty()) {
compressed_gvm_data = prs_compress_optimal(decompressed_gvm_data); compressed_gvm_data = prs_compress_optimal(decompressed_gvm_data);
} }
if (compressed_gvm_data.size() > 0x3800) { if (compressed_gvm_data.size() > 0x3800) {
throw runtime_error(std::format("banner {} cannot be compressed small enough (0x{:X} bytes; maximum size is 0x3800 bytes compressed)", it->at(2).as_string(), compressed_gvm_data.size())); throw runtime_error(std::format(
"banner {} cannot be compressed small enough (0x{:X} bytes; maximum size is 0x3800 bytes compressed)",
it->at(2).as_string(), compressed_gvm_data.size()));
} }
config_log.info_f("Loaded Episode 3 lobby banner {} (0x{:X} -> 0x{:X} bytes)", path, decompressed_size, compressed_gvm_data.size()); config_log.info_f(
"Loaded Episode 3 lobby banner {} (0x{:X} -> 0x{:X} bytes)",
path, decompressed_size, compressed_gvm_data.size());
this->ep3_lobby_banners.emplace_back( this->ep3_lobby_banners.emplace_back(
Ep3LobbyBannerEntry{.type = static_cast<uint32_t>(it->at(0).as_int()), Ep3LobbyBannerEntry{.type = static_cast<uint32_t>(it->at(0).as_int()),
.which = static_cast<uint32_t>(it->at(1).as_int()), .which = static_cast<uint32_t>(it->at(1).as_int()),
@@ -1182,7 +1172,7 @@ void ServerState::load_config_early() {
this->quest_category_index = make_shared<QuestCategoryIndex>(this->config_json->at("QuestCategories")); this->quest_category_index = make_shared<QuestCategoryIndex>(this->config_json->at("QuestCategories"));
} catch (const exception& e) { } catch (const exception& e) {
throw runtime_error(std::format( throw runtime_error(std::format(
"QuestCategories is missing or invalid in config.json ({}) - see config.example.json for an example", e.what())); "QuestCategories is missing or invalid in config ({}); see config.example.json for an example", e.what()));
} }
config_log.info_f("Creating menus"); config_log.info_f("Creating menus");
@@ -1455,7 +1445,8 @@ void ServerState::load_config_late() {
auto& list = it->as_list(); auto& list = it->as_list();
size_t price = list.at(0)->as_int(); size_t price = list.at(0)->as_int();
try { try {
this->quest_F95F_results.emplace_back(make_pair(price, this->parse_item_description(Version::BB_V4, list.at(1)->as_string()))); this->quest_F95F_results.emplace_back(make_pair(
price, this->parse_item_description(Version::BB_V4, list.at(1)->as_string())));
} catch (const exception& e) { } catch (const exception& e) {
config_log.warning_f("Cannot parse item description \"{}\": {} (skipping entry)", list.at(1)->as_string(), e.what()); config_log.warning_f("Cannot parse item description \"{}\": {} (skipping entry)", list.at(1)->as_string(), e.what());
} }
@@ -1463,7 +1454,8 @@ void ServerState::load_config_late() {
} catch (const out_of_range&) { } catch (const out_of_range&) {
} }
try { try {
this->quest_F960_failure_results = QuestF960Result(this->config_json->at("QuestF960FailureResultItems"), this->item_name_index(Version::BB_V4)); this->quest_F960_failure_results = QuestF960Result(
this->config_json->at("QuestF960FailureResultItems"), this->item_name_index(Version::BB_V4));
for (const auto& it : this->config_json->get_list("QuestF960SuccessResultItems")) { for (const auto& it : this->config_json->get_list("QuestF960SuccessResultItems")) {
this->quest_F960_success_results.emplace_back(*it, this->item_name_index(Version::BB_V4)); this->quest_F960_success_results.emplace_back(*it, this->item_name_index(Version::BB_V4));
} }
@@ -1585,8 +1577,7 @@ void ServerState::load_maps() {
unordered_map<uint64_t, shared_ptr<const MapFile>> new_map_file_for_source_hash; unordered_map<uint64_t, shared_ptr<const MapFile>> new_map_file_for_source_hash;
map<uint32_t, array<shared_ptr<const MapFile>, NUM_VERSIONS>> new_map_files_for_free_play_key; map<uint32_t, array<shared_ptr<const MapFile>, NUM_VERSIONS>> new_map_files_for_free_play_key;
{ {
// TODO: Ep3 NTE loads map_city00_on, but it appears there are some // TODO: Ep3 NTE loads map_city00_on, but it appears there are variants. Figure this out and load those maps too.
// variants. Figure this out and load those maps too.
auto objects_data = this->load_map_file(Version::GC_EP3, "map_city_on_battle_o.dat"); auto objects_data = this->load_map_file(Version::GC_EP3, "map_city_on_battle_o.dat");
auto enemies_data = this->load_map_file(Version::GC_EP3, "map_city_on_battle_e.dat"); auto enemies_data = this->load_map_file(Version::GC_EP3, "map_city_on_battle_e.dat");
if (objects_data || enemies_data) { if (objects_data || enemies_data) {
@@ -1965,8 +1956,6 @@ void ServerState::load_drop_tables() {
size_t ext_offset = filename.rfind('.'); size_t ext_offset = filename.rfind('.');
string basename = (ext_offset == string::npos) ? filename : filename.substr(0, ext_offset); string basename = (ext_offset == string::npos) ? filename : filename.substr(0, ext_offset);
// AFSV2CommonItemSet(std::shared_ptr<const std::string> pt_afs_data, std::shared_ptr<const std::string> ct_afs_data);
if (filename.ends_with(".json")) { if (filename.ends_with(".json")) {
config_log.info_f("Loading JSON common item table {}", filename); config_log.info_f("Loading JSON common item table {}", filename);
new_common_item_sets.emplace(basename, make_shared<JSONCommonItemSet>(phosg::JSON::parse(phosg::load_file(path)))); new_common_item_sets.emplace(basename, make_shared<JSONCommonItemSet>(phosg::JSON::parse(phosg::load_file(path))));
+5 -11
View File
@@ -42,10 +42,9 @@ struct PortConfiguration {
}; };
struct CheatFlags { struct CheatFlags {
// This structure describes which behaviors are considered cheating (that is, // This structure describes which behaviors are considered cheating (that is, require cheat mode to be enabled or the
// require cheat mode to be enabled or the user to have the CHEAT_ANYWHERE // user to have the CHEAT_ANYWHERE account flag). A false value here means that that particular behavior is NOT
// account flag). A false value here means that that particular behavior is // cheating, so cheat mode is NOT required.
// NOT cheating, so cheat mode is NOT required.
bool create_items = true; bool create_items = true;
bool edit_section_id = true; bool edit_section_id = true;
bool edit_stats = true; bool edit_stats = true;
@@ -332,9 +331,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
void on_player_left_lobby(std::shared_ptr<Lobby> l, uint8_t leaving_client_id); void on_player_left_lobby(std::shared_ptr<Lobby> l, uint8_t leaving_client_id);
std::shared_ptr<Client> find_client( std::shared_ptr<Client> find_client(
const std::string* identifier = nullptr, const std::string* identifier = nullptr, uint64_t account_id = 0, std::shared_ptr<Lobby> l = nullptr);
uint64_t account_id = 0,
std::shared_ptr<Lobby> l = nullptr);
uint32_t connect_address_for_client(std::shared_ptr<Client> c) const; uint32_t connect_address_for_client(std::shared_ptr<Client> c) const;
uint16_t game_server_port_for_version(Version v) const; uint16_t game_server_port_for_version(Version v) const;
@@ -408,10 +405,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::shared_ptr<const SuperMap> get_free_play_supermap( std::shared_ptr<const SuperMap> get_free_play_supermap(
Episode episode, GameMode mode, Difficulty difficulty, uint8_t floor, uint32_t layout, uint32_t entities); Episode episode, GameMode mode, Difficulty difficulty, uint8_t floor, uint32_t layout, uint32_t entities);
std::vector<std::shared_ptr<const SuperMap>> supermaps_for_variations( std::vector<std::shared_ptr<const SuperMap>> supermaps_for_variations(
Episode episode, Episode episode, GameMode mode, Difficulty difficulty, const Variations& variations);
GameMode mode,
Difficulty difficulty,
const Variations& variations);
void create_default_lobbies(); void create_default_lobbies();
void collect_network_addresses(); void collect_network_addresses();
+4 -9
View File
@@ -49,13 +49,8 @@ shared_ptr<Client> ShellCommand::Args::get_proxy_client() const {
return c; return c;
} }
ShellCommand::ShellCommand( ShellCommand::ShellCommand(const char* name, const char* help_text, asio::awaitable<deque<string>> (*run)(Args&))
const char* name, : name(name), help_text(help_text), run(run) {
const char* help_text,
asio::awaitable<deque<string>> (*run)(Args&))
: name(name),
help_text(help_text),
run(run) {
ShellCommand::commands_by_order.emplace_back(this); ShellCommand::commands_by_order.emplace_back(this);
ShellCommand::commands_by_name.emplace(this->name, this); ShellCommand::commands_by_name.emplace(this->name, this);
} }
@@ -413,8 +408,8 @@ ShellCommand c_update_account(
auto account = args.s->account_index->from_account_id(stoul(tokens[0], nullptr, 16)); auto account = args.s->account_index->from_account_id(stoul(tokens[0], nullptr, 16));
tokens.erase(tokens.begin()); tokens.erase(tokens.begin());
// Do all the parsing first, then the updates afterward, so we won't // Do all the parsing first, then the updates afterward, so we won't partially update the account if parsing a
// partially update the account if parsing a later option fails // later option fails
int64_t new_ep3_current_meseta = -1; int64_t new_ep3_current_meseta = -1;
int64_t new_ep3_total_meseta = -1; int64_t new_ep3_total_meseta = -1;
int64_t new_flags = -1; int64_t new_flags = -1;
+2 -1
View File
@@ -34,6 +34,7 @@ struct ShellCommand {
ShellCommand(const char* name, const char* help_text, asio::awaitable<std::deque<std::string>> (*run)(Args&)); ShellCommand(const char* name, const char* help_text, asio::awaitable<std::deque<std::string>> (*run)(Args&));
static asio::awaitable<std::deque<std::string>> dispatch_str(std::shared_ptr<ServerState> s, const std::string& command); static asio::awaitable<std::deque<std::string>> dispatch_str(
std::shared_ptr<ServerState> s, const std::string& command);
static asio::awaitable<std::deque<std::string>> dispatch(Args& args); static asio::awaitable<std::deque<std::string>> dispatch(Args& args);
}; };
+47 -233
View File
@@ -118,147 +118,47 @@ const char* abbreviation_for_mode(GameMode mode) {
} }
static const array<const char*, 10> section_id_to_name = { static const array<const char*, 10> section_id_to_name = {
"Viridia", "Greennill", "Skyly", "Bluefull", "Purplenum", "Viridia", "Greennill", "Skyly", "Bluefull", "Purplenum", "Pinkal", "Redria", "Oran", "Yellowboze", "Whitill"};
"Pinkal", "Redria", "Oran", "Yellowboze", "Whitill"};
static const array<const char*, 10> section_id_to_abbreviation = { static const array<const char*, 10> section_id_to_abbreviation = {
"Vir", "Grn", "Sky", "Blu", "Prp", "Pnk", "Red", "Orn", "Ylw", "Wht"}; "Vir", "Grn", "Sky", "Blu", "Prp", "Pnk", "Red", "Orn", "Ylw", "Wht"};
const unordered_map<string, uint8_t> name_to_section_id({ const unordered_map<string, uint8_t> name_to_section_id({{"viridia", 0},
{"viridia", 0}, // Greennill is spelled Greenill in some places, so we accept both spellings
{"greennill", 1}, {"greennill", 1}, {"greenill", 1}, {"skyly", 2}, {"bluefull", 3}, {"purplenum", 4}, {"pinkal", 5}, {"redria", 6},
{"greenill", 1}, {"oran", 7}, {"yellowboze", 8}, {"whitill", 9},
{"skyly", 2},
{"bluefull", 3},
{"purplenum", 4},
{"pinkal", 5},
{"redria", 6},
{"oran", 7},
{"yellowboze", 8},
{"whitill", 9},
// Shortcuts for chat commands // Shortcuts for chat commands
{"b", 3}, {"b", 3}, {"g", 1}, {"o", 7}, {"pi", 5}, {"pu", 4}, {"r", 6}, {"s", 2}, {"v", 0}, {"w", 9}, {"y", 8}});
{"g", 1},
{"o", 7},
{"pi", 5},
{"pu", 4},
{"r", 6},
{"s", 2},
{"v", 0},
{"w", 9},
{"y", 8},
});
const vector<string> lobby_event_to_name = { const vector<string> lobby_event_to_name = {
"none", "xmas", "none", "val", "easter", "hallo", "sonic", "newyear", "none", "xmas", "none", "val", "easter", "hallo", "sonic", "newyear",
"summer", "white", "wedding", "fall", "s-spring", "s-summer", "spring"}; "summer", "white", "wedding", "fall", "s-spring", "s-summer", "spring"};
const unordered_map<string, uint8_t> name_to_lobby_event({ const unordered_map<string, uint8_t> name_to_lobby_event = {
{"none", 0}, {"none", 0}, {"xmas", 1}, {"val", 3}, {"easter", 4}, {"hallo", 5}, {"sonic", 6}, {"newyear", 7}, {"summer", 8},
{"xmas", 1}, {"white", 9}, {"wedding", 10}, {"fall", 11}, {"s-spring", 12}, {"s-summer", 13}, {"spring", 14}};
{"val", 3},
{"easter", 4},
{"hallo", 5},
{"sonic", 6},
{"newyear", 7},
{"summer", 8},
{"white", 9},
{"wedding", 10},
{"fall", 11},
{"s-spring", 12},
{"s-summer", 13},
{"spring", 14},
});
const unordered_map<uint8_t, string> lobby_type_to_name({ const unordered_map<uint8_t, string> lobby_type_to_name = {
{0x00, "normal"}, {0x00, "normal"}, {0x0F, "inormal"}, {0x10, "ipc"}, {0x11, "iball"}, {0x67, "cave2u"}, {0xD4, "cave1"},
{0x0F, "inormal"}, {0xE9, "planet"}, {0xEA, "clouds"}, {0xED, "cave"}, {0xEE, "jungle"}, {0xEF, "forest2-2"}, {0xF0, "forest2-1"},
{0x10, "ipc"}, {0xF1, "windpower"}, {0xF2, "overview"}, {0xF3, "seaside"}, {0xF4, "fons"}, {0xF5, "dmorgue"}, {0xF6, "caelum"},
{0x11, "iball"}, {0xF8, "cyber"}, {0xF9, "boss1"}, {0xFA, "boss2"}, {0xFB, "dolor"}, {0xFC, "dragon"}, {0xFD, "derolle"},
{0x67, "cave2u"}, {0xFE, "volopt"}, {0xFF, "darkfalz"}};
{0xD4, "cave1"},
{0xE9, "planet"},
{0xEA, "clouds"},
{0xED, "cave"},
{0xEE, "jungle"},
{0xEF, "forest2-2"},
{0xF0, "forest2-1"},
{0xF1, "windpower"},
{0xF2, "overview"},
{0xF3, "seaside"},
{0xF4, "fons"},
{0xF5, "dmorgue"},
{0xF6, "caelum"},
{0xF8, "cyber"},
{0xF9, "boss1"},
{0xFA, "boss2"},
{0xFB, "dolor"},
{0xFC, "dragon"},
{0xFD, "derolle"},
{0xFE, "volopt"},
{0xFF, "darkfalz"},
});
const unordered_map<string, uint8_t> name_to_lobby_type({ const unordered_map<string, uint8_t> name_to_lobby_type = {
{"normal", 0x00}, {"normal", 0x00}, {"inormal", 0x0F}, {"ipc", 0x10}, {"iball", 0x11}, {"cave1", 0xD4}, {"cave2u", 0x67},
{"inormal", 0x0F}, {"dragon", 0xFC}, {"derolle", 0xFD}, {"volopt", 0xFE}, {"darkfalz", 0xFF}, {"planet", 0xE9}, {"clouds", 0xEA},
{"ipc", 0x10}, {"cave", 0xED}, {"jungle", 0xEE}, {"forest2-2", 0xEF}, {"forest2-1", 0xF0}, {"windpower", 0xF1},
{"iball", 0x11}, {"overview", 0xF2}, {"seaside", 0xF3}, {"fons", 0xF4}, {"dmorgue", 0xF5}, {"caelum", 0xF6}, {"cyber", 0xF8},
{"cave1", 0xD4}, {"boss1", 0xF9}, {"boss2", 0xFA}, {"dolor", 0xFB}, {"ravum", 0xFC}, {"sky", 0xFE}, {"morgue", 0xFF}};
{"cave2u", 0x67},
{"dragon", 0xFC},
{"derolle", 0xFD},
{"volopt", 0xFE},
{"darkfalz", 0xFF},
{"planet", 0xE9},
{"clouds", 0xEA},
{"cave", 0xED},
{"jungle", 0xEE},
{"forest2-2", 0xEF},
{"forest2-1", 0xF0},
{"windpower", 0xF1},
{"overview", 0xF2},
{"seaside", 0xF3},
{"fons", 0xF4},
{"dmorgue", 0xF5},
{"caelum", 0xF6},
{"cyber", 0xF8},
{"boss1", 0xF9},
{"boss2", 0xFA},
{"dolor", 0xFB},
{"ravum", 0xFC},
{"sky", 0xFE},
{"morgue", 0xFF},
});
const vector<string> npc_id_to_name({ const vector<string> npc_id_to_name = {
"ninja", "ninja", "rico", "sonic", "knuckles", "tails", "flowen", "elly", "momoka", "irene", "guild", "nurse"};
"rico",
"sonic",
"knuckles",
"tails",
"flowen",
"elly",
"momoka",
"irene",
"guild",
"nurse",
});
const unordered_map<string, uint8_t> name_to_npc_id = { const unordered_map<string, uint8_t> name_to_npc_id = {
{"ninja", 0}, {"ninja", 0}, {"rico", 1}, {"sonic", 2}, {"knuckles", 3}, {"tails", 4}, {"flowen", 5}, {"elly", 6}, {"momoka", 7},
{"rico", 1}, {"irene", 8}, {"guild", 9}, {"nurse", 10}};
{"sonic", 2},
{"knuckles", 3},
{"tails", 4},
{"flowen", 5},
{"elly", 6},
{"momoka", 7},
{"irene", 8},
{"guild", 9},
{"nurse", 10},
};
bool npc_valid_for_version(uint8_t npc, Version version) { bool npc_valid_for_version(uint8_t npc, Version version) {
switch (version) { switch (version) {
@@ -393,19 +293,8 @@ uint8_t npc_for_name(const string& name, Version version) {
const char* name_for_char_class(uint8_t cls) { const char* name_for_char_class(uint8_t cls) {
static const array<const char*, 12> names = { static const array<const char*, 12> names = {
"HUmar", "HUmar", "HUnewearl", "HUcast", "RAmar", "RAcast", "RAcaseal", "FOmarl", "FOnewm", "FOnewearl", "HUcaseal",
"HUnewearl", "FOmar", "RAmarl"};
"HUcast",
"RAmar",
"RAcast",
"RAcaseal",
"FOmarl",
"FOnewm",
"FOnewearl",
"HUcaseal",
"FOmar",
"RAmarl",
};
try { try {
return names.at(cls); return names.at(cls);
} catch (const out_of_range&) { } catch (const out_of_range&) {
@@ -415,19 +304,7 @@ const char* name_for_char_class(uint8_t cls) {
const char* abbreviation_for_char_class(uint8_t cls) { const char* abbreviation_for_char_class(uint8_t cls) {
static const array<const char*, 12> names = { static const array<const char*, 12> names = {
"HUmr", "HUmr", "HUnl", "HUct", "RAmr", "RAct", "RAcl", "FOml", "FOnm", "FOnl", "HUcl", "FOmr", "RAml"};
"HUnl",
"HUct",
"RAmr",
"RAct",
"RAcl",
"FOml",
"FOnm",
"FOnl",
"HUcl",
"FOmr",
"RAml",
};
try { try {
return names.at(cls); return names.at(cls);
} catch (const out_of_range&) { } catch (const out_of_range&) {
@@ -489,8 +366,7 @@ bool char_class_is_force(uint8_t cls) {
} }
const char* name_for_difficulty(Difficulty difficulty) { const char* name_for_difficulty(Difficulty difficulty) {
static const array<const char*, 4> names = { static const array<const char*, 4> names = {"Normal", "Hard", "Very Hard", "Ultimate"};
"Normal", "Hard", "Very Hard", "Ultimate"};
try { try {
return names.at(static_cast<size_t>(difficulty)); return names.at(static_cast<size_t>(difficulty));
} catch (const out_of_range&) { } catch (const out_of_range&) {
@@ -499,8 +375,7 @@ const char* name_for_difficulty(Difficulty difficulty) {
} }
const char* token_name_for_difficulty(Difficulty difficulty) { const char* token_name_for_difficulty(Difficulty difficulty) {
static const array<const char*, 4> names = { static const array<const char*, 4> names = {"Normal", "Hard", "VeryHard", "Ultimate"};
"Normal", "Hard", "VeryHard", "Ultimate"};
try { try {
return names.at(static_cast<size_t>(difficulty)); return names.at(static_cast<size_t>(difficulty));
} catch (const out_of_range&) { } catch (const out_of_range&) {
@@ -518,14 +393,8 @@ char abbreviation_for_difficulty(Difficulty difficulty) {
} }
const char* name_for_language(Language language) { const char* name_for_language(Language language) {
array<const char*, 8> names = {{"Japanese", array<const char*, 8> names = {
"English", "Japanese", "English", "German", "French", "Spanish", "Simplified Chinese", "Traditional Chinese", "Korean"};
"German",
"French",
"Spanish",
"Simplified Chinese",
"Traditional Chinese",
"Korean"}};
size_t lang_index = static_cast<size_t>(language); size_t lang_index = static_cast<size_t>(language);
return (lang_index < 8) ? names[lang_index] : "Unknown"; return (lang_index < 8) ? names[lang_index] : "Unknown";
} }
@@ -599,33 +468,13 @@ Language language_for_name(const string& name) {
} }
const vector<string> tech_id_to_name = { const vector<string> tech_id_to_name = {
"foie", "gifoie", "rafoie", "foie", "gifoie", "rafoie", "barta", "gibarta", "rabarta", "zonde", "gizonde", "razonde", "grants", "deband",
"barta", "gibarta", "rabarta", "jellen", "zalure", "shifta", "ryuker", "resta", "anti", "reverser", "megid"};
"zonde", "gizonde", "razonde",
"grants", "deband", "jellen", "zalure", "shifta",
"ryuker", "resta", "anti", "reverser", "megid"};
const unordered_map<string, uint8_t> name_to_tech_id({ const unordered_map<string, uint8_t> name_to_tech_id = {
{"foie", 0}, {"foie", 0}, {"gifoie", 1}, {"rafoie", 2}, {"barta", 3}, {"gibarta", 4}, {"rabarta", 5}, {"zonde", 6},
{"gifoie", 1}, {"gizonde", 7}, {"razonde", 8}, {"grants", 9}, {"deband", 10}, {"jellen", 11}, {"zalure", 12}, {"shifta", 13},
{"rafoie", 2}, {"ryuker", 14}, {"resta", 15}, {"anti", 16}, {"reverser", 17}, {"megid", 18}};
{"barta", 3},
{"gibarta", 4},
{"rabarta", 5},
{"zonde", 6},
{"gizonde", 7},
{"razonde", 8},
{"grants", 9},
{"deband", 10},
{"jellen", 11},
{"zalure", 12},
{"shifta", 13},
{"ryuker", 14},
{"resta", 15},
{"anti", 16},
{"reverser", 17},
{"megid", 18},
});
const string& name_for_technique(uint8_t tech) { const string& name_for_technique(uint8_t tech) {
try { try {
@@ -652,49 +501,15 @@ uint8_t technique_for_name(const string& name) {
return 0xFF; return 0xFF;
} }
const vector<const char*> name_for_mag_color({ const vector<const char*> name_for_mag_color = {
/* 00 */ "red", "red", "blue", "yellow", "green", "purple", "black", "white", "cyan", "brown", "orange", "light-blue", "olive",
/* 01 */ "blue", "turquoise", "fuchsia", "grey", "cream", "pink", "dark-green", "costume"};
/* 02 */ "yellow",
/* 03 */ "green",
/* 04 */ "purple",
/* 05 */ "black",
/* 06 */ "white",
/* 07 */ "cyan",
/* 08 */ "brown",
/* 09 */ "orange",
/* 0A */ "light-blue",
/* 0B */ "olive",
/* 0C */ "turquoise",
/* 0D */ "fuchsia",
/* 0E */ "grey",
/* 0F */ "cream",
/* 10 */ "pink",
/* 11 */ "dark-green",
/* 12 */ "costume",
});
const unordered_map<string, uint8_t> mag_color_for_name({ const unordered_map<string, uint8_t> mag_color_for_name = {
{"red", 0x00}, {"red", 0x00}, {"blue", 0x01}, {"yellow", 0x02}, {"green", 0x03}, {"purple", 0x04}, {"black", 0x05},
{"blue", 0x01}, {"white", 0x06}, {"cyan", 0x07}, {"brown", 0x08}, {"orange", 0x09}, {"light-blue", 0x0A}, {"olive", 0x0B},
{"yellow", 0x02}, {"turquoise", 0x0C}, {"fuchsia", 0x0D}, {"grey", 0x0E}, {"cream", 0x0F}, {"pink", 0x10}, {"dark-green", 0x11},
{"green", 0x03}, {"costume-color", 0x12}};
{"purple", 0x04},
{"black", 0x05},
{"white", 0x06},
{"cyan", 0x07},
{"brown", 0x08},
{"orange", 0x09},
{"light-blue", 0x0A},
{"olive", 0x0B},
{"turquoise", 0x0C},
{"fuchsia", 0x0D},
{"grey", 0x0E},
{"cream", 0x0F},
{"pink", 0x10},
{"dark-green", 0x11},
{"costume-color", 0x12},
});
static constexpr uint8_t F_CITY = FloorDefinition::Flag::CITY; static constexpr uint8_t F_CITY = FloorDefinition::Flag::CITY;
static constexpr uint8_t F_LOBBY = FloorDefinition::Flag::LOBBY; static constexpr uint8_t F_LOBBY = FloorDefinition::Flag::LOBBY;
@@ -836,8 +651,7 @@ size_t FloorDefinition::limit_for_episode(Episode ep) {
} }
uint32_t class_flags_for_class(uint8_t char_class) { uint32_t class_flags_for_class(uint8_t char_class) {
static constexpr uint8_t flags[12] = { static constexpr uint8_t flags[12] = {0x25, 0x2A, 0x31, 0x45, 0x51, 0x52, 0x86, 0x89, 0x8A, 0x32, 0x85, 0x46};
0x25, 0x2A, 0x31, 0x45, 0x51, 0x52, 0x86, 0x89, 0x8A, 0x32, 0x85, 0x46};
if (char_class >= 12) { if (char_class >= 12) {
throw runtime_error("invalid character class"); throw runtime_error("invalid character class");
} }
+6 -12
View File
@@ -14,9 +14,7 @@
using namespace std; using namespace std;
TeamIndex::Team::Member::Member(const phosg::JSON& json) TeamIndex::Team::Member::Member(const phosg::JSON& json)
: flags(json.get_int("Flags", 0)), : flags(json.get_int("Flags", 0)), points(json.get_int("Points", 0)), name(json.get_string("Name", "")) {
points(json.get_int("Points", 0)),
name(json.get_string("Name", "")) {
try { try {
this->account_id = json.get_int("AccountID"); this->account_id = json.get_int("AccountID");
} catch (const out_of_range&) { } catch (const out_of_range&) {
@@ -26,12 +24,8 @@ TeamIndex::Team::Member::Member(const phosg::JSON& json)
} }
phosg::JSON TeamIndex::Team::Member::json() const { phosg::JSON TeamIndex::Team::Member::json() const {
return phosg::JSON::dict({ return phosg::JSON::dict(
{"AccountID", this->account_id}, {{"AccountID", this->account_id}, {"Flags", this->flags}, {"Points", this->points}, {"Name", this->name}});
{"Flags", this->flags},
{"Points", this->points},
{"Name", this->name},
});
} }
uint32_t TeamIndex::Team::Member::privilege_level() const { uint32_t TeamIndex::Team::Member::privilege_level() const {
@@ -472,10 +466,10 @@ void TeamIndex::add_to_indexes(shared_ptr<Team> team) {
this->id_to_team.erase(team->team_id); this->id_to_team.erase(team->team_id);
throw runtime_error("team name is already in use"); throw runtime_error("team name is already in use");
} }
for (const auto& it : team->members) { for (const auto& [_, member] : team->members) {
if (!this->account_id_to_team.emplace(it.second.account_id, team).second) { if (!this->account_id_to_team.emplace(member.account_id, team).second) {
static_game_data_log.warning_f("Serial number {:08X} ({:010}) exists in multiple teams", static_game_data_log.warning_f("Serial number {:08X} ({:010}) exists in multiple teams",
it.second.account_id, it.second.account_id); member.account_id, member.account_id);
} }
} }
} }
+3 -3
View File
@@ -45,8 +45,7 @@ public:
}; };
enum class RewardFlag { enum class RewardFlag {
// Only 0x00000001 and 0x00000002 are used by the client; the rest are // Only 0x00000001 and 0x00000002 are used by the client; the rest are free to be used however the server chooses
// free to be used however the server chooses.
NONE = 0x00000000, NONE = 0x00000000,
TEAM_FLAG = 0x00000001, TEAM_FLAG = 0x00000001,
DRESSING_ROOM = 0x00000002, DRESSING_ROOM = 0x00000002,
@@ -130,7 +129,8 @@ public:
std::shared_ptr<const Team> get_by_account_id(uint32_t account_id) const; std::shared_ptr<const Team> get_by_account_id(uint32_t account_id) const;
std::vector<std::shared_ptr<const Team>> all() const; std::vector<std::shared_ptr<const Team>> all() const;
std::shared_ptr<const Team> create(const std::string& name, uint32_t master_account_id, const std::string& master_name); std::shared_ptr<const Team> create(
const std::string& name, uint32_t master_account_id, const std::string& master_name);
void disband(uint32_t team_id); void disband(uint32_t team_id);
void rename(uint32_t team_id, const std::string& new_name); void rename(uint32_t team_id, const std::string& new_name);
+11 -17
View File
@@ -14,8 +14,7 @@ using namespace std;
const iconv_t TextTranscoder::INVALID_IC = (iconv_t)(-1); const iconv_t TextTranscoder::INVALID_IC = (iconv_t)(-1);
const size_t TextTranscoder::FAILURE_RESULT = static_cast<size_t>(-1); const size_t TextTranscoder::FAILURE_RESULT = static_cast<size_t>(-1);
TextTranscoder::TextTranscoder(const char* to, const char* from) TextTranscoder::TextTranscoder(const char* to, const char* from) : ic(iconv_open(to, from)) {
: ic(iconv_open(to, from)) {
if (ic == this->INVALID_IC) { if (ic == this->INVALID_IC) {
string error_str = phosg::string_for_error(errno); string error_str = phosg::string_for_error(errno);
throw runtime_error(std::format("failed to initialize {} -> {} text converter: {}", from, to, error_str)); throw runtime_error(std::format("failed to initialize {} -> {} text converter: {}", from, to, error_str));
@@ -77,10 +76,7 @@ TextTranscoder::Result TextTranscoder::operator()(
size_t bytes_read = reinterpret_cast<const char*>(src) - reinterpret_cast<const char*>(orig_src); size_t bytes_read = reinterpret_cast<const char*>(src) - reinterpret_cast<const char*>(orig_src);
size_t bytes_written = reinterpret_cast<char*>(dest) - reinterpret_cast<char*>(orig_dest); size_t bytes_written = reinterpret_cast<char*>(dest) - reinterpret_cast<char*>(orig_dest);
return Result{ return Result{.bytes_read = bytes_read, .bytes_written = bytes_written};
.bytes_read = bytes_read,
.bytes_written = bytes_written,
};
} }
string TextTranscoder::operator()(const void* src, size_t src_bytes) { string TextTranscoder::operator()(const void* src, size_t src_bytes) {
@@ -198,15 +194,14 @@ uint32_t decode_utf8_char(const void** vdata, size_t* size) {
} }
std::string TextTranscoderCustomSJISToUTF8::on_untranslatable(const void** vsrc, size_t* size) const { std::string TextTranscoderCustomSJISToUTF8::on_untranslatable(const void** vsrc, size_t* size) const {
// Sega implemented some nonstandard Shift-JIS characters on PSO GC (and // Sega implemented some nonstandard Shift-JIS characters on PSO GC (and probably XB as well): the heart symbol,
// probably XB as well): the heart symbol, encoded as F040, and the PSO font, // encoded as F040, and the PSO font, encoded as F041-F064. Understandably, libiconv doesn't know what to do with
// encoded as F041-F064. Understandably, libiconv doesn't know what to do // these because they're not actually part of Shift-JIS, so we have to handle them manually here. We convert them to
// with these because they're not actually part of Shift-JIS, so we have to // actual UTF-8 symbols:
// handle them manually here. We convert them to actual UTF-8 symbols: // F040 (heart symbol) -> U+2665 (heart suit symbol)
// F040 (heart symbol) -> U+2665 (heart suit symbol) // F041 (PSO font number 0) -> 24EA (circled digit zero)
// F041 (PSO font number 0) -> 24EA (circled digit zero) // F042-F04A (PSO font numbers 1-9) -> 2460-2468 (circled digits 1-9)
// F042-F04A (PSO font numbers 1-9) -> 2460-2468 (circled digits 1-9) // F04B-F064 (PSO font letters) -> 1D4D0-1D4E9 (script letters A-Z)
// F04B-F064 (PSO font letters) -> 1D4D0-1D4E9 (script letters A-Z)
const uint8_t* src = reinterpret_cast<const uint8_t*>(*vsrc); const uint8_t* src = reinterpret_cast<const uint8_t*>(*vsrc);
if ((*size < 2) || (src[0] != 0xF0)) { if ((*size < 2) || (src[0] != 0xF0)) {
@@ -394,8 +389,7 @@ size_t add_color_inplace(char* a, size_t max_chars) {
a++; a++;
} }
*d = 0; *d = 0;
// TODO: we should clear the chars after the null if the new string is shorter // TODO: we should clear the chars after the null if the new string is shorter than the original
// than the original
return d - orig_d; return d - orig_d;
} }
+7 -8
View File
@@ -155,8 +155,8 @@ struct parray {
if (index >= Count) { if (index >= Count) {
throw std::out_of_range("array index out of bounds"); throw std::out_of_range("array index out of bounds");
} }
// Note: This looks really dumb, but apparently works around an issue in GCC // Note: This looks really dumb, but apparently works around an issue in GCC that causes a "returning address of
// that causes a "returning address of temporary" error here. // temporary" error here.
return *&this->items[index]; return *&this->items[index];
} }
const ItemT& operator[](size_t index) const { const ItemT& operator[](size_t index) const {
@@ -463,8 +463,8 @@ void decrypt_challenge_rank_text_t(void* vdata, size_t count) {
} }
} }
// This struct does not inherit from parray, even though it's semantically // This struct does not inherit from parray, even though it's semantically similar, because we want to enforce that the
// similar, because we want to enforce that the correct encoding is used. // correct encoding is used.
template < template <
TextEncoding Encoding, TextEncoding Encoding,
size_t Chars, size_t Chars,
@@ -737,8 +737,7 @@ struct pstring {
return this->data[pos]; return this->data[pos];
} }
// Note: The contents of a pstring do not have to be null-terminated, so there // Note: The contents of a pstring do not have to be null-terminated, so there is no .c_str() function.
// is no function.
} __attribute__((packed)); } __attribute__((packed));
// Helper functions // Helper functions
@@ -751,8 +750,8 @@ std::string add_color(const std::string& s);
size_t add_color_inplace(char* a, size_t max_chars); size_t add_color_inplace(char* a, size_t max_chars);
void add_color_inplace(std::string& s); void add_color_inplace(std::string& s);
// remove_color does the opposite of add_color (it changes \t into $, for // remove_color does the opposite of add_color (it changes \t into $, for example). strip_color is irreversible; it
// example). strip_color is irreversible; it deletes color escape sequences. // deletes color escape sequences.
void remove_color(phosg::StringWriter& w, const char* src, size_t max_input_chars); void remove_color(phosg::StringWriter& w, const char* src, size_t max_input_chars);
std::string remove_color(const std::string& s); std::string remove_color(const std::string& s);
+7 -10
View File
@@ -174,9 +174,8 @@ BinaryTextSet::BinaryTextSet(const std::string& pr2_data, size_t collection_coun
auto decompressed = prs_decompress(pr2_decrypted.compressed_data); auto decompressed = prs_decompress(pr2_decrypted.compressed_data);
phosg::StringReader r(decompressed); phosg::StringReader r(decompressed);
// Annoyingly, there doesn't appear to be any bounds-checking on the language // Annoyingly, there doesn't appear to be any bounds-checking on the language functions, so there are no counts of
// functions, so there are no counts of strings in each collection. We have to // strings in each collection. We have to figure out where each collection ends by collecting all the relevant
// figure out where each collection ends by collecting all the relevant
// offsets in the file instead. // offsets in the file instead.
::set<uint32_t> used_offsets; ::set<uint32_t> used_offsets;
size_t root_offset = has_rel_footer size_t root_offset = has_rel_footer
@@ -195,8 +194,8 @@ BinaryTextSet::BinaryTextSet(const std::string& pr2_data, size_t collection_coun
while (!collection_offsets_r.eof()) { while (!collection_offsets_r.eof()) {
auto& collection = this->collections.emplace_back(); auto& collection = this->collections.emplace_back();
uint32_t first_string_offset_offset = collection_offsets_r.get_u32l(); uint32_t first_string_offset_offset = collection_offsets_r.get_u32l();
// TODO: Apparently the early formats do actually include keyboards, but // TODO: Apparently the early formats do actually include keyboards, but they're just in the middle of the
// they're just in the middle of the collections list. Sigh... // collections list. Sigh...
try { try {
for (uint32_t string_offset_offset = first_string_offset_offset; for (uint32_t string_offset_offset = first_string_offset_offset;
(string_offset_offset == first_string_offset_offset) || !used_offsets.count(string_offset_offset); (string_offset_offset == first_string_offset_offset) || !used_offsets.count(string_offset_offset);
@@ -306,9 +305,8 @@ void BinaryTextAndKeyboardsSet::parse_t(const string& pr2_data, bool is_sjis) {
auto decompressed = prs_decompress(pr2_decrypted.compressed_data); auto decompressed = prs_decompress(pr2_decrypted.compressed_data);
phosg::StringReader r(decompressed); phosg::StringReader r(decompressed);
// Annoyingly, there doesn't appear to be any bounds-checking on the language // Annoyingly, there doesn't appear to be any bounds-checking on the language functions, so there are no counts of
// functions, so there are no counts of strings in each collection. We have to // strings in each collection. We have to figure out where each collection ends by collecting all the relevant
// figure out where each collection ends by collecting all the relevant
// offsets in the file instead. // offsets in the file instead.
::set<uint32_t> used_offsets; ::set<uint32_t> used_offsets;
used_offsets.emplace(r.size() - 8); used_offsets.emplace(r.size() - 8);
@@ -449,8 +447,7 @@ pair<string, string> BinaryTextAndKeyboardsSet::serialize_t(bool is_sjis) const
} }
TextIndex::TextIndex( TextIndex::TextIndex(
const string& directory, const string& directory, function<shared_ptr<const string>(Version, const string&)> get_patch_file)
function<shared_ptr<const string>(Version, const string&)> get_patch_file)
: log("[TextIndex] ", static_game_data_log.min_level) { : log("[TextIndex] ", static_game_data_log.min_level) {
if (!directory.empty()) { if (!directory.empty()) {
auto add_version = [&](Version version, const string& subdirectory, function<shared_ptr<TextSet>(const string&, bool)> make_set) -> void { auto add_version = [&](Version version, const string& subdirectory, function<shared_ptr<TextSet>(const string&, bool)> make_set) -> void {
+5 -8
View File
@@ -138,11 +138,9 @@ uint32_t default_sub_version_for_version(Version version) {
} }
uint32_t default_specific_version_for_version(Version version, int64_t sub_version) { uint32_t default_specific_version_for_version(Version version, int64_t sub_version) {
// For versions that don't support send_function_call by default, we need // For versions that don't support send_function_call by default, we need to set the specific_version based on
// to set the specific_version based on sub_version. Fortunately, all // sub_version. Fortunately, all versions that share sub_version values also support send_function_call, so for those
// versions that share sub_version values also support send_function_call, // versions we get the specific_version later by sending VersionDetectDC, VersionDetectGC, or VersionDetectXB.
// so for those versions we get the specific_version later by sending the
// VersionDetectDC, VersionDetectGC, or VersionDetectXB call.
switch (version) { switch (version) {
case Version::DC_NTE: case Version::DC_NTE:
return SPECIFIC_VERSION_DC_NTE; // 1OJ1 (NTE) return SPECIFIC_VERSION_DC_NTE; // 1OJ1 (NTE)
@@ -151,7 +149,7 @@ uint32_t default_specific_version_for_version(Version version, int64_t sub_versi
case Version::DC_V1: case Version::DC_V1:
switch (sub_version) { switch (sub_version) {
case 0x20: case 0x20:
return SPECIFIC_VERSION_DC_V1_JP; // 1OJF (1OJ1 and 1OJ2 use 0x20 as well, but are detected without using sub_version) return SPECIFIC_VERSION_DC_V1_JP; // 1OJF (1OJ1 and 1OJ2 use 0x20 also, but are detected without sub_version)
case 0x21: case 0x21:
return SPECIFIC_VERSION_DC_V1_US; // 1OEF return SPECIFIC_VERSION_DC_V1_US; // 1OEF
case 0x22: case 0x22:
@@ -301,8 +299,7 @@ uint64_t generate_random_hardware_id(Version version) {
case Version::PC_V2: case Version::PC_V2:
return 0x0000FFFFFFFFFFFF; return 0x0000FFFFFFFFFFFF;
case Version::GC_NTE: case Version::GC_NTE:
// On GC NTE, the low byte is uninitialized memory from the TProtocol // On GC NTE, the low byte is uninitialized memory from the TProtocol constructor's stack
// constructor's stack
return phosg::random_object<uint8_t>(); return phosg::random_object<uint8_t>();
case Version::GC_V3: case Version::GC_V3:
case Version::GC_EP3_NTE: case Version::GC_EP3_NTE:
+3 -6
View File
@@ -210,9 +210,8 @@ WordSelectTable::WordSelectTable(
static_assert(NUM_NON_PATCH_VERSIONS == 12, "Don\'t forget to update the WordSelectTable constructor"); static_assert(NUM_NON_PATCH_VERSIONS == 12, "Don\'t forget to update the WordSelectTable constructor");
array<const WordSelectSet*, NUM_NON_PATCH_VERSIONS> ws_sets = { array<const WordSelectSet*, NUM_NON_PATCH_VERSIONS> ws_sets = {
&dc_nte_ws, &dc_112000_ws, &dc_v1_ws, &dc_v2_ws, &dc_nte_ws, &dc_112000_ws, &dc_v1_ws, &dc_v2_ws, &pc_nte_ws, &pc_v2_ws, &gc_nte_ws, &gc_v3_ws, &gc_ep3_nte_ws,
&pc_nte_ws, &pc_v2_ws, &gc_nte_ws, &gc_v3_ws, &gc_ep3_ws, &xb_v3_ws, &bb_v4_ws};
&gc_ep3_nte_ws, &gc_ep3_ws, &xb_v3_ws, &bb_v4_ws};
for (size_t s_version = 0; s_version < ws_sets.size(); s_version++) { for (size_t s_version = 0; s_version < ws_sets.size(); s_version++) {
Version version = static_cast<Version>(static_cast<size_t>(Version::DC_NTE) + s_version); Version version = static_cast<Version>(static_cast<size_t>(Version::DC_NTE) + s_version);
@@ -297,9 +296,7 @@ void WordSelectTable::validate(const WordSelectMessage& msg, Version version) co
} }
WordSelectMessage WordSelectTable::translate( WordSelectMessage WordSelectTable::translate(
const WordSelectMessage& msg, const WordSelectMessage& msg, Version from_version, Version to_version) const {
Version from_version,
Version to_version) const {
const auto& index = this->tokens_for_version(from_version); const auto& index = this->tokens_for_version(from_version);
WordSelectMessage ret; WordSelectMessage ret;
+7 -9
View File
@@ -7,11 +7,10 @@
.long_desc "" .long_desc ""
start: start:
// Create quest opcode handlers for F9FE to call flush_code and F9FF to call // Create quest opcode handlers for F9FE to call flush_code and F9FF to call the copied code. Fortunately,
// the copied code. Fortunately, quest_call_l leaves the byteswapped value of // quest_call_l leaves the byteswapped value of the opcode argument in r4, so as long as the address ends with 00,
// the opcode argument in r4, so as long as the address ends with 00, it will // it will be valid as the size argument to flush_code. We'll end up flushing many more bytes than needed, but this
// be valid as the size argument to flush_code. We'll end up flushing many // isn't a problem.
// more bytes than needed, but this isn't a problem.
leti r3, 0x80004000 // dest addr leti r3, 0x80004000 // dest addr
write4 0x804C81C8, 0x801F2A14 // quest_call_l write4 0x804C81C8, 0x801F2A14 // quest_call_l
write4 0x804C81CC, 0x8000C274 // flush_code write4 0x804C81CC, 0x8000C274 // flush_code
@@ -40,10 +39,9 @@ copy_done:
// Call the copied native code // Call the copied native code
.data F9FF .data F9FF
// This script runs on the first frame during the quest loading procedure, // This script runs on the first frame during the quest loading procedure, but this procedure is started from the
// but this procedure is started from the lobby overview, not from a game! // lobby overview, not from a game! To make the result of loading a quest sane, we need to set some extra state that
// To make the result of loading a quest sane, we need to set some extra // will take effect when loading is done.
// state that will take effect when loading is done.
ba_initial_floor 17 // Make player spawn in lobby (for one frame) ba_initial_floor 17 // Make player spawn in lobby (for one frame)
write2 0x805D5CE8, 1 // Leave "game" immediately (sends 98) write2 0x805D5CE8, 1 // Leave "game" immediately (sends 98)
+1 -2
View File
@@ -7,8 +7,7 @@
.long_desc "" .long_desc ""
start: start:
// This script is identical to q88530-gc-e.bin.txt, except the addresses are // This script is identical to q88530-gc-e.bin.txt, except the addresses are changed to be suitable for JP v1.5.
// changed to be suitable for JP v1.5.
leti r3, 0x80004000 leti r3, 0x80004000
write4 0x804C88F0, 0x801F29C0 write4 0x804C88F0, 0x801F29C0
+1 -2
View File
@@ -7,8 +7,7 @@
.long_desc "" .long_desc ""
start: start:
// This script is identical to q88530-gc-e.bin.txt, except the addresses are // This script is identical to q88530-gc-e.bin.txt, except the addresses are changed to be suitable for US Ep3.
// changed to be suitable for US Ep3.
leti r3, 0x80004000 leti r3, 0x80004000
write4 0x80452A4C, 0x80109B28 write4 0x80452A4C, 0x80109B28
+1 -2
View File
@@ -7,8 +7,7 @@
.long_desc "" .long_desc ""
start: start:
// This script is identical to q88530-gc-e.bin.txt, except the addresses are // This script is identical to q88530-gc-e.bin.txt, except the addresses are changed to be suitable for EU Ep3.
// changed to be suitable for EU Ep3.
leti r3, 0x80004000 leti r3, 0x80004000
write4 0x80454E04, 0x80109FB4 write4 0x80454E04, 0x80109FB4
+80 -93
View File
@@ -1,32 +1,26 @@
// This file documents newserv's quest assembler syntax and format. This is a // This file documents newserv's quest assembler syntax and format. This is a slightly modified copy of the English
// slightly modified copy of the English version of Sega's Lost HEAT SWORD quest // version of Sega's Lost HEAT SWORD quest for PSO GC.
// for PSO GC.
// Generally the metadata directives should appear before the quest's code. // Generally the metadata directives should appear before the quest's code. These specify the quest's name,
// These specify the quest's name, description, and other information. // description, and other information.
// The .version directive specifies which version of the game the quest is for. // The .version directive specifies which version of the game the quest is for. The values are DC_NTE, DC_11_2000,
// The values are DC_NTE, DC_11_2000, DC_V1, DC_V2, PC_V2, GC_NTE, GC_V3, // DC_V1, DC_V2, PC_V2, GC_NTE, GC_V3, GC_EP3_NTE, GC_EP3, XB_V3, and BB_V4. This determines which set of opcodes to
// GC_EP3_NTE, GC_EP3, XB_V3, and BB_V4. This determines which set of opcodes // use during compilation, and also specifies the header format and string encoding. This does not affect where the
// to use during compilation, and also specifies the header format and string // quest appears in menus, so for versions that use the same opcodes, headers, and string encodings, it is OK to use a
// encoding. This does not affect where the quest appears in menus, so for // symbolic link (hence q058-xb-e.bin.txt is a link to this file).
// versions that use the same opcodes, headers, and string encodings, it is OK
// to use a symbolic link (hence q058-xb-e.bin.txt is a link to this file).
.version GC_V3 .version GC_V3
// The .quest_num directive specifies the internal number of the quest. This // The .quest_num directive specifies the internal number of the quest. This has no meaning for online quests, though
// has no meaning for online quests, though it's recommended for this value to // it's recommended for this value to match the number in the filename. For download quests, the game deduplicates
// match the number in the filename. For download quests, the game deduplicates // quest files with the same number, so download quests should all have unique numbers in this field. On Episodes 1&2,
// quest files with the same number, so download quests should all have unique // this field must be in the range 0-255; on other versions, it can be 0-65535, but generally numbers less than 1000
// numbers in this field. On Episodes 1&2, this field must be in the range // are recommended.
// 0-255; on other versions, it can be 0-65535, but generally numbers less than
// 1000 are recommended.
.quest_num 58 .quest_num 58
// The .language field specifies the internal language of the quest. On console // The .language field specifies the internal language of the quest. On console versions (DC, GC, and XB), this affects
// versions (DC, GC, and XB), this affects how strings are encoded - Japanese // how strings are encoded - Japanese uses Shift-JIS and other languages use ISO8859. (On PC V2 and BB, UTF-16 is used
// uses Shift-JIS and other languages use ISO8859. (On PC V2 and BB, UTF-16 is // for strings in all languages.) The language values are:
// used for strings in all languages.) The language values are:
// J = Japanese // J = Japanese
// E = English // E = English
// G = German // G = German
@@ -37,96 +31,89 @@
// K = Korean // K = Korean
.language E .language E
// The .episode directive specifies the quest's episode. The server ignores this // The .episode directive specifies the quest's episode. The server ignores this if a set_episode or set_episode2
// if a set_episode or set_episode2 opcode is present in the code following the // opcode is present in the code following the start label.
// start label.
.episode Episode1 .episode Episode1
// These directives specify the quest's name, short description, and long // These directives specify the quest's name, short description, and long description. Non-ASCII characters can be used
// description. Non-ASCII characters can be used here and in the script below; // here and in the script below; this entire file is encoded as UTF-8 and strings are transcoded to the encoding the
// this entire file is encoded as UTF-8 and strings are transcoded to the // client expects based on the .version and .language directives. Common escape codes (e.g. \n for a newline) are
// encoding the client expects based on the .version and .language directives. // supported in these strings.
// Common escape codes (e.g. \n for a newline) are supported in these strings.
.name "Lost HEAT SWORD" .name "Lost HEAT SWORD"
.short_desc "Retrieve a\nweapon from\na Dragon!" .short_desc "Retrieve a\nweapon from\na Dragon!"
.long_desc "Client: Hopkins, hunter\nQuest:\n My weapon was taken\n from me when I was\n fighting a Dragon.\nReward: ??? Meseta\n\n\n" .long_desc "Client: Hopkins, hunter\nQuest:\n My weapon was taken\n from me when I was\n fighting a Dragon.\nReward: ??? Meseta\n\n\n"
// On BB, quests may specify a maximum number of players with this directive. If // On BB, quests may specify a maximum number of players with this directive. If not given, the default is 4. On non-BB
// not given, the default is 4. On non-BB versions, this directive is ignored. // versions, this directive is ignored.
// .max_players 4 // .max_players 4
// On BB, quests may be joinable while in progress. This directive enables that // On BB, quests may be joinable while in progress. This directive enables that capability.
// capability.
// .joinable // .joinable
// On BB, quests that create items via the script must specify which items are // On BB, quests that create items via the script must specify which items are allowed to be created. To do so, use
// allowed to be created. To do so, use this directive one or more times, which // this directive one or more times, which instructs the server to allow creation of that item. These masks can specify
// instructs the server to allow creation of that item. These masks can specify // each byte of the item's data1 as ranges, to allow for parameters in the item data. For example, this directive
// each byte of the item's data1 as ranges, to allow for parameters in the item // allows the quest to create Trifluids (030102) with stack sizes of 1-10:
// data. For example, this directive allows the quest to create Trifluids
// (030102) with stack sizes of 1-10:
// .allow_create_item 0301020000[01-0A]000000000000 // .allow_create_item 0301020000[01-0A]000000000000
// Another example: this directive allows the quest to create any weapon in the // Another example: this directive allows the quest to create any weapon in the basic rifle series, not including the
// basic rifle series, not including the rares in that series, with a grind // rares in that series, with a grind value of up to 5 and up to two bonuses between 0 and 30% each:
// value of up to 5 and up to two bonuses between 0 and 30% each:
// .allow_create_item 0007[00-04][00-05]0000[00-05][00-1E][00-05][00-1E]0000 // .allow_create_item 0007[00-04][00-05]0000[00-05][00-1E][00-05][00-1E]0000
// The quest script begins after the header directives. A quest script is a // The quest script begins after the header directives. A quest script is a sequence of opcodes, and labels denoting
// sequence of opcodes, and labels denoting positions within that sequence that // positions within that sequence that can be jumped to or called like a function. All labels have names, and some have
// can be jumped to or called like a function. All labels have names, and some // numbers. (In the compiled format, labels have only numbers and no names; during compilation, each label that doesn't
// have numbers. (In the compiled format, labels have only numbers and no names; // have a number is assigned a number that isn't in use by another label.) To explicitly specify a label number (for
// during compilation, each label that doesn't have a number is assigned a // example, if an object or NPC refers to a label by number), use an @ sign followed by the desired number. Note that
// number that isn't in use by another label.) To explicitly specify a label // numbers can be specified in decimal or hexadecimal; see on_talk_to_npc1 and on_talk_to_npc2 for examples.
// number (for example, if an object or NPC refers to a label by number), use an
// @ sign followed by the desired number. Note that numbers can be specified in
// decimal or hexadecimal; see on_talk_to_npc1 and on_talk_to_npc2 for examples.
// Registers may be named as well as labels. (In the compiled script, registers // Registers may be named as well as labels. (In the compiled script, registers do not have names, so disassembling a
// do not have names, so disassembling a quest script always produces only // quest script always produces only numbered registers.) When compiling, all of the following are valid:
// numbered registers.) When compiling, all of the following are valid:
// r83 (explicitly numbered register) // r83 (explicitly numbered register)
// r:difficulty_level (the compiler will assign an unused register number) // r:difficulty_level (the compiler will assign an unused register number)
// r:difficulty_level@83 (named and explicitly numbered) // r:difficulty_level@83 (named and explicitly numbered)
// You don't always have to use the same form for each register; for example, // You don't always have to use the same form for each register; for example, if you use r:difficulty_level@83 anywhere
// if you use r:difficulty_level@83 anywhere in the quest script, you can also // in the quest script, you can also use r:difficulty_level and r83 in other places and they will all refer to the same
// use r:difficulty_level and r83 in other places and they will all refer to the // register. (However, if you don't use r:difficulty_level@83 anywhere, but you do use r83 and r:difficulty_level, the
// same register. (However, if you don't use r:difficulty_level@83 anywhere, but // compiler will assign these to two different registers since there is nothing linking the name to the number.)
// you do use r83 and r:difficulty_level, the compiler will assign these to two
// different registers since there is nothing linking the name to the number.)
// Using opcodes that take a consecutive sequence of registers, such as // There are a few registers that have predefined names, since they have hardcoded behaviors on the client. These are:
// map_designate which takes 4, introduces constraints on which registers may be // r:quest_board_item1 = r74
// assigned to which numbers. For example, before one of the map_designate // r:quest_board_item2 = r75
// opcodes after the start label, we explicitly assign one register's number, // r:quest_board_item3 = r76
// but leave the nearby registers' numbers unassigned. The compiler assigns // r:quest_board_item4 = r77
// those four registers to r60-r63, because they are used in a map_designate // r:quest_board_item5 = r78
// call. If we didn't explicitly number any of those registers, the compiler // r:quest_board_item6 = r79
// would instead choose a consecutive sequence of register numbers that aren't // r:quest_board_item7 = r80
// used anywhere else in the script. // r:quest_failed = r253
// r:quest_succeeded = r255
// This quest does not contain any examples of non-script data, but such data // Using opcodes that take a consecutive sequence of registers, such as map_designate which takes 4, introduces
// can be included in the quest script using the .data directive, like this: // constraints on which registers may be assigned to which numbers. For example, before one of the map_designate
// opcodes after the start label, we explicitly assign one register's number, but leave the nearby registers' numbers
// unassigned. The compiler assigns those four registers to r60-r63, because they are used in a map_designate call. If
// we didn't explicitly number any of those registers, the compiler would instead choose a consecutive sequence of
// register numbers that aren't used anywhere else in the script.
// This quest does not contain any examples of non-script data, but such data can be included in the quest script using
// the .data directive, like this:
// hello_symbol_chat: // hello_symbol_chat:
// .data 28000000 FFFF 0D00 FFFF FFFF 05 18 1D 00 05 28 1D 01 36 20 2A 00 3C 00 32 00 FF 00 00 00 FF 00 00 00 FF 00 00 00 FF 00 00 02 FF 00 00 02 FF 00 00 02 FF 00 00 02 FF 00 00 02 // .data 28000000 FFFF 0D00 FFFF FFFF 05 18 1D 00 05 28 1D 01 36 20 2A 00 3C 00 32 00 FF 00 00 00 FF 00 00 00 FF 00 00 00 FF 00 00 02 FF 00 00 02 FF 00 00 02 FF 00 00 02 FF 00 00 02
// You can also include binary data from another file in the same directory // You can also include binary data from another file in the same directory (the contents of the file are "pasted" into
// (the contents of the file are "pasted" into the assembled script, as if you // the assembled script, as if you had pasted in the hex along with a .data directive):
// had pasted in the hex along with a .data directive):
// movement_data: // movement_data:
// .include_bin movement_data.bin // .include_bin movement_data.bin
// There is also a directive for including a large number of zero bytes: // There is also a directive for including a large number of zero bytes:
// lots_of_zeroes: // lots_of_zeroes:
// .zero 0x400 // 1024 bytes of zeroes // .zero 0x400 // 1024 bytes of zeroes
// There is also a way for quest scripts to include other files. This works by // There is also a way for quest scripts to include other files. This works by simply "pasting" the contents of the
// simply "pasting" the contents of the file in place of the include directive, // file in place of the include directive, so all labels in the included file will be accessible from the file that
// so all labels in the included file will be accessible from the file that // included it. newserv looks for the included file in the same directory as the quest file, then looks in the
// included it. newserv looks for the included file in the same directory as // system/quest/includes directory. The syntax for this is:
// the quest file, then looks in the system/quest/includes directory. Here's // .include my-function.txt
// the syntax:
// .include my-function.txt
// Every quest must have a start label; this is the main thread that starts when // Every quest must have a start label; this is the main thread that starts when the quest begins. The start label is
// the quest begins. The start label is always assigned number 0. // always assigned number 0.
start: start:
gget 0x0091, r:flag_0091_value@252 gget 0x0091, r:flag_0091_value@252
set_floor_handler 0, floor_handler_pioneer_2 set_floor_handler 0, floor_handler_pioneer_2
@@ -135,26 +122,26 @@ start:
set_floor_handler 11, floor_handler_dragon set_floor_handler 11, floor_handler_dragon
set_qt_success on_quest_success set_qt_success on_quest_success
get_difficulty_level_v2 r:difficulty_level@83 get_difficulty_level_v2 r:difficulty_level@83
leti r:op_arg1, 0 // Pioneer 2 leti r:op_arg1, 0
leti r:op_arg2, 0 leti r:op_arg2, 0
leti r:op_arg3@62, 0 // See comment above about register assignment leti r:op_arg3@62, 0 // See comment above about register assignment
leti r:op_arg4, 0 leti r:op_arg4, 0
map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4) map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4) // Pioneer 2
leti r:op_arg1, 1 // Forest 1 leti r:op_arg1, 1
leti r:op_arg2, 0 leti r:op_arg2, 0
leti r:op_arg3, 0 leti r:op_arg3, 0
leti r:op_arg4, 0 leti r:op_arg4, 0
map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4) map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4) // Forest 1
leti r:op_arg1, 2 // Forest 2 leti r:op_arg1, 2
leti r:op_arg2, 0 leti r:op_arg2, 0
leti r:op_arg3, 0 leti r:op_arg3, 0
leti r:op_arg4, 0 leti r:op_arg4, 0
map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4) map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4) // Forest 2
leti r:op_arg1, 11 // Dragon leti r:op_arg1, 11
leti r:op_arg2, 0 leti r:op_arg2, 0
leti r:op_arg3, 0 leti r:op_arg3, 0
leti r:op_arg4, 0 leti r:op_arg4, 0
map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4) map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4) // Dragon
ret ret
return_immediately: return_immediately: