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;
// TODO: fix style in this file, especially in psobb functions
RandomGenerator::RandomGenerator(uint32_t seed) : initial_seed(seed) {}
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)
: PSOLFGEncryption(seed, STREAM_LENGTH + 1, STREAM_LENGTH) {
PSOV2Encryption::PSOV2Encryption(uint32_t seed) : PSOLFGEncryption(seed, STREAM_LENGTH + 1, STREAM_LENGTH) {
uint32_t a = 1, b = this->initial_seed;
this->stream[0x37] = b;
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;
}
PSOV3Encryption::PSOV3Encryption(uint32_t seed)
: PSOLFGEncryption(seed, STREAM_LENGTH, STREAM_LENGTH) {
PSOV3Encryption::PSOV3Encryption(uint32_t seed) : PSOLFGEncryption(seed, STREAM_LENGTH, STREAM_LENGTH) {
uint32_t x, y, basekey, source1, source2, source3;
basekey = 0;
@@ -154,9 +150,7 @@ PSOEncryption::Type PSOV3Encryption::type() const {
return Type::V3;
}
PSOBBEncryption::PSOBBEncryption(
const KeyFile& key, const void* original_seed, size_t seed_size)
: state(key) {
PSOBBEncryption::PSOBBEncryption(const KeyFile& key, const void* original_seed, size_t seed_size) : state(key) {
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) {
// Note: This part is done in the 03 command handler in the BB client, and
// isn't actually part of the encryption library. (Why did they do this?)
// Note: This part is done in the 03 command handler in the BB client, and isn't actually part of the encryption
// library. (Why did they do this?)
string seed;
const uint8_t* original_seed_data = reinterpret_cast<const uint8_t*>(
original_seed);
const uint8_t* original_seed_data = reinterpret_cast<const uint8_t*>(original_seed);
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 + 1] ^ 0x16);
@@ -614,12 +607,8 @@ void PSOBBEncryption::apply_seed(const void* original_seed, size_t seed_size) {
}
PSOV2OrV3DetectorEncryption::PSOV2OrV3DetectorEncryption(
uint32_t key,
const std::unordered_set<uint32_t>& v2_matches,
const std::unordered_set<uint32_t>& v3_matches)
: key(key),
v2_matches(v2_matches),
v3_matches(v3_matches) {}
uint32_t key, const std::unordered_set<uint32_t>& v2_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) {
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);
if (!v2_match && !v3_match) {
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) {
throw runtime_error(std::format("ambiguous crypt version (v2={:08X}, v3={:08X})", decrypted_v2, decrypted_v3));
} else if (v2_match) {
@@ -665,8 +655,7 @@ PSOEncryption::Type PSOV2OrV3DetectorEncryption::type() const {
PSOV2OrV3ImitatorEncryption::PSOV2OrV3ImitatorEncryption(
uint32_t key, std::shared_ptr<PSOV2OrV3DetectorEncryption> detector_crypt)
: key(key),
detector_crypt(detector_crypt) {}
: key(key), detector_crypt(detector_crypt) {}
void PSOV2OrV3ImitatorEncryption::encrypt(void* data, size_t size) {
if (!this->active_crypt) {
@@ -761,9 +750,8 @@ shared_ptr<PSOBBEncryption> PSOBBMultiKeyImitatorEncryption::ensure_crypt() {
if (!key.get()) {
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
// ignores the server seed (though each end has its own state after that).
// To handle this, we use the other crypt's seed if the type is JSD1.
// Hack: JSD1 uses the client seed for both ends of the connection and ignores the server seed (though each end has
// its own state after that). 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) {
const auto& detector_seed = this->detector_crypt->get_seed();
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 mask = (value >> 0x10);
uint8_t mask_one_bits = count_one_bits(mask);
return ((mask_one_bits < 4) || (mask_one_bits > 12))
? 0xFFFF
: ((mask ^ value) & 0xFFFF);
return ((mask_one_bits < 4) || (mask_one_bits > 12)) ? 0xFFFF : ((mask ^ value) & 0xFFFF);
}
string decrypt_v2_registry_value(const void* data, size_t size) {
+9 -15
View File
@@ -159,8 +159,7 @@ public:
};
struct KeyFile {
// initial_keys are actually a stream of uint32_ts, but we treat them as
// bytes for code simplicity
// initial_keys are actually a stream of uint32_ts, but we treat them as bytes for code simplicity
union InitialKeys {
uint8_t jsd1_stream_offset;
parray<uint8_t, 0x48> as8;
@@ -176,11 +175,9 @@ public:
} __packed_ws__(PrivateKeys, 0x1000);
InitialKeys initial_keys;
PrivateKeys private_keys;
// This field only really needs to be one byte, but annoyingly, some
// compilers pad this structure to a longer alignment, presumably because
// the unions above contain structures with 32-bit alignment. To prevent
// this structure's size from not matching the .nsk files' sizes, we use
// an unnecessarily large size for this field.
// This field only really needs to be one byte, but annoyingly, some compilers pad this structure to a longer
// alignment, presumably because the unions above contain structures with 32-bit alignment. To prevent this
// structure's size from not matching the .nsk files' sizes, we use an unnecessarily large size for this field.
le_uint64_t subtype;
} __packed_ws__(KeyFile, 0x1050);
@@ -198,8 +195,8 @@ protected:
void apply_seed(const void* original_seed, size_t seed_size);
};
// The following classes provide support for automatically detecting which type
// of encryption a client is using based on their initial response to the server
// The following classes provide support for automatically detecting which type of encryption a client is using based
// on their initial response to the server
class PSOV2OrV3DetectorEncryption : public PSOEncryption {
public:
@@ -234,9 +231,8 @@ protected:
std::shared_ptr<PSOEncryption> active_crypt;
};
// The following classes provide support for multiple PSOBB private keys, and
// the ability to automatically detect which key the client is using based on
// the first 8 bytes they send
// The following classes provide support for multiple PSOBB private keys, and the ability to automatically detect which
// key the client is using based on the first 8 bytes they send
class PSOBBMultiKeyDetectorEncryption : public PSOEncryption {
public:
@@ -397,9 +393,7 @@ DecryptedPR2 decrypt_pr2_data(const std::string& data) {
throw std::runtime_error("not enough data for PR2 header");
}
phosg::StringReader r(data);
DecryptedPR2 ret = {
.compressed_data = data.substr(8),
.decompressed_size = r.get<U32T<BE>>()};
DecryptedPR2 ret = {.compressed_data = data.substr(8), .decompressed_size = r.get<U32T<BE>>()};
PSOV2Encryption crypt(r.get<U32T<BE>>());
if (BE) {
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) {
if (size < min_size) {
throw std::runtime_error(std::format(
"command too small (expected at least 0x{:X} bytes, received 0x{:X} bytes)",
min_size, size));
"command too small (expected at least 0x{:X} bytes, received 0x{:X} bytes)", min_size, size));
}
if (max_size < min_size) {
max_size = min_size;
}
if (size > max_size) {
throw std::runtime_error(std::format(
"command too large (expected at most 0x{:X} bytes, received 0x{:X} bytes)",
max_size, size));
"command too large (expected at most 0x{:X} bytes, received 0x{:X} bytes)", max_size, size));
}
}
std::string prepend_command_header(
Version version,
bool encryption_enabled,
uint16_t cmd,
uint32_t flag,
const std::string& data) {
Version version, bool encryption_enabled, uint16_t cmd, uint32_t flag, const std::string& data) {
phosg::StringWriter ret;
switch (version) {
case Version::DC_NTE:
+5 -16
View File
@@ -46,23 +46,16 @@ union PSOCommandHeader {
PSOCommandHeader();
} __packed_ws__(PSOCommandHeader, 8);
// This function is used in a lot of places to check received command sizes and
// cast them to the appropriate type
// This function is used in a lot of places to check received command sizes and cast them to the appropriate type
template <typename RetT, typename PtrT>
RetT& check_size_generic(
PtrT data,
size_t size,
size_t min_size,
size_t max_size) {
RetT& check_size_generic(PtrT data, size_t size, size_t min_size, size_t max_size) {
if (size < min_size) {
throw std::runtime_error(std::format(
"command too small (expected at least 0x{:X} bytes, received 0x{:X} bytes)",
min_size, size));
"command too small (expected at least 0x{:X} bytes, received 0x{:X} bytes)", min_size, size));
}
if (size > max_size) {
throw std::runtime_error(std::format(
"command too large (expected at most 0x{:X} bytes, received 0x{:X} bytes)",
max_size, size));
"command too large (expected at most 0x{:X} bytes, received 0x{:X} bytes)", max_size, size));
}
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);
std::string prepend_command_header(
Version version,
bool encryption_enabled,
uint16_t cmd,
uint32_t flag,
const std::string& data);
Version version, 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
// commands in either direction. The functions have abbreviated names in order
// to make the massive table more readable. The functions' names are, in
// general, <SC>_[VERSIONS]_<COMMAND-NUMBERS>, where <SC> denotes who sent the
// command, VERSIONS denotes which versions this handler is for (with shortcuts
// - so v123 refers to all non-BB versions, for example, and DGX refers to all
// 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).
// Command handlers. These are called to preprocess or react to specific commands in either direction. The functions
// have abbreviated names in order to make the massive table more readable. The functions' names are, in general,
// <SC>_[VERSIONS]_<COMMAND-NUMBERS>, where <SC> denotes who sent the command, VERSIONS denotes which versions this
// handler is for (with shortcuts - so v123 refers to all non-BB versions, for example, and DGX refers to all 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&) {
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&) {
// We always assume a 97 has already been received by the client - we should
// have sent 97 01 before sending the client to the proxy server.
// We always assume a 97 has already been received by the client - we should have sent 97 01 before sending the
// client to the proxy server.
c->proxy_session->server_channel->send(0xB1, 0x00);
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&) {
// TODO: Either delete this handler or finish implementing it (flag=00/02
// should do the below, 01 should send 9C, anything else should end the
// session)
// TODO: Either delete this handler or finish implementing it (flag=00/02 should do the below, 01 should send 9C,
// anything else should end the session)
C_LoginExtended_GC_9E cmd;
if (c->proxy_session->remote_guild_card_number < 0) {
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.client_config = c->proxy_session->remote_client_config_data;
// If there's a guild card number, a shorter 9E is sent that ends
// right after the client config data
// If there's a guild card number, a shorter 9E is sent that ends right after the client config data
c->proxy_session->server_channel->send(
0x9E, 0x01, &cmd,
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");
}
// Most servers don't include after_message or have a shorter
// after_message than newserv does, so don't require it
// Most servers don't include after_message or have a shorter 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);
// 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);
}
// Respond with an appropriate login command. We don't let the client do this
// because it believes it already did (when it was in an unlinked session, or
// in the patch server case, during the current session due to a hidden
// Respond with an appropriate login command. We don't let the client do this because it believes it already did
// (when it was in an unlinked session, or in the patch server case, during the current session due to a hidden
// redirect).
switch (c->version()) {
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) {
// Most servers don't include after_message or have a shorter after_message
// than newserv does, so don't require it
// Most servers don't include after_message or have a shorter after_message than newserv does, so don't require it
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.
// Use the same crypt type as the client... the server has the luxury of
// being able to try all the crypts it knows to detect what type the client
// uses, but the client can't do this since it sends the first encrypted
// data on the connection.
// This isn't forwarded to the client, so only recreate the server's crypts. Use the same crypt type as the client...
// the server has the luxury of being able to try all the crypts it knows to detect what type the client uses, but
// the client can't do this since it sends the first encrypted data on the connection.
if (!c->bb_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;
}
// Some servers send a short 04 command if they don't use all of the 0x20
// bytes available. We should be prepared to handle that.
// Some servers send a short 04 command if they don't use all of the 0x20 bytes available. We should be prepared to
// handle that.
auto& cmd = msg.check_size_t<S_UpdateClientConfig_V3_04>(
offsetof(S_UpdateClientConfig_V3_04, client_config),
sizeof(S_UpdateClientConfig_V3_04));
offsetof(S_UpdateClientConfig_V3_04, client_config), sizeof(S_UpdateClientConfig_V3_04));
// If this is a logged-in session, hide the guild card number assigned by the
// remote server so the client doesn't see it change. If this is a logged-out
// session, then the client never received a guild card number from newserv
// If this is a logged-in session, hide the guild card number assigned by the remote server so the client doesn't see
// it change. If this is a logged-out 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.
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) {
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);
string message = std::format(
"The remote server\nhas assigned your\nGuild Card number:\n$C6{}",
c->proxy_session->remote_guild_card_number);
"The remote server\nhas assigned your\nGuild Card number:\n$C6{}", c->proxy_session->remote_guild_card_number);
send_ship_info(c->channel, message);
}
if (c->login) {
cmd.guild_card_number = c->login->account->account_id;
}
// It seems the client ignores the length of the 04 command, and always copies
// 0x20 bytes to its config data. So if the server sends a short 04 command,
// part of the previous command ends up in the security data (usually part of
// the copyright string from the server init command). We simulate that here.
// If there was previously a guild card number, assume we got the lobby server
// init text instead of the port map init text.
// It seems the client ignores the length of the 04 command, and always copies 0x20 bytes to its config data. So if
// the server sends a short 04 command, 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
// 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(),
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,
min<size_t>(msg.data.size() - offsetof(S_UpdateClientConfig_V3_04, client_config),
c->proxy_session->remote_client_config_data.bytes()));
// If the guild card number was not set, pretend (to the server) that this is
// the first 04 command the client has received. The client responds with a 96
// (checksum) in that case.
// If the guild card number was not set, pretend (to the server) that this is the first 04 command the client has
// received. The client responds with a 96 (checksum) in that case.
if (!had_guild_card_number) {
le_uint64_t checksum = phosg::random_object<uint64_t>() & 0x0000FFFFFFFFFFFF;
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,
// and the message has private_flags, and the private_flags say that you
// shouldn't see the message, then change the private_flags
// If the session is Ep3, and Unmask Whispers is on, and there's enough data, and the message has private_flags, and
// the private_flags say that you shouldn't see the message, then change the private_flags
if (is_ep3(c->version()) &&
c->check_flag(Client::Flag::PROXY_EP3_UNMASK_WHISPERS) &&
(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>;
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
// it receives this while loading; other versions probably also will crash)
// If the client isn't in the lobby, suppress the command (Ep3 can crash if it receives this while loading; other
// versions probably also will crash)
if (!c->proxy_session->is_in_lobby) {
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&) {
// Block all time updates from the remote server, so client's time remains
// consistent
// Block all time updates from the remote server, so client's time remains consistent
c->proxy_session->server_channel->send(0x99, 0x00);
co_return HandlerResult::SUPPRESS;
}
@@ -597,7 +578,6 @@ static asio::awaitable<HandlerResult> S_B2(shared_ptr<Client> c, Channel::Messag
using FooterT = RELFileFooterT<BE>;
// TODO: Support SH-4 disassembly too
bool is_ppc = ::is_ppc(c->version());
bool is_x86 = ::is_x86(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;
if (c->login && c->login->account->account_id != c->proxy_session->remote_guild_card_number) {
size_t expected_size = sizeof(CmdT) * msg.flag;
// Some servers (e.g. Schtserv) send extra data on the end of this command;
// the client ignores it so we can ignore it too
// Schtserv sends extra data on the end of this command; the client ignores it so we can ignore it too
auto* entries = &msg.check_size_t<CmdT>(expected_size, 0xFFFF);
for (size_t x = 0; x < msg.flag; x++) {
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) {
// We use this command (which is sent before the init encryption command) to
// detect a particular server behavior that we'll have to work around later.
// It looks like this command's existence is an anti-proxy measure, since
// this command is 0x34 bytes in total, and the logic that adds padding bytes
// when the command size isn't a multiple of 8 is only active when encryption
// is enabled. Presumably some simpler proxies would get this wrong.
// 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.
// We use this command (which is sent before the init encryption command) to detect a particular server behavior that
// we'll have to work around later. It looks like this command's existence is an anti-proxy measure, since this
// command is 0x34 bytes in total, and the logic that adds padding bytes when the command size isn't a multiple of 8
// is only active when encryption is enabled. Presumably some simpler proxies would get this wrong.
// 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)) {
c->log.info_f("Enabling remote IP CRC patch");
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) {
// If the command is shorter than 6 bytes, use the previous server command to
// fill it in. This simulates a behavior used by some private servers where a
// 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 previous command's data as part of
// the 19 command's contents. They presumably do this in an attempt to prevent
// If the command is shorter than 6 bytes, use the previous server command to fill it in. This simulates a behavior
// used by some private servers where a 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
// 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.
if (msg.data.size() < sizeof(c->proxy_session->prev_server_command_bytes)) {
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);
new_ep = make_endpoint_ipv6(cmd.address.data(), cmd.port);
} else {
// This weird maximum size is here to properly handle the version-split
// command that some servers (including newserv) use on port 9100
// This weird maximum size is here to properly handle the version-split command that some servers (including
// newserv) use on port 9100
auto& cmd = msg.check_size_t<S_Reconnect_19>(0xFFFF);
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&) {
// If the client is a version that sends close confirmations and the client
// has the no-close-confirmation flag set in its newserv client config, send a
// fake confirmation to the remote server immediately.
// If the client is a version that sends close confirmations and the client has the no-close-confirmation flag set in
// its newserv client config, send a fake confirmation to the remote server immediately.
if (is_v3(c->version()) && c->check_flag(Client::Flag::NO_D6)) {
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) {
bool modified = false;
// TODO: We should check if the info board text was actually modified and
// return MODIFIED if so.
// TODO: We should check if the info board text was actually modified and return MODIFIED if so.
if (is_v4(c->version())) {
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;
if (msg.flag == 4) { // Episode 3
auto& ep3_pd = msg.check_size_t<C_CharacterData_Ep3_61_98>();
// Technically we could decrypt the Ep3 config struct within the player
// data, but this may confuse some non-newserv upstream servers if they
// implement this structure incorrectly. The decryption would go like:
// Technically we could decrypt the Ep3 config struct within the player data, but this may confuse the upstream
// server if it implements this structure incorrectly. The decryption would go like:
// if (ep3_pd.ep3_config.is_encrypted) {
// decrypt_trivial_gci_data(
// &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) {
msg.data.push_back(0);
}
// TODO: We should check if the info board text was actually modified and
// return FORWARD if not.
// TODO: We should check if the info board text was actually modified and return FORWARD if not.
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) {
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
// return HandlerResult::FORWARD if not.
// TODO: We should check if the info board text was actually modified and return HandlerResult::FORWARD if not.
co_return HandlerResult::MODIFIED;
}
@@ -1250,8 +1219,8 @@ static asio::awaitable<HandlerResult> S_44_A6(shared_ptr<Client> c, Channel::Mes
} else {
basename = filename;
}
output_filename = std::format("{}.{}.{}{}",
basename, is_download ? "download" : "online", phosg::now(), extension);
output_filename = std::format(
"{}.{}.{}{}", basename, is_download ? "download" : "online", phosg::now(), extension);
for (size_t x = 0; x < output_filename.size(); 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);
}
// Unset the flag specifying that the client has newserv's card definitions,
// so the file sill be sent again if the client returns to newserv.
// Unset the flag specifying that the client has newserv's card definitions, so the file sill be sent again if the
// client returns to newserv.
c->clear_flag(Client::Flag::HAS_EP3_CARD_DEFS);
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
// (hence not using is_ep3() here)
// This command exists only in final Episode 3 and not in Trial Edition (hence not using is_ep3() here)
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&) {
// See the comments on EF in CommandFormats.hh for why we unconditionally
// suppress these.
// See the comments on EF in CommandFormats.hh for why we unconditionally suppress these.
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->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)) {
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;
}
} else if (c->check_flag(Client::Flag::PROXY_PLAYER_NOTIFICATIONS_ENABLED) && (msg.command != 0x67)) {
send_text_message_fmt(c->channel, "$C6Join: {}/{}\n{}",
index, entry.lobby_data.guild_card_number, name);
send_text_message_fmt(c->channel, "$C6Join: {}/{}\n{}", index, entry.lobby_data.guild_card_number, name);
}
auto& p = c->proxy_session->lobby_players[index];
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.section_id = entry.disp.visual.section_id;
p.char_class = entry.disp.visual.char_class;
c->log.info_f("Added lobby player: ({}) {} {}",
index, p.guild_card_number, p.name);
c->log.info_f("Added lobby player: ({}) {} {}", index, p.guild_card_number, p.name);
}
}
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_ep3 = &msg.check_size_t<S_JoinGame_Ep3_64>();
} else if (c->version() == Version::XB_V3) {
// Schtserv doesn't send the unknown_a1 field in this command, and we don't
// use it here, so we allow it to be omitted.
// Schtserv doesn't send the unknown_a1 field here, and we don't use it, so we allow it to be omitted.
cmd = &msg.check_size_t<CmdT>(sizeof(CmdT) - 0x18, sizeof(CmdT));
} else {
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_difficulty = cmd->difficulty;
c->proxy_session->lobby_section_id = cmd->section_id;
// We only need the game mode for overriding drops, and SOLO behaves the same
// as NORMAL in that regard, so we can conveniently ignore SOLO here
// We only need the game mode for overriding drops, and SOLO behaves the same as NORMAL in that regard, so we can
// conveniently ignore SOLO here
if (cmd->battle_mode) {
c->proxy_session->lobby_mode = GameMode::BATTLE;
} 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) {
// GC NTE ignores the variations field entirely, so clear the array to
// ensure we'll load the correct maps
// GC NTE ignores the variations field entirely, so clear the array to ensure we'll load the correct maps
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);
if (!is_ep3(c->version()) && (c->proxy_session->lobby_mode != GameMode::CHALLENGE)) {
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->id,
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) {
// Schtserv sends a large command here for unknown reasons. The client ignores
// the extra data, so we allow the large command here.
// Schtserv sends a large command here for unknown reasons. The client ignores the extra data, so we allow the large
// command here.
const auto& cmd = msg.check_size_t<S_LeaveLobby_66_69_Ep3_E9>(0xFFFF);
size_t index = cmd.client_id;
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;
case 0x06:
// On BB, the 6x06 command is blank - the server generates the actual
// Guild Card contents and sends it to the target client, so we only
// expect data here if the client isn't BB.
// On BB, the 6x06 command is blank - the server generates the actual Guild Card contents and sends it to the
// target client, so we only expect data here if the client isn't BB.
if (!is_v4(c->version()) &&
c->login &&
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>;
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
// original server (ending the proxy session), except on BB.
// We override Change Ship and Change Block to send the player back to the original server (ending the proxy
// session), except on BB.
c->proxy_session->server_channel->disconnect();
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(
shared_ptr<Client> c, shared_ptr<ProxySession> ses, shared_ptr<Channel> channel) {
std::string error_str;
// server_channel can be changed by receiving a 19 command, hence the
// exception handler is inside the loop here
// server_channel can be changed by receiving a 19 command, hence the exception handler is inside the loop here
while ((c->proxy_session == ses) && (ses->server_channel == channel) && channel->connected()) {
unique_ptr<Channel::Message> msg;
try {
@@ -2472,8 +2431,7 @@ asio::awaitable<void> handle_proxy_server_commands(
if (ec == asio::error::eof || ec == asio::error::connection_reset) {
error_str = "Server channel\ndisconnected";
} else if (ec == asio::error::operation_aborted) {
// This happens when the player chooses Change Ship/Change Block, so we
// don't show an error message
// This happens when the player chooses Change Ship/Change Block, so we don't show an error message
} else {
error_str = e.what();
}
+2 -3
View File
@@ -55,9 +55,8 @@ struct ProxySession {
std::shared_ptr<MapState> map_state;
std::shared_ptr<const std::string> last_bin_contents;
std::shared_ptr<const std::string> last_dat_contents;
// Note: We intentionally don't use the client's item ID space because the
// client may create items at the same time as the proxy, so server/client
// state could go out of sync
// Note: We intentionally don't use the client's item ID space because the client may create items at the same time
// as the proxy, so server/client state could go out of sync
uint32_t next_item_id = 0x44000000;
struct PersistentConfig {
+75 -118
View File
@@ -22,8 +22,7 @@
using namespace std;
QuestCategoryIndex::Category::Category(uint32_t category_id, const phosg::JSON& json)
: category_id(category_id) {
QuestCategoryIndex::Category::Category(uint32_t category_id, const phosg::JSON& json) : category_id(category_id) {
this->enabled_flags = json.get_int(0);
this->directory_name = json.get_string(1);
this->name = json.get_string(2);
@@ -46,9 +45,8 @@ shared_ptr<const QuestCategoryIndex::Category> QuestCategoryIndex::at(uint32_t c
template <bool BE>
struct PSOMemCardDLQFileEncryptedHeaderT {
U32T<BE> round2_seed;
// To compute checksum, set checksum to zero, then compute the CRC32 of the
// entire data section, including this header struct (but not the unencrypted
// header struct).
// To compute checksum, set checksum to zero, then compute the CRC32 of the entire data section, including this
// header struct (but not the unencrypted header struct).
U32T<BE> checksum;
le_uint32_t decompressed_size;
le_uint32_t round3_seed;
@@ -67,15 +65,12 @@ string decrypt_download_quest_data_section(
size_t orig_size = decrypted.size();
decrypted.resize((decrypted.size() + 3) & (~3));
// Note: Other PSO save files have the round 2 seed at the end of the data,
// not at the beginning. Presumably they did this because the system,
// character, and Guild Card files are a constant size, but download quest
// files can vary in size.
// Other PSO save files have the round 2 seed at the end, not at the beginning. Presumably this is because the
// system, character, and Guild Card files are a constant size, but download quest files can vary in size.
using HeaderT = PSOMemCardDLQFileEncryptedHeaderT<BE>;
auto* header = reinterpret_cast<HeaderT*>(decrypted.data());
PSOV2Encryption round2_crypt(header->round2_seed);
round2_crypt.encrypt_t<BE>(
decrypted.data() + 4, (decrypted.size() - 4));
round2_crypt.encrypt_t<BE>(decrypted.data() + 4, (decrypted.size() - 4));
if (is_ep3_trial) {
phosg::StringReader r(decrypted);
@@ -85,9 +80,8 @@ string decrypt_download_quest_data_section(
}
r.skip(9);
// Some Ep3 trial download quests don't have a stop opcode in the PRS
// stream; it seems the client just automatically stops when the correct
// amount of data has been produced. To handle this, we allow the PRS stream
// Some Ep3 trial download quests don't have a stop opcode in the PRS stream; it seems the client just
// automatically stops when the correct amount of data has been produced. To handle this, we allow the PRS stream
// to be unterminated here.
size_t decompressed_size = prs_decompress_size(
r.getv(r.remaining(), false), r.remaining(), sizeof(Episode3::MapDefinitionTrial), true);
@@ -100,8 +94,7 @@ string decrypt_download_quest_data_section(
} else {
if (header->decompressed_size & 0xFFF00000) {
throw runtime_error(std::format(
"decompressed_size too large ({:08X})", header->decompressed_size));
throw runtime_error(std::format("decompressed_size too large ({:08X})", header->decompressed_size));
}
if (!skip_checksum) {
@@ -111,29 +104,23 @@ string decrypt_download_quest_data_section(
header->checksum = expected_crc;
if (expected_crc != actual_crc && expected_crc != phosg::bswap32(actual_crc)) {
throw runtime_error(std::format(
"incorrect decrypted data section checksum: expected {:08X}; received {:08X}",
expected_crc, actual_crc));
"incorrect decrypted data section checksum: expected {:08X}; received {:08X}", expected_crc, actual_crc));
}
}
// Unlike the above rounds, round 3 is always little-endian (it corresponds to
// the round of encryption done on the server before sending the file to the
// client in the first place)
// Unlike the above rounds, round 3 is always little-endian (it corresponds to the round of encryption done on the
// server before sending the file to the client in the first place)
PSOV2Encryption(header->round3_seed).decrypt(decrypted.data() + sizeof(HeaderT), decrypted.size() - sizeof(HeaderT));
decrypted.resize(orig_size);
// Some download quest GCI files have decompressed_size fields that are 8
// bytes smaller than the actual decompressed size of the data. They seem to
// work fine, so we accept both cases as correct.
// Some download quest GCI files have decompressed_size fields that are 8 bytes smaller than the actual
// decompressed size of the data. They seem to work fine, so we accept both cases as correct.
size_t decompressed_size = prs_decompress_size(
decrypted.data() + sizeof(HeaderT),
decrypted.size() - sizeof(HeaderT));
decrypted.data() + sizeof(HeaderT), decrypted.size() - sizeof(HeaderT));
size_t expected_decompressed_size = header->decompressed_size;
if ((decompressed_size != expected_decompressed_size) &&
(decompressed_size != expected_decompressed_size - 8)) {
if ((decompressed_size != expected_decompressed_size) && (decompressed_size != expected_decompressed_size - 8)) {
throw runtime_error(std::format(
"decompressed size ({}) does not match expected size ({})",
decompressed_size, expected_decompressed_size));
"decompressed size ({}) does not match expected size ({})", decompressed_size, expected_decompressed_size));
}
return decrypted.substr(sizeof(HeaderT));
@@ -169,8 +156,7 @@ string find_seed_and_decrypt_download_quest_data_section(
string result;
uint64_t result_seed = phosg::parallel_range_blocks<uint64_t>([&](uint64_t seed, size_t) {
try {
string ret = decrypt_download_quest_data_section<BE>(
data_section, size, seed, skip_checksum, is_ep3_trial);
string ret = decrypt_download_quest_data_section<BE>(data_section, size, seed, skip_checksum, is_ep3_trial);
lock_guard<mutex> g(result_lock);
result = std::move(ret);
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);
}
Quest::Quest(shared_ptr<const VersionedQuest> initial_version)
: meta(initial_version->meta), supermap(nullptr) {
Quest::Quest(shared_ptr<const VersionedQuest> initial_version) : meta(initial_version->meta), supermap(nullptr) {
this->add_version(initial_version);
}
@@ -320,10 +305,7 @@ phosg::JSON Quest::json() const {
}));
}
return phosg::JSON::dict({
{"Metadata", this->meta.json()},
{"Versions", std::move(versions_json)},
});
return phosg::JSON::dict({{"Metadata", this->meta.json()}, {"Versions", std::move(versions_json)}});
}
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;
}
QuestIndex::QuestIndex(const string& directory, shared_ptr<const QuestCategoryIndex> category_index, bool raise_on_any_failure)
: directory(directory),
category_index(category_index) {
QuestIndex::QuestIndex(
const string& directory, shared_ptr<const QuestCategoryIndex> category_index, bool raise_on_any_failure)
: directory(directory), category_index(category_index) {
struct FileData {
string filename;
@@ -462,9 +444,8 @@ QuestIndex::QuestIndex(const string& directory, shared_ptr<const QuestCategoryIn
if (!files.emplace(basename, FileData{filename, data_ptr}).second) {
throw runtime_error("file " + basename + " already exists");
}
// There is a bug in the client that prevents quests from loading properly
// if any file's size is a multiple of 0x400. See the comments on the 13
// command in CommandFormats.hh for more details.
// There is a bug in the client that prevents quests from loading properly if any file's size is a multiple of
// 0x400. See the comments on the 13 command in CommandFormats.hh for more details.
if (check_chunk_size && !(data_ptr->size() & 0x3FF)) {
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
// different), so we use bin_files as the primary list of all quests that
// should be indexed
// All quests have a bin file (even in Episode 3, though its format is different), so we use bin_files as the primary
// list of all quests that should be indexed
unordered_map<const FileData*, shared_ptr<const phosg::JSON>> parsed_json_files;
for (auto& [basename, entry] : bin_files) {
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.)
// LANG = client language (j, e, g, f, s)
// EXT = file type (bin, bind, bin.dlq, qst, etc.)
// EXT has already been stripped off by the time we get here, so we just
// parse the remaining fields.
// EXT has already been stripped off by the time we get here, so we just parse the remaining fields.
string quest_number_token, version_token, language_token;
{
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);
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
// bin file; if not found, look for them without the language suffix
// Find the corresponding dat and pvr files with the same basename as the bin file; if not found, look for them
// without the language suffix
const DATFileData* dat_filedata = nullptr;
const FileData* pvr_filedata = nullptr;
try {
@@ -672,8 +651,7 @@ QuestIndex::QuestIndex(const string& directory, shared_ptr<const QuestCategoryIn
try {
pvr_filedata = &pvr_files.at(quest_number_token + "-" + version_token);
} catch (const out_of_range&) {
// pvr files aren't required (and most quests do not have them), so
// don't fail if it's missing
// pvr files aren't required (and most quests do not have them), so don't fail if it's missing
}
}
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(
QuestMenuType menu_type,
Episode episode,
uint16_t version_flags,
IncludeCondition include_condition) const {
QuestMenuType menu_type, Episode episode, uint16_t version_flags, IncludeCondition include_condition) const {
vector<shared_ptr<const QuestCategoryIndex::Category>> ret;
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()) {
@@ -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) {
// Download quest files are like normal (PRS-compressed) quest files, but they
// are encrypted with PSO V2 encryption (even on V3 / PSO GC), and a small
// header (PSODownloadQuestHeader) is prepended to the encrypted data.
// Download quest files are like normal (PRS-compressed) quest files, but they are encrypted with PSO V2 encryption
// (even on V3 / PSO GC), and a small header (PSODownloadQuestHeader) is prepended to the encrypted data.
if (encryption_seed == 0) {
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;
data += compressed_data;
// Add temporary extra bytes if necessary so encryption won't fail - the data
// size must be a multiple of 4 for PSO V2 encryption.
// Add extra bytes if necessary so encryption won't fail; the data size must be a multiple of 4 for PSO V2 encryption
size_t original_size = data.size();
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 {
// The download flag needs to be set in the bin header, or else the client
// will ignore it when scanning for download quests in an offline game. To set
// this flag, we need to decompress the quest's .bin file, set the flag, then
// The download flag needs to be set in the bin header, or else the client will ignore it when scanning for download
// quests in an offline game. To set this flag, we need to decompress the quest's .bin file, set the flag, then
// recompress it again.
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);
// Return a new VersionedQuest object with appropriately-processed .bin and
// .dat file contents
// Return a new VersionedQuest object with appropriately-processed .bin and .dat file contents
auto dlq = make_shared<VersionedQuest>(*this);
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));
@@ -944,20 +915,15 @@ shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(Language overri
return dlq;
}
string decode_gci_data(
const string& data,
ssize_t find_seed_num_threads,
int64_t known_seed,
bool skip_checksum) {
string decode_gci_data(const string& data, ssize_t find_seed_num_threads, int64_t known_seed, bool skip_checksum) {
phosg::StringReader r(data);
const auto& header = r.get<PSOGCIFileHeader>();
header.check();
if (header.is_ep12()) {
const auto& dlq_header = r.get<PSOGCIDLQFileEncryptedHeader>(false);
// Unencrypted GCI files appear to always have zeroes in these fields.
// Encrypted GCI files are highly unlikely to have zeroes in ALL of these
// fields, so assume it's encrypted if any of them are nonzero.
// Unencrypted GCI files appear to always have zeroes in these fields. Encrypted GCI files are highly unlikely to
// have zeroes in ALL of these 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 (known_seed >= 0) {
return decrypt_download_quest_data_section<true>(
@@ -1010,14 +976,13 @@ string decode_gci_data(
}
} else {
// The first 0x10 bytes in the data segment appear to be unused. In most
// files I've seen, the last half of it (8 bytes) are duplicates of the
// first 8 bytes of the unscrambled, compressed data, though this is the
// result of an uninitialized memory bug when the client encodes the file
// and not an actual constraint on what should be in these 8 bytes.
// The first 0x10 bytes in the data segment appear to be unused. In most files I've seen, the last half of it (8
// bytes) are duplicates of the first 8 bytes of the unscrambled, compressed data, though this is the result of
// an uninitialized memory bug when the client encodes the file and not an actual constraint on what should be in
// these 8 bytes.
r.skip(16);
// The game treats this field as a 16-byte string (including the \0). The 8
// bytes after it appear to be completely unused.
// The game treats this field as a 16-byte string (including the \0). The 8 bytes after it appear to be
// completely unused.
if (r.readx(15) != "SONICTEAM,SEGA.") {
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);
// For some reason, Sega decided not to encrypt Episode 3 quest files in the
// same way as Episodes 1&2 quest files (see above). Instead, they just
// wrote a fairly trivial XOR loop over the first 0x100 bytes, leaving the
// For some reason, Sega decided not to encrypt Episode 3 quest files in the same way as Episodes 1&2 quest files
// (see above). Instead, they just wrote a fairly trivial XOR loop over the first 0x100 bytes, leaving the
// remaining bytes completely unencrypted (but still compressed).
size_t unscramble_size = min<size_t>(0x100, data.size());
decrypt_trivial_gci_data(decrypted.data(), unscramble_size, 0);
@@ -1046,11 +1010,7 @@ string decode_gci_data(
}
}
string decode_vms_data(
const string& data,
ssize_t find_seed_num_threads,
int64_t known_seed,
bool skip_checksum) {
string decode_vms_data(const string& data, ssize_t find_seed_num_threads, int64_t known_seed, bool skip_checksum) {
phosg::StringReader r(data);
const auto& header = r.get<PSOVMSFileHeader>();
if (!header.checksum_correct()) {
@@ -1065,8 +1025,7 @@ string decode_vms_data(
}
if (known_seed >= 0) {
return decrypt_download_quest_data_section<false>(
data_section, header.data_size, known_seed);
return decrypt_download_quest_data_section<false>(data_section, header.data_size, known_seed);
} else {
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 key = r.get_u32l();
// The compressed data size does not need to be a multiple of 4, but the V2
// encryption (which is used for all download quests, even in V3) requires the
// data size to be a multiple of 4. We'll just temporarily stick a few bytes
// on the end, then throw them away later if needed.
// The compressed data size does not need to be a multiple of 4, but the V2 encryption (which is used for all
// download quests, even in V3) requires the data size to be a multiple of 4. We'll just temporarily stick a few
// bytes on the end, then throw them away later if needed.
string decrypted = r.read(r.remaining());
PSOV2Encryption encr(key);
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) {
// We have to allow larger commands here, because it seems some tools
// encoded QST files with BB's extra 4 padding bytes included in the
// command size.
// We have to allow larger commands here, because it seems some tools encoded QST files with BB's extra 4 padding
// bytes included in the command size.
if (header.size < sizeof(HeaderT) + sizeof(S_WriteFile_13_A7)) {
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) {
// QST files start with an open file command, but the format differs depending
// on the PSO version that the qst file is for. We can detect the format from
// the first 4 bytes in the file:
// QST files start with an open file command, but the format differs depending on the PSO version that the qst file
// is for. We can detect the format from the first 4 bytes in the file:
// - BB: 58 00 44 00 or 58 00 A6 00
// - PC: 3C 00 44 ?? or 3C 00 A6 ??
// - 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)) {
return decode_qst_data_t<PSOCommandHeaderPC, S_OpenFile_PC_GC_44_A6>(data);
} 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
// the filename was shifted over by one byte. To detect this, we check if
// the V3 type field has a reasonable value, and if not, we assume the file
// is for PSO DC.
// In PSO DC, the type field is only one byte, but in V3 it's two bytes and the filename was shifted over by one
// byte. To detect this, we check if the V3 type field has a reasonable value, and if not, we assume the file is
// for PSO DC.
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);
} else {
@@ -1249,8 +1204,7 @@ void add_open_file_command_t(
cmd.filename.encode(filename);
cmd.type = 0;
cmd.file_size = file_size;
// TODO: It'd be nice to have something like w.emplace(...) to avoid copying
// the command structs into the StringWriter.
// TODO: It'd be nice to have something like w.emplace(...) to avoid copying the structs into the StringWriter.
w.put(cmd);
}
@@ -1289,9 +1243,8 @@ void add_write_file_commands_t(
memcpy(cmd.data.data(), &data[z], chunk_size);
cmd.data_size = chunk_size;
w.put(cmd);
// On BB, the write file command size is a multiple of 4 but not a multiple
// of 8; in QST format the implicit extra 4 bytes are apparently stored in
// the file.
// On BB, the write file command size is a multiple of 4 but not a multiple of 8; in QST format the implicit extra
// 4 bytes are apparently stored in the file.
if (bb_alignment) {
w.put_u32(0);
}
@@ -1307,15 +1260,15 @@ string encode_qst_file(
bool is_dlq_encoded) {
phosg::StringWriter w;
// Some tools expect both open file commands at the beginning, hence this
// unfortunate abstraction-breaking.
// Some tools expect both open file commands at the beginning, hence this unfortunate abstraction-breaking.
switch (version) {
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_V1:
case Version::DC_V2:
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) {
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_V2:
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) {
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:
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) {
add_write_file_commands_t<PSOCommandHeaderDCV3>(w, it.first, *it.second, is_dlq_encoded, false);
@@ -1343,7 +1298,8 @@ string encode_qst_file(
break;
case Version::XB_V3:
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) {
add_write_file_commands_t<PSOCommandHeaderDCV3>(w, it.first, *it.second, is_dlq_encoded, false);
@@ -1351,7 +1307,8 @@ string encode_qst_file(
break;
case Version::BB_V4:
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) {
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 {
QuestMetadata meta;
// Most of these default values are intentionally invalid; we use these
// values to check if each field was parsed during quest indexing.
// Most of these default values are intentionally invalid; we use these values to check if each field was parsed
// during quest indexing.
std::shared_ptr<const std::string> bin_contents;
std::shared_ptr<const std::string> dat_contents;
std::shared_ptr<const MapFile> map_file;
@@ -151,20 +151,12 @@ struct QuestIndex {
};
std::string encode_download_quest_data(
const std::string& compressed_data,
size_t decompressed_size = 0,
uint32_t encryption_seed = 0);
const std::string& compressed_data, size_t decompressed_size = 0, uint32_t encryption_seed = 0);
std::string decode_gci_data(
const std::string& data,
ssize_t find_seed_num_threads = -1,
int64_t known_seed = -1,
bool skip_checksum = false);
const std::string& data, ssize_t find_seed_num_threads = -1, int64_t known_seed = -1, bool skip_checksum = false);
std::string decode_vms_data(
const std::string& data,
ssize_t find_seed_num_threads = -1,
int64_t known_seed = -1,
bool skip_checksum = false);
const std::string& data, 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::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 {
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);
}
@@ -82,8 +83,7 @@ void QuestMetadata::assign_default_floors() {
void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
if (this->quest_number != other.quest_number) {
throw logic_error(std::format(
"incorrect versioned quest number (existing: {:08X}, new: {:08X})",
this->quest_number, other.quest_number));
"incorrect versioned quest number (existing: {:08X}, new: {:08X})", this->quest_number, other.quest_number));
}
if (this->category_id != other.category_id) {
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) {
throw runtime_error(std::format(
"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) {
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);
if ((this_has_player_limit || other_has_player_limit) && (this->max_players != other.max_players)) {
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) {
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) {
throw runtime_error(std::format("quest version has a different set of solo unlock flags"));
}
if (!this->create_item_mask_entries.empty() &&
!other.create_item_mask_entries.empty() &&
if (!this->create_item_mask_entries.empty() && !other.create_item_mask_entries.empty() &&
this->create_item_mask_entries != other.create_item_mask_entries) {
string this_str, other_str;
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 new_str = other.battle_rules->json().serialize();
throw runtime_error(std::format(
"quest version has different battle rules (existing: {}, new: {})",
existing_str, new_str));
"quest version has different battle rules (existing: {}, new: {})", existing_str, new_str));
}
if (this->challenge_template_index != other.challenge_template_index) {
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];
if (this_fa.area != other_fa.area) {
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) {
@@ -190,8 +191,7 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
string existing_str = this->available_expression->str();
string new_str = other.available_expression->str();
throw runtime_error(std::format(
"quest version has a different available expression (existing: {}, new: {})",
existing_str, new_str));
"quest version has a different available expression (existing: {}, new: {})", existing_str, new_str));
}
if (!this->enabled_expression != !other.enabled_expression) {
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 new_str = other.enabled_expression->str();
throw runtime_error(std::format(
"quest version has a different enabled expression (existing: {}, new: {})",
existing_str, new_str));
"quest version has a different enabled expression (existing: {}, new: {})", existing_str, new_str));
}
if (this->common_item_set_name != other.common_item_set_name) {
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)));
}
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"));
}
}
@@ -239,7 +239,8 @@ phosg::JSON QuestMetadata::json() const {
auto difficulty = static_cast<Difficulty>((key >> 24) & 3);
auto floor = static_cast<uint8_t>((key >> 16) & 0xFF);
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);
}
+3 -4
View File
@@ -17,10 +17,9 @@
#include "StaticGameData.hh"
struct QuestMetadata {
// This structure contains configuration that should be the same across all
// versions of the quest, except for the name and description strings. This
// is used in both the Quest and VersionedQuest structures; in Quest, the
// name and description are used only internally.
// This structure contains configuration that should be the same across all versions of the quest, except for the
// name and description strings. This is used in both the Quest and VersionedQuest structures; in Quest, the name and
// description are used only internally.
Version version;
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;
/* 0010 */ Language language = Language::JAPANESE;
/* 0011 */ uint8_t unknown_a3 = 0;
// Note: The GC client byteswaps this field, then loads it as a byte, so
// technically the high byte of this is what the client uses as the quest
// number. In practice, this only matters if the quest runs send_statistic
// without running prepare_statistic first, which is not the intended usage.
// Note: The GC client byteswaps this field, then loads it as a byte, so technically the high byte of this is what
// the client uses as the quest number. In practice, this only matters if the quest runs send_statistic without
// running prepare_statistic first, which is not the intended usage.
/* 0012 */ le_uint16_t quest_number = 0;
/* 0014 */ pstring<TextEncoding::MARKED, 0x20> name;
/* 0034 */ pstring<TextEncoding::MARKED, 0x80> short_description;
@@ -95,8 +94,7 @@ struct CreateItemMaskEntry {
operator QuestMetadata::CreateItemMask() const;
} __packed_ws__(CreateItemMaskEntry, 0x38);
// Some quest authoring tools don't generate the full quest header, hence the
// split structure here.
// Some quest authoring tools don't generate the full quest header, hence the split structure here.
struct PSOQuestHeaderBBBase {
/* 0000 */ le_uint32_t text_offset = 0;
/* 0004 */ le_uint32_t label_table_offset = 0;
@@ -117,8 +115,8 @@ struct PSOQuestHeaderBBBase {
struct PSOQuestHeaderBB : PSOQuestHeaderBBBase {
struct FloorAssignment {
// These fields match the bb_map_designate arguments (see QuestScript.cc).
// Unused AreaAssignment structures should have all fields set to 0xFF.
// These fields match the bb_map_designate arguments (see QuestScript.cc). Unused AreaAssignment structures should
// have all fields set to 0xFF.
uint8_t floor = 0xFF;
uint8_t area = 0xFF;
uint8_t type = 0xFF;
+8 -13
View File
@@ -32,10 +32,9 @@ uint32_t RareItemSet::expand_rate(uint8_t pc) {
// pc = bits SSSSSVVV
// shift = S - 4 (so shift is 0-27)
// value = V + 7 (so value is 7-14)
// Then, take the value 0x00000002, shift it left by shift (0-27), and
// multiply the result by value (7-14) to get the actual drop rate. The result
// is a probability out of 0xFFFFFFFF (so 0x40000000 means the item will drop
// 25% of the time, for example).
// Then, take the value 0x00000002, shift it left by shift (0-27), and multiply the result by value (7-14) to get the
// actual drop rate. The result is a probability out of 0xFFFFFFFF (so 0x40000000 means the item will drop 25% of the
// time, for example).
int8_t shift = ((pc >> 3) & 0x1F) - 4;
if (shift < 0) {
shift = 0;
@@ -44,8 +43,7 @@ uint32_t RareItemSet::expand_rate(uint8_t pc) {
}
uint8_t RareItemSet::compress_rate(uint32_t probability) {
// I'm too lazy to figure out the reverse bitwise math, so we just compute all
// the expansions and take the closest one
// I'm too lazy to figure out the reverse math, so we just compute all the expansions and take the closest one
static std::map<uint32_t, uint8_t> inverse_map;
if (inverse_map.empty()) {
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);
ParsedRELData rel(gsl.get_reader(filename), is_big_endian, false);
this->collections.emplace(
this->key_for_params(mode, episode, difficulty, section_id),
rel.as_collection());
this->key_for_params(mode, episode, difficulty, section_id), rel.as_collection());
} 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;
ParsedRELData rel(r.sub(0x280 * index, 0x280), is_big_endian, false);
this->collections.emplace(
this->key_for_params(GameMode::NORMAL, episode, difficulty, section_id),
rel.as_collection());
this->key_for_params(GameMode::NORMAL, episode, difficulty, section_id), rel.as_collection());
} catch (const out_of_range&) {
}
}
@@ -715,9 +711,8 @@ string RareItemSet::serialize_html(
ItemData example_item = spec.data;
if (example_item.can_be_encoded_in_rel_rare_table()) {
// Apparently Return to Ragol has a patch that allows it to use the
// value 5 in data1[0] to specify a specific tech disk, so we handle
// that here.
// Apparently Return to Ragol has a patch that allows it to use the value 5 in data1[0] to specify a specific
// tech disk, so we handle that here.
if (example_item.data1[0] == 5) {
example_item.data1[4] = example_item.data1[1];
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;
// The functions in this file are called when a client sends a game command (60, 62, 6C, 6D, C9, or CB).
struct SubcommandMessage {
uint16_t command;
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) {
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
// battle, forward everything to watcher lobbies. (This is necessary because
// if we forward everything before battle, the blocking menu subcommands
// cause the battle setup menu to appear in the spectator room, which looks
// weird and is generally undesirable.)
// Before battle, forward only chat commands to watcher lobbies; during battle, forward everything to watcher
// lobbies. (This is necessary because if we forward everything before battle, the blocking menu subcommands 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)) ||
(def_flags & SDF::ALWAYS_FORWARD_TO_WATCHERS)) {
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>
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
// need to stay in sync)
// I'm lazy and this should never happen for item commands (since all players need to stay in sync)
if (command_is_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)>
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
// need to stay in sync)
// I'm lazy and this should never happen for item commands (since all players need to stay in sync)
if (command_is_private(msg.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>
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
// need to stay in sync)
// I'm lazy and this should never happen for item commands (since all players need to stay in sync)
if (command_is_private(msg.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;
}
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)) {
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.
// TODO: We should do this by checking if the supermaps are the same instead
// of hardcoding this here.
// TODO: We should do this by checking if the supermaps are the same instead of hardcoding this here.
auto collapse_version = +[](Version v) -> Version {
// Collapse DC v1/v2 and PC into PC_V2
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
// to see items that the joining player should not see. We always generate
// a new item state for the joining player instead.
// The leader's item state is never forwarded since the leader may be able to see items that the joining player
// should not see. We always generate a new item state for the joining player instead.
send_game_item_state(target);
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));
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>(
r.where() + dec_header.entity_set_flags_size,
event_set_flags_count * sizeof(le_uint16_t));
r.where() + dec_header.entity_set_flags_size, event_set_flags_count * sizeof(le_uint16_t));
l->map_state->import_flag_states_from_sync(
c->version(),
object_set_flags,
@@ -685,19 +664,17 @@ static asio::awaitable<void> on_sync_joining_player_compressed_state(shared_ptr<
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);
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
// causes this array to be too small. It looks like Sega forgot to
// account for the header (G_SyncSetFlagState_6x6E_Decompressed)
// before compressing the buffer, so the game cuts off the last 8
// bytes of the switch flags. Since this only affects the last floor,
// 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.
// There is a bug in most (perhaps all) versions of the game, which causes this array to be too small. It
// looks like Sega forgot to account for the header (G_SyncSetFlagState_6x6E_Decompressed) before compressing
// the buffer, so the game cuts off the last 8 bytes of the switch flags. Since this only affects the last
// floor, 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++) {
uint8_t& l_flags = l->switch_flags->array(floor).data[z];
uint8_t r_flags = switch_flags_r.get_u8();
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);
l_flags = r_flags;
}
@@ -942,7 +919,11 @@ G_SyncPlayerDispAndInventory_DCNTE_6x70 Parsed6x70Data::as_dc_nte(shared_ptr<Ser
ret.items = this->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);
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;
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);
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) {
// The format of game_flags for players was changed significantly between v2
// and v3, and not accounting for this results in odd effects like other
// characters not appearing when joining a game. Unfortunately, some bits
// were deleted on v3 and other bits were added, so it doesn't suffice to
// simply store the most complete format of this field - we have to be able
// to convert between the two.
// The format of game_flags for players was changed significantly between v2 and v3, and not accounting for this
// results in odd effects like other characters not appearing when joining a game. Unfortunately, some bits were
// deleted on v3 and other bits were added, so it doesn't suffice to 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 v3: JIHGFEDC BAzyxwvu srqponkj hgfedcba
// The bits ilmt were removed in v3 and the bits to their left were shifted
// right. The bits DEFG were added in v3 and do not exist on v2.
// Known meanings for these bits:
// The bits ilmt were removed in v3 and the bits to their left were shifted right. The bits DEFG were added in v3 and
// do not exist on v2. Known meanings for these bits so far:
// o = is dead
// n = should play hit animation
// 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) {
auto s = c->require_server_state();
// In V1/V2 games, this command sometimes is sent after the new client has
// finished loading, so we don't check l->any_client_loading() here.
// In V1/V2 games, this command sometimes is sent after the new client has finished loading, so we don't check
// l->any_client_loading() here.
auto target = get_sync_target(c, msg.command, msg.flag, true);
if (!target) {
co_return;
}
// 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 tell the target all state has been
// sent. (If both are pre-V1, the target won't expect this command; if both
// are V1 or later, the leader will send this command itself.)
// 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
// tell the target all state has been sent. (If both are pre-V1, the target won't expect this command; if both are V1
// or later, the leader will send this command itself.)
Version target_v = target->version();
Version c_v = c->version();
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;
}
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Ep3 subcommands
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) {
set_mask_for_ep3_game_command(msg.data, msg.size, 0);
} else {
// Ep3 NTE sends uninitialized data in this field; clear it so we know the
// command isn't masked
// Ep3 NTE sends uninitialized data in this field; clear it so we know the command isn't masked
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);
}
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Chat commands and the like
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;
}
case Version::BB_V4:
// Nothing to do... the command is blank; the server generates the guild
// card to be sent
// Nothing to do... the command is blank; the server generates the guild card to be sent
break;
default:
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) {
if (is_pre_v1(c->version())) {
// The Word Select command is a different size in final vs. NTE and
// proto, so handle that here by appending FFFFFFFF0000000000000000
// The Word Select command is a different size in final vs. NTE and proto, so handle that here by appending
// FFFFFFFF0000000000000000
string effective_data(reinterpret_cast<const char*>(msg.data), msg.size);
effective_data.resize(0x20, 0x00);
effective_data[0x01] = 0x08;
@@ -1596,13 +1575,12 @@ static asio::awaitable<void> on_set_player_visible(shared_ptr<Client> c, Subcomm
co_return;
}
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static asio::awaitable<void> on_change_floor_6x1F(shared_ptr<Client> c, SubcommandMessage& msg) {
if (is_pre_v1(c->version())) {
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
// the loading flag here instead.
// DC NTE and 11/2000 don't send 6F when they're done loading, so we clear the loading flag here instead.
if (c->check_flag(Client::Flag::LOADING)) {
c->clear_flag(Client::Flag::LOADING);
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))
? static_cast<const void*>(&v3_cmd)
: static_cast<const void*>(&v2_cmd);
// TODO: We might need to send different versions of the command here to
// different clients in certain crossplay scenarios, so just using
// echo_to_lobby would not suffice. Figure out a way to handle this.
// TODO: We might need to send different versions of the command here to different clients in certain crossplay
// scenarios, so just using 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);
}
}
@@ -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);
}
// Apparently sometimes 6x05 is sent with an invalid switch flag number. The
// client seems to just ignore the command in that case, so we go ahead and
// forward it (in case the client's object update function is meaningful
// somehow) and just don't update our view of the switch flags.
// Apparently sometimes 6x05 is sent with an invalid switch flag number. The client seems to just ignore the command
// in that case, so we go ahead and forward it (in case the client's object update function is meaningful somehow)
// and just don't update our view of the switch flags.
if (l->switch_flags && (cmd.switch_flag_num < 0x100)) {
if (cmd.flags & 1) {
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) {
const auto& cmd = msg.check_size_t<G_PlaySoundFromPlayer_6xB2>();
// This command can be used to play arbitrary sounds, but the client only
// ever sends it for the camera shutter sound, so we only allow that one.
// This command can be used to play arbitrary sounds, but the client only ever sends it for the camera shutter sound,
// so we only allow that one.
if (cmd.sound_id == 0x00051720) {
forward_subcommand(c, msg);
}
co_return;
}
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename CmdT>
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;
}
// The animation numbers were changed on V3. This is the most common one to
// see in the lobby (it occurs when a player talks to the counter), so we
// take care to translate it specifically.
// The animation numbers were changed on V3. This is the most common one to see in the lobby (it occurs when a player
// talks to the counter), so we take care to translate it specifically.
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)))) {
forward_subcommand(c, msg);
@@ -1926,7 +1901,7 @@ static asio::awaitable<void> on_set_animation_state(shared_ptr<Client> c, Subcom
}
}
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Item commands
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());
l->on_item_id_generated_externally(item.id);
// Players cannot send this on behalf of another player, but they can send it
// on behalf of an NPC; we don't track items for NPCs so in that case we just
// 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.
// 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.
// Players cannot send this on behalf of another player, but they can send it on behalf of an NPC; we don't track
// items for NPCs so in that case we just 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.
// 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 that.
auto s = c->require_server_state();
if (cmd.header.client_id != c->lobby_client_id) {
// 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?
// We don't delete anything from the inventory here; the client will send a
// 6x29 to do so immediately following this command.
// We don't delete anything from the inventory here; the client will send a 6x29 to do so following this command.
ItemData item = cmd.item_data;
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());
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
// needs a new ID. remove_item signals this by returning an item with an ID
// of 0xFFFFFFFF.
// If a stack was split, the original item still exists, so the dropped item needs a new ID. remove_item signals this
// by returning an item with an ID of 0xFFFFFFFF.
if (item.id == 0xFFFFFFFF) {
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
// the item back to the player's inventory to correct for this (it will get
// removed again by the 6x29 handler)
// PSOBB sends a 6x29 command after it receives the 6x5D, so we need to add the item back to the player's inventory
// to correct for this (it will get removed again by the 6x29 handler)
p->add_item(item, limits);
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>
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
// need to stay in sync)
// I'm lazy and this should never happen for item commands (since all players need to stay in sync)
if (command_is_private(msg.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)) {
// This can happen if the network is slow, and the client tries to pick up
// the same item multiple times. Or multiple clients could try to pick up
// the same item at approximately the same time; only one should get it.
// This can happen if the network is slow, and the client tries to pick up the same item multiple times. Or
// multiple clients could try to pick up 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);
} else {
// This is handled by the server on BB, and by the leader on other versions.
// However, the client's logic is to simply always send a 6x59 command when
// it receives a 6x5A and the floor item exists, so we just implement that
// This is handled by the server on BB, and by the leader on other versions. However, the client's logic is to
// simply always send a 6x59 command when it receives a 6x5A and the floor item exists, so we just implement that
// logic here instead of forwarding the 6x5A to the leader.
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);
string mag_name, fed_name;
{
// Note: We do this weird scoping thing because player_feed_mag will
// likely delete the items, which will break the references here.
// Note: We downscope these because player_feed_mag will likely delete the items, which will break these references
const auto& fed_item = p->inventory.items[fed_index].data;
fed_name = s->describe_item(c->version(), fed_item);
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);
// On BB, the player only sends a 6x28; on other versions, the player sends
// a 6x29 immediately after to destroy the fed item. So on BB, we should
// remove the fed item here, but on other versions, we allow the following
// 6x29 command to do that.
// On BB, the player only sends a 6x28; on other versions, the player sends a 6x29 immediately after to destroy the
// fed item. So on BB, we should remove the fed item here, but on other versions, we allow the following 6x29 command
// to do that.
if (c->version() == Version::BB_V4) {
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;
}
// Command should not be forwarded across the GC NTE boundary, but may be
// forwarded to other clients within that boundary
// Command should not be forwarded across the GC NTE boundary, but may be forwarded to other clients within that
// boundary
bool c_is_nte = (c->version() == Version::GC_NTE);
auto l = c->require_lobby();
@@ -2555,8 +2519,7 @@ bool validate_6xBB(G_SyncCardTradeServerState_Ep3_6xBB& cmd) {
return false;
}
// TTradeCardServer uses 4 to indicate the slot is empty, so we allow 4 in
// the client ID checks below
// TTradeCardServer uses 4 to indicate the slot is empty, so we allow 4 in the client ID checks below
switch (cmd.what) {
case 1:
if (cmd.args[0] >= 5) {
@@ -2684,10 +2647,9 @@ static asio::awaitable<void> on_ep3_private_word_select_bb_bank_action(
} else { // Deposit item
const auto& limits = *s->item_stack_limits(c->version());
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
// inventory item. This is annoying but doesn't cause any problems
// because we always generate a new item ID when withdrawing from the
// bank, so there's no chance of conflict later.
// If a stack was split, the bank item retains the same item ID as the inventory item. This is annoying but
// doesn't cause any problems because we always generate a new item ID when withdrawing from the bank, so
// there's no chance of conflict later.
if (item.id == 0xFFFFFFFF) {
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>();
auto p = c->character_file();
// Make sure the set of item IDs passed in by the client exactly matches the
// set of item IDs present in the inventory
// Make sure the set of item IDs passed in by the client exactly matches the set of item IDs present in the inventory
unordered_set<uint32_t> sorted_item_ids;
size_t expected_count = 0;
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];
}
}
// It's annoying that extension data is stored in the inventory items array,
// because we have to be careful to avoid sorting it here too.
// It's annoying that extension data is stored in the inventory items array, because we have to be careful to avoid
// sorting it here too.
for (size_t x = 0; x < 30; x++) {
sorted[x].extension_data1 = p->inventory.items[x].extension_data1;
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) {
// There is a GCC bug that causes this function to not compile properly
// unless the sorting implementation is in a separate function. I think it's
// something to do with how it allocates the coroutine's locals, but it's
// enough to avoid for now.
// There is a GCC bug that causes this function to not compile properly unless the sorting implementation is in a
// separate function. I think it's something to do with how it allocates the coroutine's locals, but it's enough to
// avoid for now.
on_sort_inventory_bb_inner(c, msg);
co_return;
}
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// EXP/Drop Item commands
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);
}
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
// parameters from the client on those versions
// V1/V2 don't have 6xA2, so we can't get ignore_def or the object parameters from the client on those versions
cmd.param3 = set_entry->param3;
cmd.param4 = set_entry->param4;
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 {})",
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;
// rt_indexes in Episode 4 don't match those sent in the command; we just
// ignore what the client sends.
// rt_indexes in Episode 4 don't match those sent in the command; we just ignore what the client sends.
if ((area < 0x24) && (cmd.rt_index != res.effective_rt_index)) {
// Special cases: BULCLAW => BULK and DARK_GUNNER => DEATH_GUNNER
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;
}
// Note: We always call reconcile_drop_request_with_map, even in client drop
// mode, so that we can correctly mark enemies and objects as having dropped
// their items in persistent games.
// Note: We always call reconcile_drop_request_with_map, even in client drop mode, so that we can correctly mark
// enemies and objects as having dropped their items in persistent games.
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);
@@ -2957,9 +2914,8 @@ static asio::awaitable<void> on_entity_drop_item_request(shared_ptr<Client> c, S
co_return;
case ServerDropMode::CLIENT: {
// If the leader is BB, use SERVER_SHARED instead
// TODO: We should also use server drops if any clients have incompatible
// object lists, since they might generate incorrect IDs for items and we
// can't override them
// TODO: We should also use server drops if any clients have incompatible object lists, since they might generate
// incorrect IDs for items and we can't override them
auto leader = l->clients[l->leader_id];
if (leader && leader->version() == Version::BB_V4) {
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);
return l->item_creator->on_box_item_drop(cmd.effective_area);
} else {
l->log.info_f("Creating item from box {:04X} => K-{:03X} (area {:02X}; specialized with {:g} {:08X} {:08X} {:08X})",
cmd.entity_index, rec.obj_st->k_id, cmd.effective_area,
cmd.param3, cmd.param4, cmd.param5, cmd.param6);
l->log.info_f(
"Creating item from box {:04X} => K-{:03X} (area {:02X}; specialized with {:g} {:08X} {:08X} {:08X})",
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(
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");
case ServerDropMode::SERVER_SHARED:
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();
if (res.item.empty()) {
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());
}
// The client explicitly checks action for both 0 and 1 - any other value
// means no operation is performed.
// The client explicitly checks action for both 0 and 1 - any other value means no operation is performed.
if ((flag_num >= 0x400) || (static_cast<size_t>(difficulty) > 3) || (action > 1)) {
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;
uint8_t area = l->area_for_floor(c->version(), c->floor);
if (area == 0x0E) {
// On Normal, Dark Falz does not have a third phase, so send the drop
// request after the end of the second phase. On all other difficulty
// levels, send it after the third phase.
// On Normal, Dark Falz does not have a third phase, so send the drop request after the end of the second phase.
// On all other difficulty levels, send it after the third phase.
if ((difficulty == Difficulty::NORMAL) && (flag_num == 0x0035)) {
boss_enemy_type = EnemyType::DARK_FALZ_2;
} 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");
}
// If the lock status register is being written, change the game's flags to
// allow or forbid joining
// If the lock status register is being written, change the game's flags to allow or forbid joining
if (l->quest &&
l->quest->meta.joinable &&
(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;
if (l->quest->meta.enable_schtserv_commands) {
// We currently only implement one Schtserv server command here. There
// are likely many more which we don't support.
// We currently only implement one Schtserv server command here. There are likely many more which we don't support.
if (cmd.register_number == 0xF0) {
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)) {
// When all enemies in a wave event have (set_flags & 8), which means
// they are defeated, set event_flags = (event_flags | 0x18) & (~4),
// which means it is done and should not trigger
// When all enemies in a wave event have (set_flags & 8), which means they are defeated, set event_flags =
// (event_flags | 0x18) & (~4), which means it is done and should not trigger again
bool all_enemies_defeated = true;
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)) {
@@ -3370,16 +3319,14 @@ static asio::awaitable<void> on_set_entity_set_flag(shared_ptr<Client> c, Subcom
}
case 0x0A: // set_switch_flag
case 0x0B: { // clear_switch_flag
// These opcodes cause the client to send 6x05 commands, so
// we don't have to do anything here.
// These opcodes cause the client to send 6x05 commands, so we don't have to do anything here.
uint16_t switch_flag_num = actions_r.get_u16l();
l->log.info_f("(W-{:03X} script) {}able_switch_flag {:04X}",
ev_st->w_id, (opcode & 1) ? "dis" : "en", switch_flag_num);
break;
}
case 0x0C: { // trigger_event
// This opcode causes the client to send a 6x67 command, so
// we don't have to do anything here.
// This opcode causes the client to send a 6x67 command, so we don't have to do anything here.
uint32_t event_id = actions_r.get_u32l();
l->log.info_f("(W-{:03X} script) trigger_event {:08X}", ev_st->w_id, event_id);
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_lobby_id = l->lobby_id;
// See the comments in G_SetTelepipeState_6x68 in CommandsFormats.hh for
// why we have to do this
// See the comments in G_SetTelepipeState_6x68 in CommandsFormats.hh for why we have to do this
if (is_big_endian(c->version())) {
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);
}
// TODO: It'd be nice if this worked on bosses too, but it seems we have to
// use each boss' specific state-syncing command, or the cutscenes misbehave.
// Just setting flag 0x800 does work on Vol Opt (and the various parts), but
// doesn't work on other Episode 1 bosses. Other episodes are not yet tested.
// TODO: It'd be nice if this worked on bosses too, but it seems we have to use each boss' specific state-syncing
// command, or the cutscenes misbehave. 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.
bool is_fast_kill = c->check_flag(Client::Flag::FAST_KILLS_ENABLED) &&
!type_definition_for_enemy(ene_st->super_ene->type).is_boss() &&
!(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;
}
// 6x17 is used to transport players to the other part of the Vol Opt boss
// arena, so phase 2 can begin. We only allow 6x17 in the Monitor Room (Vol
// Opt arena).
// 6x17 is used to transport players to the other part of the Vol Opt boss arena, so phase 2 can begin. We only allow
// 6x17 in the Monitor Room (Vol Opt arena).
if (l->area_for_floor(c->version(), c->floor) != 0x0D) {
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
// command instead of raising; this could have been due to a data race
// If the target is on a different floor or does not exist, just drop the command instead of raising; this could have
// been due to a data race
if (cmd.header.entity_id < 0x1000) {
auto target = l->clients.at(cmd.header.entity_id);
if (!target || target->floor != c->floor) {
@@ -3902,8 +3846,7 @@ static asio::awaitable<void> on_level_up(shared_ptr<Client> c, SubcommandMessage
co_return;
}
// On the DC prototypes, this command doesn't include any stats - it just
// increments the player's level by 1.
// On the DC prototypes, this command doesn't include any stats - it just increments the player's level by 1.
auto p = c->character_file();
if (is_pre_v1(c->version())) {
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
// Ep1 next if in Crater and Ep2 next if in Desert (this mirrors the logic in
// BB Patch Project's omnispawn patch).
// Always try the current episode first. If the current episode is Ep4, try Ep1 next if in Crater and Ep2 next if in
// Desert (this mirrors the logic in BB Patch Project's omnispawn patch).
array<Episode, 3> episode_order;
episode_order[0] = current_episode;
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(
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
// applying the android bonus if so. We don't do anything for those special
// types, so we don't check for that here.
// Note: The original code checks if special.type is 9, 10, or 11, and skips applying the android bonus if so. We
// don't do anything for those special 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 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);
@@ -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;
}
// If the requesting player never hit this enemy, they are probably cheating;
// ignore the command. Also, each player sends a 6xC8 if they ever hit the
// enemy; we only react to the first 6xC8 for each enemy (and give all
// relevant players EXP then, if they deserve it).
// If the requesting player never hit this enemy, they are probably cheating; ignore the command. Also, each player
// sends a 6xC8 if they ever hit the enemy; we only react to the first 6xC8 for each enemy (and give all relevant
// players EXP then, if they deserve it).
if (!ene_st->ever_hit_by_client_id(c->lobby_client_id) ||
(ene_st->server_flags & MapState::EnemyState::Flag::EXP_GIVEN)) {
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 this player killed the enemy, they get full EXP; if they tagged the
// enemy, they get 80% EXP; if auto EXP share is enabled and they are
// close enough to the monster, they get a smaller share; if none of
// these situations apply, they get no EXP. In Battle and Challenge
// modes, if a quest is loaded, EXP share is disabled.
// If this player killed the enemy, they get full EXP; if they tagged the enemy, they get 80% EXP; if auto EXP
// share is enabled and they are close enough to the monster, they get a smaller share; if none of 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)
? 0.0f
: 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) {
// In PSOBB, Sega decided to add a 30% EXP boost for Episode 2. They
// could have done something reasonable, like edit the BattleParamEntry
// files so the monsters would all give more EXP, but they did
// something far lazier instead: they just stuck an if statement in the
// client's EXP request function. We, unfortunately, have to do the
// same thing here.
// In PSOBB, Sega decided to add a 30% EXP boost for Episode 2. They could have done something reasonable, like
// edit the BattleParamEntry files so the monsters would all give more EXP, but they did something far lazier
// instead: they just stuck an if statement in the client's EXP request function. We, unfortunately, have to do
// the same thing here.
uint32_t player_exp = base_exp *
rate_factor *
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
// actually killed the enemy
// Update kill counts on unsealable items, but only for the player who actually killed the enemy
if (ene_st->last_hit_by_client_id(client_id)) {
auto& inventory = lc->character_file()->inventory;
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) {
// We always enforce these restrictions if the quest has any restrictions
// defined, even if the client has cheat mode enabled or has debug enabled.
// If the client can cheat, there are much easier ways to create items (e.g.
// the $item chat command) than spoofing these quest item creation commands,
// so they should just do that instead.
// We always enforce these restrictions if the quest has any restrictions defined, even if the client has cheat mode
// enabled or has debug enabled. If the client can cheat, there are much easier ways to create items (e.g. the $item
// chat command) than spoofing these quest item creation commands, so they should just do that instead.
if (!l->quest) {
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;
item = cmd.item_data;
// enforce_stack_size_limits must come after this assert since quests may
// attempt to create stackable items with a count of zero
// enforce_stack_size_limits must come after this assert since quests may attempt to create stackable items with a
// count of zero
assert_quest_item_create_allowed(l, item);
item.enforce_stack_size_limits(limits);
item.id = l->generate_item_id(c->lobby_client_id);
// The logic for the item_create and item_create2 quest opcodes (B3 and B4)
// includes a precondition check to see if the player can actually add the
// item to their inventory or not, and the entire command is skipped if not.
// However, on BB, the implementation performs this check and sends a 6xCA
// command instead - the item is not immediately added to the inventory, and
// is instead added when the server sends back a 6xBE command. So if a quest
// creates multiple items in quick succession, there may be another 6xCA/6xBE
// 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.
// The logic for the item_create and item_create2 quest opcodes (B3 and B4) includes a precondition check to see if
// the player can actually add the item to their inventory or not, and the entire command is skipped if not. However,
// on BB, the implementation performs this check and sends a 6xCA command instead - the item is not immediately added
// to the inventory, and is instead added when the server sends back a 6xBE command. So if a quest creates multiple
// items in quick succession, there may be another 6xCA/6xBE 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 {
c->character_file()->add_item(item, limits);
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();
}
// To receive an item, the player must be online, using BB, have a character
// loaded (that is, be in a lobby or game), not be at the bank counter at the
// moment, and there must be room in their bank to receive the item.
// To receive an item, the player must be online, using BB, have a character loaded (that is, be in a lobby or game),
// not be at the bank counter at the moment, and there must be room in their bank to receive the item.
bool item_sent = false;
auto target_c = s->find_client(nullptr, cmd.target_guild_card_number);
if (target_c &&
@@ -4339,10 +4267,8 @@ asio::awaitable<void> on_transfer_item_via_mail_message_bb(shared_ptr<Client> c,
}
if (item_sent) {
// See the comment in the 6xCC handler about why we do this. Similar to
// that case, the 6xCB handler on the client side does exactly the same
// thing as 6x29, but 6x29 is backward-compatible with other PSO versions
// and 6xCB is not.
// See the comment in the 6xCC handler about why we do this. Similar to that case, the 6xCB handler on the client
// side does exactly the same thing as 6x29, but 6x29 is backward-compatible with other versions and 6xCB is not.
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)};
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();
}
// The original implementation forwarded the 6xCC command to all other
// clients. However, the handler does exactly the same thing as 6x29 if the
// affected client isn't the local client. Since the sender has already
// processed the 6xCC that they sent by the time we receive this, we pretend
// that they sent 6x29 instead and send that to the others in the game.
// The original implementation forwarded the 6xCC command to all other clients. However, the handler does exactly the
// same thing as 6x29 if the affected client isn't the local client. Since the sender has already processed the 6xCC
// that they sent by the time we receive this, we pretend 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};
SubcommandMessage delete_item_msg{msg.command, msg.flag, &cmd29, sizeof(cmd29)};
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) {
// There are generally two data races that could occur here. Either the
// player attempted to evict the item at the same time the server did (that
// is, the client's and server's 6x63 commands crossed paths on the
// network), or the player attempted to evict an item that was already
// picked up. The former case is easy to handle; we can just ignore the
// command. The latter case is more difficult - we have to know which
// player picked up the item and send a 6x2B command to the sender, to sync
// their item state with the server's again. We can't just look through the
// players' inventories to find the item ID, since item IDs can be
// 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.
// There are generally two data races that could occur here. Either the player attempted to evict the item at the
// same time the server did (that is, the client's and server's 6x63 commands crossed paths on the network), or the
// player attempted to evict an item that was already picked up. The former case is easy to handle; we can just
// ignore the command. The latter case is more difficult - we have to know which player picked up the item and send
// a 6x2B command to the sender, to sync their item state with the server's again. We can't just look through the
// players' inventories to find the item ID, since item IDs can be 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",
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");
}
// It seems the client expects an item ID to be consumed here, even though
// the returned item has the same ID as the original item. Perhaps this was
// not the case on Sega's original server, and the returned item had a new
// ID instead.
// It seems the client expects an item ID to be consumed here, even though the returned item has the same ID as the
// original item. Perhaps this was not the case on Sega's original server, and the returned item had a new ID
// instead.
l->generate_item_id(c->lobby_client_id);
p->disp.stats.meseta -= 100;
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();
}
// If the leader (c) is BB, they are expected to send 02DF later, which
// will recreate the overlays.
// If the leader (c) is BB, they are expected to send 02DF later, which will recreate the overlays.
if (!is_v4(c->version())) {
for (auto lc : l->clients) {
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()];
if (item.data1[0] == 0x04) { // Meseta
// TODO: What is the right amount of Meseta to use here? Presumably it
// should be random within a certain range, but it's not obvious what
// that range should be.
// TODO: What is the right amount of Meseta to use here? Presumably it should be random within a certain range,
// but it's not obvious what that range should be.
item.data2d = 100;
} else if (item.data1[0] == 0x00) {
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());
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);
// TODO: Shouldn't we send a 6x29 here? Check if this causes desync in an
// actual game
// TODO: Shouldn't we send a 6x29 here? Check if this causes desync in an actual game
G_ExchangeItemInQuest_BB_6xDB cmd_6xDB;
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");
}
// The client sends a 6xC9 to remove Meseta before sending 6xE2, so we don't
// have to deal with Meseta here.
// The client sends a 6xC9 to remove Meseta before sending 6xE2, so we don't have to deal with Meseta here.
item.id = l->generate_item_id(c->lobby_client_id);
// 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;
}
// The 6xE3 handler on the client fails if the item already exists, so we
// need to send 6xE3 before we call send_create_inventory_item_to_lobby.
// The 6xE3 handler on the client fails if the item already exists, so we need to send 6xE3 before we call
// send_create_inventory_item_to_lobby.
G_SetMesetaSlotPrizeResult_BB_6xE3 cmd_6xE3 = {
{0xE3, sizeof(G_SetMesetaSlotPrizeResult_BB_6xE3) >> 2, c->lobby_client_id}, item};
send_command_t(c, 0x60, 0x00, cmd_6xE3);
// Add the item to the player's inventory if possible; if not, drop it on the
// floor where the player is standing
// Add the item to the player's inventory if possible; if not, drop it on the floor where the player is standing
bool added_to_inventory;
try {
p->add_item(item, *s->item_stack_limits(c->version()));
added_to_inventory = true;
} catch (const out_of_range&) {
// If the game's drop mode is private or duplicate, make the item visible
// only to this player; in other modes, make it visible to everyone
// If the game's drop mode is private or duplicate, make the item visible only to this player; in other modes, make
// it visible to everyone
uint16_t flags = ((l->drop_mode == ServerDropMode::SERVER_PRIVATE) || (l->drop_mode == ServerDropMode::SERVER_DUPLICATE))
? (1 << c->lobby_client_id)
: 0x000F;
@@ -5474,10 +5390,9 @@ static asio::awaitable<void> on_write_quest_counter_bb(shared_ptr<Client> c, Sub
co_return;
}
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This makes it easier to see which handlers exist on which prototypes via
// syntax highlighting
// This makes it easier to see which handlers exist on which prototypes via syntax highlighting
constexpr uint8_t NONE = 0x00;
const vector<SubcommandDefinition> subcommand_definitions{
+1 -4
View File
@@ -119,10 +119,7 @@ public:
protected:
Parsed6x70Data(
const G_6x70_Base_V1& base,
uint32_t guild_card_number,
Version from_version,
bool from_client_customization);
const G_6x70_Base_V1& base, uint32_t guild_card_number, Version from_version, bool from_client_customization);
G_6x70_Base_V1 base_v1(bool is_v3) const;
static uint32_t convert_game_flags(uint32_t game_flags, bool to_v3);
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 0x91:
case 0x9B: {
auto& mask = check_size_t<S_ServerInitDefault_DC_PC_V3_02_17_91_9B>(
mask_data, mask_size, 0xFFFF);
auto& mask = check_size_t<S_ServerInitDefault_DC_PC_V3_02_17_91_9B>(mask_data, mask_size, 0xFFFF);
mask.server_key = 0;
mask.client_key = 0;
break;
@@ -370,7 +369,8 @@ ReplaySession::ReplaySession(shared_ptr<ServerState> state, FILE* input_log, boo
// ### cc $<chat command>
if (this->clients.size() != 1) {
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;
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);
try {
parsing_command = this->create_event(
from_client ? Event::Type::SEND : Event::Type::RECEIVE,
this->clients.at(client_id),
line_num);
from_client ? Event::Type::SEND : Event::Type::RECEIVE, this->clients.at(client_id), line_num);
num_events++;
} catch (const out_of_range&) {
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();
auto msg = co_await c->channel->recv();
// TODO: Use the iovec form of phosg::print_data here instead of
// prepend_command_header (which copies the string)
// TODO: Use the iovec form of phosg::print_data here instead of prepend_command_header (which copies data)
string full_command = prepend_command_header(
c->version, (c->channel->crypt_in.get() != nullptr), msg.command, msg.flag, msg.data);
this->commands_received++;
@@ -622,8 +619,8 @@ asio::awaitable<void> ReplaySession::run() {
case Version::BB_V4:
if (msg.command == 0x03 || msg.command == 0x9B) {
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.
// Don't just blindly use the first one here.
// TODO: At some point it may matter which BB private key file we use. Don't just blindly use the
// first one here.
c->channel->crypt_in = make_shared<PSOBBEncryption>(
*this->state->bb_private_keys[0], cmd.server_key.data(), cmd.server_key.size());
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;
if (!this->run_failed) {
// Wait a bit longer to ensure that any command sent at the end of the replay
// session don't crash the server
// Wait a bit longer to ensure that any command sent at the end of the replay session don't crash the server
co_await async_sleep(std::chrono::seconds(2));
replay_log.info_f("Replay complete: {} commands sent ({} bytes), {} commands received ({} bytes)",
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,
0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00};
// Originally there was going to be a language-based header for .nsc files, but
// then I decided against it. This string was already in use for that parser,
// so I didn't bother changing it.
// Originally there was going to be a language-based header for .nsc files, but then I decided against it. This string
// was already in use for that parser, so I didn't bother changing it.
const char* LegacySavedAccountDataBB::SIGNATURE = "newserv account file format; 7 sections present; sequential;";
ShuffleTables::ShuffleTables(PSOV2Encryption& crypt) {
@@ -195,14 +194,8 @@ bool PSOGCIFileHeader::is_nte() const {
}
uint32_t compute_psogc_timestamp(
uint16_t year,
uint8_t month,
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};
uint16_t year, uint8_t month, 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;
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),
nullptr);
ret->inventory = src.inventory;
// Note: We intentionally do not call ret->inventory.decode_from_client here.
// This is because the GC client byteswaps data2 in each item before sending
// it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does
// not do this, so the data2 fields are already in the correct order here.
// Note: We intentionally do not call ret->inventory.decode_from_client here. This is because the GC client byteswaps
// data2 in each item before sending it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does not
// do this, so the data2 fields are already in the correct order here.
Language language = ret->inventory.language;
ret->disp = src.disp.to_bb(language, language);
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),
nullptr);
ret->inventory = src.inventory;
// Note: We intentionally do not call ret->inventory.decode_from_client here.
// This is because the GC client byteswaps data2 in each item before sending
// it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does
// not do this, so the data2 fields are already in the correct order here.
// Note: We intentionally do not call ret->inventory.decode_from_client here. This is because the GC client byteswaps
// data2 in each item before sending it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does not
// do this, so the data2 fields are already in the correct order here.
Language language = ret->inventory.language;
ret->disp = src.disp.to_bb(language, language);
ret->validation_flags = src.validation_flags;
@@ -926,8 +917,8 @@ PSODCNTECharacterFile::Character PSOBBCharacterFile::as_dc_nte(uint64_t hardware
PSODCNTECharacterFile::Character ret;
ret.inventory = this->inventory;
// 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 stats in the v2 format
// 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
// stats in the v2 format
ret.inventory.encode_for_client(Version::DC_NTE, nullptr);
ret.disp = this->disp.to_dcpcv3<false>(language, language);
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;
ret.inventory = this->inventory;
// 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 stats in the v2 format
// 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
// stats in the v2 format
ret.inventory.encode_for_client(Version::DC_11_2000, nullptr);
ret.disp = this->disp.to_dcpcv3<false>(language, language);
ret.disp.visual.enforce_lobby_join_limits_for_version(Version::DC_V2);
@@ -989,8 +980,8 @@ PSOBBCharacterFile::operator PSODCV1CharacterFile::Character() const {
PSODCV1CharacterFile::Character ret;
ret.inventory = this->inventory;
// 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 stats in the v2 format
// 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
// stats in the v2 format
ret.inventory.encode_for_client(Version::DC_V1, nullptr);
ret.disp = this->disp.to_dcpcv3<false>(language, language);
ret.disp.visual.enforce_lobby_join_limits_for_version(Version::DC_V2);
@@ -1021,8 +1012,8 @@ PSOBBCharacterFile::operator PSODCV2CharacterFile::Character() const {
PSODCV2CharacterFile::Character ret;
ret.inventory = this->inventory;
// 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 stats in the v2 format
// 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
// stats in the v2 format
ret.inventory.encode_for_client(Version::DC_V2, nullptr);
ret.disp = this->disp.to_dcpcv3<false>(language, language);
ret.disp.visual.enforce_lobby_join_limits_for_version(Version::DC_V2);
@@ -1062,10 +1053,9 @@ PSOBBCharacterFile::operator PSOGCNTECharacterFileCharacter() const {
PSOGCNTECharacterFileCharacter ret;
ret.inventory = this->inventory;
// Note: We intentionally do not call ret.inventory.encode_for_client here.
// This is because the GC client byteswaps data2 in each item before sending
// it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does
// not do this, so the data2 fields are already in the correct order here.
// Note: We intentionally do not call ret.inventory.encode_for_client here. This is because the GC client byteswaps
// data2 in each item before sending it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does 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.visual.enforce_lobby_join_limits_for_version(Version::GC_V3);
ret.validation_flags = this->validation_flags;
@@ -1100,10 +1090,9 @@ PSOBBCharacterFile::operator PSOGCCharacterFile::Character() const {
PSOGCCharacterFile::Character ret;
ret.inventory = this->inventory;
// Note: We intentionally do not call ret.inventory.encode_for_client here.
// This is because the GC client byteswaps data2 in each item before sending
// it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does
// not do this, so the data2 fields are already in the correct order here.
// Note: We intentionally do not call ret.inventory.encode_for_client here. This is because the GC client byteswaps
// data2 in each item before sending it to the server in the 61 and 98 commands, but GetExtendedPlayerInfo does 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.visual.enforce_lobby_join_limits_for_version(Version::GC_V3);
ret.validation_flags = this->validation_flags;
@@ -1220,27 +1209,22 @@ void PSOCHARFile::save(
phosg::fwritex(f.get(), header);
phosg::fwritex(f.get(), *character);
phosg::fwritex(f.get(), *system);
// TODO: Technically, we should write the actual team membership struct to
// the file here, but that would cause Client to depend on Account, which it
// currently does not. This data doesn't matter at all for correctness within
// newserv, since it ignores this data entirely and instead generates the
// membership struct from the team ID in the Account and the team's state.
// So, writing correct data here would mostly be for compatibility with other
// PSO servers. But if the other server is newserv, then this data wouldn't
// 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.
// TODO: Technically, we should write the actual team membership struct to the file here, but that would cause Client
// to depend on Account, which it currently does not. This data doesn't matter at all for correctness within newserv,
// since it ignores this data entirely and instead generates the membership struct from the team ID in the Account
// and the team's state. So, writing correct data here would mostly be for compatibility with other PSO servers. But
// if the other server is newserv, then this data wouldn't 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;
phosg::fwritex(f.get(), empty_membership);
}
// TODO: Eliminate duplication between this function and the parallel function
// in PlayerBankT
// TODO: Eliminate duplication between this function and the parallel function in PlayerBankT
void PSOBBCharacterFile::add_item(const ItemData& item, const ItemData::StackLimits& limits) {
uint32_t primary_identifier = item.primary_identifier();
// Annoyingly, meseta is in the disp data, not in the inventory struct. If the
// item is meseta, we have to modify disp instead.
// Meseta is in the disp data, not in the inventory struct. If the item is meseta, we have to modify disp instead.
if (primary_identifier == 0x04000000) {
this->add_meseta(item.data2d);
return;
@@ -1249,8 +1233,7 @@ void PSOBBCharacterFile::add_item(const ItemData& item, const ItemData::StackLim
// Handle combinable items
size_t combine_max = item.max_stack_size(limits);
if (combine_max > 1) {
// Get the item index if there's already a stack of the same item in the
// player's inventory
// Get the item index if there's already a stack of the same item in the player's inventory
size_t y;
for (y = 0; y < this->inventory.num_items; y++) {
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
// go into an empty inventory slot
// If we get here, then it's not meseta and not a combine item, so it needs to go into an empty inventory slot
if (this->inventory.num_items >= 30) {
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++;
}
// TODO: Eliminate code duplication between this function and the parallel
// function in PlayerBankT
// TODO: Eliminate code duplication between this function and the parallel function in PlayerBankT
ItemData PSOBBCharacterFile::remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits) {
ItemData ret;
// If we're removing meseta (signaled by an invalid item ID), then create a
// meseta item.
// If we're removing meseta (signaled by an invalid item ID), then create a meseta item.
if (item_id == 0xFFFFFFFF) {
this->remove_meseta(amount, !is_v4(limits.version));
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];
bool is_equipped = (inventory_item.flags & 0x00000008);
// 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 amount of the existing stack. Note
// that passing amount == 0 means to remove the entire stack, so this only
// applies if amount is nonzero.
// 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
// amount of the existing stack. Note that passing amount == 0 means to remove the entire stack, so this only applies
// if amount is nonzero.
if (amount && (inventory_item.data.stack_size(limits) > 1) && (amount < inventory_item.data.data1[5])) {
if (is_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;
}
// If we get here, then it's not meseta, and either it's not a combine item or
// we're removing the entire stack. Delete the item from the inventory slot
// and return the deleted item.
// If we get here, then it's not meseta, and either it's not a combine item or we're removing the entire stack.
// Delete the item from the inventory slot and return the deleted item.
if (is_equipped) {
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) {
// Tethealla (Ephinea) doesn't store material counts anywhere in the file,
// so if the material counts in the inventory extension data are all zero,
// check the current stats against the expected stats for the character's
// current level and set the material counts if they make sense.
// Tethealla (Ephinea) doesn't store material counts anywhere in the file, so if the material counts in the inventory
// extension data are all zero, check the current stats against the expected stats for the character's current level
// and set the material counts if they make sense.
if (this->get_material_usage(PSOBBCharacterFile::MaterialType::POWER) |
this->get_material_usage(PSOBBCharacterFile::MaterialType::MIND) |
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 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,
// there are no limits, and we don't want to reject legitimate characters
// that have used more than 250 materials.
// We intentionally do not check any limits here. This is because on pre-v3, there are no limits, and we don't want
// to reject legitimate characters that have used more than 250 materials.
this->set_material_usage(MaterialType::POWER, pow);
this->set_material_usage(MaterialType::MIND, mind);
+94 -145
View File
@@ -18,7 +18,7 @@
#include "PlayerSubordinates.hh"
#include "Text.hh"
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Memory card / VMU structures
struct PSOVMSFileHeader {
@@ -45,12 +45,10 @@ struct PSOVMSFileHeader {
} __packed_ws__(PSOVMSFileHeader, 0x80);
struct PSOGCIFileHeader {
// Every PSOGC save file begins with a PSOGCIFileHeader. The first 0x40 bytes
// of this structure are the .gci file header; the remaining bytes after that
// 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 not encrypted, but the character
// and Guild Card files are encrypted using a seed stored in the system file.
// Every PSOGC save file begins with a PSOGCIFileHeader. The first 0x40 bytes of this structure are the .gci file
// header; the remaining bytes after that 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
// 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.
/* 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.
@@ -67,19 +65,17 @@ struct PSOGCIFileHeader {
/* 0038 */ be_uint16_t num_blocks = 0;
/* 003A */ parray<uint8_t, 2> unused2;
/* 003C */ be_uint32_t comment_offset = 0;
// GCI header ends here (and memcard file data begins here)
// game_name is e.g. "PSO EPISODE I & II" or "PSO EPISODE III"
/* 0040 */ pstring<TextEncoding::MARKED, 0x1C> game_name;
// GCI header ends here (and memcard data begins here)
/* 0040 */ pstring<TextEncoding::MARKED, 0x1C> game_name; // e.g. "PSO EPISODE I & II" or "PSO EPISODE III"
/* 005C */ be_uint32_t embedded_seed = 0; // Used in some of Ralf's quest packs
/* 0060 */ pstring<TextEncoding::MARKED, 0x20> file_name;
/* 0080 */ parray<uint8_t, 0x1800> banner;
/* 1880 */ parray<uint8_t, 0x800> icon;
// data_size specifies the number of bytes remaining in the file. In all cases
// except for the system file, this data is encrypted.
// data_size specifies the number of bytes remaining in the file. In all cases except for the system file, this data
// is encrypted.
/* 2080 */ be_uint32_t data_size = 0;
// To compute checksum, set checksum to zero, then compute the CRC32 of all
// fields in this struct starting with gci_header.game_name. (Yes, including
// the checksum field, which is temporarily zero.) See checksum_correct below.
// To compute checksum, set checksum to zero, then compute the CRC32 of all fields in this struct starting with
// gci_header.game_name. (Yes, including the checksum field, which is temporarily zero.) See checksum_correct below.
/* 2084 */ be_uint32_t checksum = 0;
/* 2088 */
@@ -93,13 +89,9 @@ struct PSOGCIFileHeader {
struct PSOXBFileHeader {
// The signature is computed by doing the following:
// // TODO: Should flags be 0 or 1? It looks like it should be 0 for
// // character files, but not sure about this
// // TODO: Should flags be 0 or 1? It looks like it should be 0 for character files, but not sure about this
// auto handle = XCalculateSignatureBegin(flags);
// XCalculateSignatureUpdate(
// handle,
// &header.source_size,
// total_size - offsetof(PSOXBFileHeader, source_size));
// XCalculateSignatureUpdate(handle, &header.source_size, total_size - offsetof(PSOXBFileHeader, source_size));
// XCalculateSignatureEnd(handle, header.signature);
/* 0000 */ parray<uint8_t, 0x14> signature;
/* 0014 */ le_uint32_t source_size = 0; // == total file size - 0x4000
@@ -116,7 +108,7 @@ struct PSOXBFileHeader {
void check() const;
} __packed_ws__(PSOXBFileHeader, 0x6048);
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Subordinate structures
struct ShuffleTables {
@@ -247,15 +239,13 @@ struct PSOBBFullTeamMembership {
PSOBBFullTeamMembership() = default;
} __packed_ws__(PSOBBFullTeamMembership, 0x838);
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// System files
struct PSOPCCreationTimeFile { // PSO______FLS
// The game creates this file if necessary and fills it with random data.
// Most of the random data appears to be a decoy; only one field is used.
// As in other PSO versions, creation_timestamp is used as an encryption key
// for the other save files, but only if the serial number isn't set in the
// Windows registry.
// The game creates this file if necessary and fills it with random data. Most of the random data appears to be a
// decoy; only one field is used. As in other PSO versions, creation_timestamp is used as an encryption key for the
// other save files, but only if the serial number isn't set in the Windows registry.
/* 0000 */ parray<uint8_t, 0x624> unused1;
/* 0624 */ le_uint32_t creation_timestamp = 0;
/* 0628 */ parray<uint8_t, 0xDD8> unused2;
@@ -264,8 +254,8 @@ struct PSOPCCreationTimeFile { // PSO______FLS
struct PSOPCSystemFile { // PSO______COM
/* 0000 */ le_uint32_t checksum = 0;
// Most of these fields are guesses based on the format used in GC and the
// assumption that Sega didn't change much between versions.
// Most of these fields are guesses based on the format used in GC and the assumption that Sega didn't change much
// between versions.
/* 0004 */ le_int16_t music_volume = 0;
/* 0006 */ int8_t sound_volume = 0;
/* 0007 */ Language language = Language::ENGLISH;
@@ -282,17 +272,15 @@ struct PSOGCSystemFile {
/* 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
/* 0007 */ Language language = Language::ENGLISH;
// This field stores the effective time zone offset between the server and
// client, in frames. The default value is 1728000, which corresponds to 16
// hours. This is recomputed when the client receives a B1 command.
// This field stores the effective time zone offset between the server and client, in frames. The default value is
// 1728000, which corresponds to 16 hours. This is recomputed when the client receives a B1 command.
/* 0008 */ be_int32_t server_time_delta_frames = 1728000;
/* 000C */ be_uint16_t udp_behavior = 0; // 0 = auto, 1 = on, 2 = off
/* 000E */ be_uint16_t surround_sound_enabled = 0;
/* 0010 */ parray<uint8_t, 0x100> event_flags; // Can be set by quest opcode D8 or E8
/* 0110 */ parray<uint8_t, 8> unknown_a7;
// This timestamp is the number of seconds since 12:00AM on 1 January 2000.
// This field is also used as the round1 seed for encrypting the character and
// Guild Card files.
// This timestamp is the number of seconds since 12:00AM on 1 January 2000. This field is also used as the round1
// seed for encrypting the character and Guild Card files.
/* 0118 */ be_uint32_t creation_timestamp = 0;
/* 011C */
} __packed_ws__(PSOGCSystemFile, 0x11C);
@@ -355,7 +343,7 @@ struct PSOBBBaseSystemFile : PSOBBMinimalSystemFile {
PSOBBBaseSystemFile();
} __packed_ws__(PSOBBBaseSystemFile, 0x2B8);
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Character files
struct PSODCNTECharacterFile {
@@ -364,14 +352,12 @@ struct PSODCNTECharacterFile {
// See PSOGCCharacterFile::Character for descriptions of fields' meanings.
/* 0000:---- */ PlayerInventory inventory;
/* 034C:---- */ PlayerDispDataDCPCV3 disp;
// masked_creation_timestamp is expected to contain the value
// (creation_timestamp ^ hardware_id_mid), where hardware_id_mid contains
// the middle 32 bits of the 64-bit hardware ID returned by the SYSINFO_ID
// syscall (the top and bottom 16 bits are ignored for this purpose).
// masked_creation_timestamp is expected to contain the value (creation_timestamp ^ hardware_id_mid), where
// hardware_id_mid contains the middle 32 bits of the 64-bit hardware ID returned by the SYSINFO_ID syscall (the
// top and bottom 16 bits are ignored for this purpose).
/* 041C:0000 */ le_uint32_t masked_creation_timestamp = 0;
/* 0420:0004 */ le_uint32_t creation_timestamp = 0;
// The value of signature is approximately pi * 1e9, but they got a couple
// of digits wrong (3141562653)
// The value of signature is approximately pi * 1e9, but they got a couple of digits wrong (3141562653)
/* 0424:0008 */ le_uint32_t signature = 0xBB40711D;
/* 0428:000C */ le_uint32_t play_time_seconds = 0;
/* 042C:0010 */ le_uint32_t option_flags = 0x00040058;
@@ -379,9 +365,8 @@ struct PSODCNTECharacterFile {
/* 0432:0016 */ le_uint16_t inventory_erasure_count = 0;
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
// TODO: Figure out how quest flags work; it's obviously different from 0x80
// bytes per difficulty like in v1. Is it just 2048 flags shared across all
// difficulties, instead of 1024 in each difficulty?
// TODO: Figure out how quest flags work; it's obviously different from 0x80 bytes per difficulty like in v1. Is it
// just 2048 flags shared across all difficulties, instead of 1024 in each difficulty?
/* 0460:0044 */ parray<uint8_t, 0x100> quest_flags;
/* 0560:0144 */ le_uint16_t bank_meseta;
/* 0562:0146 */ le_uint16_t num_bank_items;
@@ -482,11 +467,9 @@ struct PSODCV2CharacterFile {
/* 15C8:11AC */ PlayerRecordsBattle battle_records;
/* 15E0:11C4 */ PlayerRecordsChallengeDC challenge_records;
/* 1680:1264 */ parray<le_uint16_t, 20> tech_menu_shortcut_entries;
// The Choice Search config is stored here as 32-bit integers, even though
// it's represented with 16-bit integers in the various commands that send it
// to and from the server. The order of the entries here is the same (that
// is, the first two of these ints are entries[0], the second two are
// entries[1], etc.).
// The Choice Search config is stored here as 32-bit integers, even though it's represented with 16-bit integers in
// the various commands that send it to and from the server. The order of the entries here is the same (that 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;
/* 16D0:12B4 */ parray<uint8_t, 4> unknown_a2;
/* 16D4:12B8 */ pstring<TextEncoding::ASCII, 0x10> v2_serial_number;
@@ -565,9 +548,8 @@ struct PSOGCNTECharacterFileCharacter {
/* 2494:2078 */ parray<uint8_t, 4> unknown_a4;
/* 2498:207C */ PlayerRecordsChallengeDC challenge_records;
/* 2538:211C */ parray<be_uint16_t, 20> tech_menu_shortcut_entries;
// TODO: choice_search_config and offline_battle_records may be in here
// somewhere. When they are found, don't forget to update the conversion
// functions in PSOBBCharacterFile.
// TODO: choice_search_config and offline_battle_records may be in here somewhere. When they are found, don't forget
// to update the conversion functions in PSOBBCharacterFile.
/* 2560:2144 */ parray<uint8_t, 0x130> unknown_n2;
/* 2690:2274 */
} __packed_ws__(PSOGCNTECharacterFileCharacter, 0x2690);
@@ -575,58 +557,45 @@ struct PSOGCNTECharacterFileCharacter {
struct PSOGCCharacterFile {
/* 00000 */ be_uint32_t checksum = 0;
struct Character {
// This structure is internally split into two by the game. The offsets
// here are relative to the start of this structure (first column), and
// relative to the start of the second internal structure (second column).
// This structure is internally split into two by the game. The offsets here are relative to the start of this
// structure (first column), and relative to the start of the second internal structure (second column).
/* 0000:---- */ PlayerInventoryBE inventory;
/* 034C:---- */ PlayerDispDataDCPCV3BE disp;
// Known bits in the validation_flags field:
// 00000001: Character was not saved after disconnecting (and the message
// about items being deleted is shown in the select menu)
// 00000001: Character was not saved after disconnecting (and the message about items being deleted is shown in
// the select menu)
// 00000002: Character has level out of range (< 0 or > max)
// 00000004: Character has EXP out of range for their current level
// 00000008: Character has one or more stats out of range (< 0 or > max)
// 00000010: Character has ever possessed a hacked item, according to the
// check_for_hacked_item function in DCv2. It appears this logic was
// removed in v3, so this flag is unused on v3+.
// 00000010: Character has ever possessed a hacked item, according to the check_for_hacked_item function in DCv2.
// It appears this logic was removed in v3, so this flag is unused on v3+.
// 00000020: Character has meseta out of range (< 0 or > 999999)
// 00000040: Character was loaded on a client that has "important" files
// modified (on GC, these files are ending_normal.sfd, psogc_j.sfd,
// psogc_j2.sfd, ult01.sfd, ult02.sfd, ult03.sfd, ult04.sfd,
// ItemPMT.prs, itemrt.gsl, itempt.gsl, and PlyLevelTbl.cpt). For files
// larger than 1000000 bytes (decimal), the game only checks the file's
// size and skips checksumming its contents.
// 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.
// 00000040: Character was loaded on a client that has "important" files modified (on GC, these files are
// ending_normal.sfd, psogc_j.sfd, psogc_j2.sfd, ult01.sfd, ult02.sfd, ult03.sfd, ult04.sfd, ItemPMT.prs,
// itemrt.gsl, itempt.gsl, and PlyLevelTbl.cpt). For files larger than 1000000 bytes (decimal), the game only
// checks the file's size and skips checksumming its contents.
// 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 00000001 and 00000002 at the same time, but 00000002 is never set.
/* 041C:0000 */ be_uint32_t validation_flags = 0;
// The creation timestamp is measured in seconds since midnight on 1
// January 2000.
// The creation timestamp is measured in seconds since midnight on 1 January 2000.
/* 0420:0004 */ be_uint32_t creation_timestamp = 0;
// The signature field holds the value 0xA205B064, which is 2718281828 in
// decimal - approximately e * 10^9. It's unknown why Sega chose this
// value. On some other versions, this field has a different value; see the
// defaults in the other versions' structures.
// The signature field holds the value 0xA205B064, which is 2718281828 in decimal - approximately e * 10^9. It's
// unknown why Sega chose this value. On some other versions, this field has a different value; see the defaults in
// the other versions' structures.
/* 0424:0008 */ be_uint32_t signature = 0xA205B064;
/* 0428:000C */ be_uint32_t play_time_seconds = 0;
// This field is a collection of several flags and small values. The known
// fields are:
// This field is a collection of several flags and small values. The known fields are:
// ------AB -----CDD EEEFFFGG HIJKLMNO
// A = Function key setting (BB; 0 = menu shortcuts; 1 = chat shortcuts).
// This bit is unused by PSO GC.
// B = Keyboard controls (BB; 0 = on; 1 = off). This field is also used
// by PSO GC, but its function is currently unknown.
// A = Function key setting (BB; 0 = menu shortcuts; 1 = chat shortcuts). 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.
// C = Choice search setting (0 = enabled; 1 = disabled)
// D = Which pane of the shortcut menu was last used
// E = Player lobby labels (0 = name; 1 = name, language, and level;
// 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 behavior due to a missing
// bounds check).
// E = Player labels (0 = name; 1 = name, language, and level; 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
// behavior due to a missing bounds check).
// 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
// text input. See TWindowKeyBoardBase_read_option_flags
// H, I, J, K = unknown; these appear to be used only during Japanese text input.
// See TWindowKeyBoardBase_read_option_flags
// L = Rumble enabled
// M = Cursor position (0 = saved; 1 = non-saved)
// N = Button config (0 = normal; 1 = L/R reversed)
@@ -668,9 +637,8 @@ struct PSOGCCharacterFile {
} __packed_ws__(PSOGCCharacterFile, 0x1156C);
struct PSOGCEp3NTECharacter {
// This structure is internally split into two by the game. The offsets here
// are relative to the start of this structure (first column), and relative
// to the start of the second internal structure (second column).
// This structure is internally split into two by the game. The offsets here are relative to the start of this
// structure (first column), and relative to the start of the second internal structure (second column).
/* 0000:---- */ PlayerInventoryBE inventory;
/* 034C:---- */ PlayerDispDataDCPCV3BE disp;
/* 041C:0000 */ be_uint32_t validation_flags = 0;
@@ -704,9 +672,8 @@ struct PSOGCEp3NTECharacter {
struct PSOGCEp3CharacterFile {
/* 00000 */ be_uint32_t checksum = 0; // crc32 of this field (as 0) through end of struct
struct Character {
// This structure is internally split into two by the game. The offsets here
// are relative to the start of this structure (first column), and relative
// to the start of the second internal structure (second column).
// This structure is internally split into two by the game. The offsets here are relative to the start of this
// structure (first column), and relative to the start of the second internal structure (second column).
/* 0000:---- */ PlayerInventoryBE inventory;
/* 034C:---- */ PlayerDispDataDCPCV3BE disp;
/* 041C:0000 */ be_uint32_t validation_flags = 0;
@@ -717,16 +684,13 @@ struct PSOGCEp3CharacterFile {
/* 0430:0014 */ be_uint32_t save_count = 1;
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
// seq_vars is an array of 8192 bits, which contain all the Episode 3 quest
// progress flags. This includes things like which maps are unlocked, which
// 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.
// seq_vars is an array of 8192 bits, which contain all the Episode 3 quest progress flags. This includes things
// like which maps are unlocked, which 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.
/* 0460:0044 */ Ep3SeqVars seq_vars;
/* 0860:0444 */ be_uint32_t death_count = 0;
// Curiously, Episode 3 characters do have item banks, but there are only 4
// item slots. Presumably Sega didn't completely remove the bank in Ep3
// because they would have had to change too much code.
// Curiously, Episode 3 characters do have item banks, but there are only 4 item slots. Presumably Sega didn't
// completely remove the bank in Ep3 because they would have had to change too much code.
/* 0864:0448 */ PlayerBankT<4, true> bank;
/* 08CC:04B0 */ GuildCardGCBE guild_card;
/* 095C:0540 */ parray<SaveFileSymbolChatEntryGC, 12> symbol_chats;
@@ -751,21 +715,17 @@ struct PSOGCEp3CharacterFile {
/* 193F0 */ pstring<TextEncoding::ASCII, 0x10> serial_number; // As {:08X} (not decimal)
/* 19400 */ pstring<TextEncoding::ASCII, 0x10> access_key; // As 12 ASCII characters (decimal)
/* 19410 */ pstring<TextEncoding::ASCII, 0x10> password;
// In Episode 3, this field still exists, but is unused since BGM test was
// removed from the options menu in favor of the jukebox. The jukebox is
// accessible online only, and which songs are available there is controlled
// by the B7 command sent by the server instead.
// In Episode 3, this field still exists, but is unused since BGM test was removed from the options menu in favor of
// the jukebox. The jukebox is accessible online only, and which songs are available there is controlled by the B7
// command sent by the server instead.
/* 19420 */ be_uint64_t bgm_test_songs_unlocked = 0;
/* 19428 */ be_uint32_t save_count = 1;
// This is an array of 999 bits, represented here as 128 bytes (the last 25
// bits are not used). Each bit corresponds 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 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 B8 command (server card
// definitions), the game clears this array, and sets all bits whose
// 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.
// This is an array of 999 bits, represented here as 128 bytes (the last 25 bits are not used). Each bit corresponds
// 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
// 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
// B8 command (server card definitions), the game clears this array, and sets all bits whose 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;
/* 194AC */ be_uint32_t round2_seed = 0;
/* 194B0 */
@@ -773,10 +733,9 @@ struct PSOGCEp3CharacterFile {
struct PSOXBCharacterFile {
struct Character {
// This structure is internally split into two by the game. The offsets here
// are relative to the start of this structure (first column), and relative
// to the start of the second internal structure (second column).
// Most fields have the same meanings as in PSOGCCharacterFile::Character.
// This structure is internally split into two by the game. The offsets here are relative to the start of this
// structure (first column), and relative to the start of the second internal structure (second column). Most
// fields have the same meanings as in PSOGCCharacterFile::Character.
/* 0000:---- */ PlayerInventory inventory;
/* 034C:---- */ PlayerDispDataDCPCV3 disp;
/* 041C:0000 */ le_uint32_t validation_flags = 0;
@@ -832,10 +791,9 @@ struct PSOXBCharacterFile {
} __packed_ws__(PSOXBCharacterFile, 0x26564);
struct PSOBBCharacterFile {
// Most fields have the same meanings as in PSOGCCharacterFile::Character.
// This is part of the .psochar file format, but it is not the first member
// of that structure, so add 8 to all the offsets here if you're working with
// a .psochar file. See PSOCHARFile below for the full file format.
// Most fields have the same meanings as in PSOGCCharacterFile::Character. This is part of the .psochar file format,
// but it is not the first member of that structure, so add 8 to all the offsets here if you're working with a
// .psochar file. See PSOCHARFile below for the full file format.
/* 0000 */ PlayerInventory inventory;
/* 034C */ PlayerDispDataBB disp;
@@ -924,9 +882,8 @@ struct PSOBBCharacterFile {
} __packed_ws__(PSOBBCharacterFile, 0x2EA4);
struct PSOCHARFile {
// This is the format of .psochar files used by newserv and Ephinea (and
// perhaps other servers as well). newserv doesn't actually use this
// structure in its logic, so it's here primarily for documentation.
// This is the format of .psochar files used by newserv and Ephinea (and perhaps other servers as well). newserv
// doesn't actually use this structure in its logic, so it's here primarily for documentation.
/* 0000 */ PSOCommandHeaderBB header; // command = 0x00E7, size = 0x399C, flag = 0
/* 0008 */ PSOBBCharacterFile character;
@@ -946,7 +903,7 @@ struct PSOCHARFile {
std::shared_ptr<const PSOBBCharacterFile> character);
} __packed_ws__(PSOCHARFile, 0x399C);
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Guild Card files
struct PSODCNTEGuildCardFile {
@@ -1048,15 +1005,14 @@ struct PSOBBGuildCardFile {
void delete_duplicates();
} __packed_ws__(PSOBBGuildCardFile, 0xD590);
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Snapshot files
struct PSOGCSnapshotFile {
/* 00000 */ be_uint32_t checksum = 0;
/* 00004 */ be_uint16_t width = 0x100;
/* 00006 */ be_uint16_t height = 0xC0;
// Pixels are stored as 4x4 blocks of RGB565 values. See the implementation
// of decode_image for details.
// Pixels are stored as 4x4 blocks of RGB565 values. See the implementation of decode_image for details.
/* 00008 */ parray<be_uint16_t, 0xC000> pixels;
/* 18008 */ uint8_t unknown_a1 = 0x18; // Always 0x18?
/* 18009 */ uint8_t unknown_a2 = 0;
@@ -1070,7 +1026,7 @@ struct PSOGCSnapshotFile {
phosg::ImageRGB888 decode_image() const;
} __packed_ws__(PSOGCSnapshotFile, 0x1818C);
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Obsolete newserv-specific formats (for backward compatibility only)
struct LegacySavedPlayerDataBB { // .nsc file format
@@ -1112,7 +1068,7 @@ struct LegacySavedAccountDataBB { // .nsa file format
/* F080 */
} __packed_ws__(LegacySavedAccountDataBB, 0xF080);
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Encoding/decoding functions
template <bool BE>
@@ -1184,8 +1140,7 @@ std::string decrypt_fixed_size_data_section_s(
checksum = expected_crc;
if (expected_crc != actual_crc) {
throw std::runtime_error(std::format(
"incorrect decrypted data section checksum: expected {:08X}; received {:08X}",
expected_crc, actual_crc));
"incorrect decrypted data section checksum: expected {:08X}; received {:08X}", expected_crc, actual_crc));
}
}
@@ -1220,8 +1175,7 @@ StructT decrypt_fixed_size_data_section_t(
ret.checksum = expected_crc;
if (expected_crc != actual_crc) {
throw std::runtime_error(std::format(
"incorrect decrypted data section checksum: expected {:08X}; received {:08X}",
expected_crc, actual_crc));
"incorrect decrypted data section checksum: expected {:08X}; received {:08X}", 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(
uint16_t year,
uint8_t month,
uint8_t day,
uint8_t hour,
uint8_t minute,
uint8_t second);
uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second);
std::string encode_psobb_hangame_credentials(
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)
0x0130009D, // (02) DCv2/GCNTE extended login
0x0130019D, // (02) DCv2/GCNTE extended login (UDP off)
// Note: PSO PC initial commands are not listed here because we don't use a
// detector encryption for PSO PC (instead, we use the split reconnect command
// to send PC to a different port).
// Note: PSO PC initial commands are not listed here because we don't use a detector encryption for PSO PC
// (instead, we use the split reconnect command to send PC to a different port).
});
const unordered_set<uint32_t> v3_crypt_initial_client_commands({
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);
}
void send_command_excluding_client(shared_ptr<Lobby> l, shared_ptr<Client> c,
uint16_t command, uint32_t flag, const void* data, size_t size) {
void send_command_excluding_client(
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) {
if (!client || (client == c)) {
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,
uint16_t command, uint32_t flag, const void* data, size_t size) {
void send_command_if_not_loading(
shared_ptr<Lobby> l, uint16_t command, uint32_t flag, const void* data, size_t size) {
for (auto& client : l->clients) {
if (!client || client->check_flag(Client::Flag::LOADING)) {
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,
const void* data, size_t size) {
void send_command(shared_ptr<Lobby> l, uint16_t command, uint32_t flag, const void* data, size_t size) {
send_command_excluding_client(l, nullptr, command, flag, data, size);
}
void send_command(shared_ptr<ServerState> s, uint16_t command, uint32_t flag,
const void* data, size_t size) {
void send_command(shared_ptr<ServerState> s, uint16_t command, uint32_t flag, const void* data, size_t size) {
for (auto& l : s->all_lobbies()) {
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_pm_server_copyright = "PSO NEW PM Server. Copyright 1999-2002 SONICTEAM.";
S_ServerInitWithAfterMessageT_DC_PC_V3_02_17_91_9B<0xB4>
prepare_server_init_contents_console(
S_ServerInitWithAfterMessageT_DC_PC_V3_02_17_91_9B<0xB4> prepare_server_init_contents_console(
uint32_t server_key, uint32_t client_key, uint8_t flags) {
bool initial_connection = (flags & SendServerInitFlag::IS_INITIAL_CONNECTION);
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>
prepare_server_init_contents_bb(
const parray<uint8_t, 0x30>& server_key,
const parray<uint8_t, 0x30>& client_key,
uint8_t flags) {
S_ServerInitWithAfterMessageT_BB_03_9B<0xB4> prepare_server_init_contents_bb(
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);
S_ServerInitWithAfterMessageT_BB_03_9B<0xB4> cmd;
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(
shared_ptr<Client> c,
vector<string>& client_path_directories,
const vector<string>& file_path_directories) {
shared_ptr<Client> c, vector<string>& client_path_directories, const vector<string>& file_path_directories) {
// First, exit all leaf directories that don't match the desired path
while (!client_path_directories.empty() &&
((client_path_directories.size() > file_path_directories.size()) ||
@@ -322,8 +313,7 @@ void send_patch_change_to_directory(
client_path_directories.pop_back();
}
// At this point, client_path_directories should be a prefix of
// file_path_directories (or should match exactly)
// At this point, client_path_directories should be a prefix of file_path_directories (or should match exactly)
if (client_path_directories.size() > file_path_directories.size()) {
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);
try {
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->specific_version, call1_res.checksum);
c->log.info_f("Version detected as {:08X} from header checksum {:08X}", c->specific_version, call1_res.checksum);
} catch (const out_of_range&) {
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";
}
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->log.info_f("Version detected as {:08X}", c->specific_version);
}
@@ -391,8 +381,8 @@ string prepare_send_function_call_data(
if (use_encrypted_format) {
uint32_t key = phosg::random_object<uint32_t>();
// This format was probably never used on any little-endian system, but we
// implement the way it would probably work there if it was used.
// This format was probably never used on any little-endian system, but we implement the way it would probably
// work there if it was used.
phosg::StringWriter w;
if (code->is_big_endian()) {
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
for (size_t offset = 0; offset < dol->data.size();) {
// Note: The protocol allows commands to be up to 0x7C00 bytes in size, but
// sending large B2 commands can cause the client to crash or softlock. To
// avoid this, we limit the payload to 4KB, which results in a B2 command
// 0x10D0 bytes in size.
// Note: The protocol allows commands to be up to 0x7C00 bytes in size, but sending large B2 commands can cause the
// client to crash or softlock. To avoid this, we limit the payload to 4KB, which results in a B2 command 0x10D0
// bytes in size.
size_t bytes_to_send = min<size_t>(0x1000, dol->data.size() - offset);
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;
}
// Send the final function, which moves the DOL's sections into place and
// calls the entrypoint
// Send the final function, which moves the DOL's sections into place and calls the entrypoint
auto fn = s->function_code_index->name_to_function.at("RunDOL");
label_writes = {{"dol_base_ptr", dol_base_addr}};
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);
}
void send_pc_console_split_reconnect(shared_ptr<Client> c, uint32_t address,
uint16_t pc_port, uint16_t console_port) {
void send_pc_console_split_reconnect(shared_ptr<Client> c, uint32_t address, uint16_t pc_port, uint16_t console_port) {
S_ReconnectSplit_19 cmd;
cmd.pc_address = address;
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.episode_4_unlocked = 1;
// If security_token is zero, the game scrambles the client config data based
// on the first character in the username. We undo the scramble here, so when
// the client scrambles the data upon receipt, it will be correct when it next
// is sent back to the server.
// If security_token is zero, the game scrambles the client config data based on the first character in the username.
// We undo the scramble here, so when the client scrambles the data upon receipt, it will be correct when it next is
// sent back to the server.
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);
}
@@ -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) {
if (!preview) {
// no player exists
if (!preview) { // No player exists
S_PlayerPreview_NoPlayer_BB_00E4 cmd = {character_index, 0x00000002};
send_command_t(c, 0x00E4, 0x00000000, cmd);
} else {
SC_PlayerPreview_CreateCharacter_BB_00E5 cmd = {character_index, *preview};
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));
cmd.unknown_a1 = 0;
cmd.chunk_index = chunk_index;
cmd.data.assign_range(
reinterpret_cast<const uint8_t*>(c->guild_card_file().get()) + chunk_offset,
data_size, 0);
cmd.data.assign_range(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);
}
@@ -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& e = entries.emplace_back();
e.size = cache_res.file->data->size();
// Computing the checksum can be slow, so we cache it along with the file
// data. If the cache result was just populated, then it may be different,
// so we always recompute the checksum in that case.
// Computing the checksum can be slow, so we cache it along with the file data. If the cache result was just
// populated, then it may be different, so we always recompute the checksum in that case.
if (cache_res.generate_called) {
e.checksum = crc32(cache_res.file->data->data(), e.size);
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);
}
////////////////////////////////////////////////////////////////////////////////
// message functions
enum class ColorMode {
NONE,
ADD,
@@ -863,12 +840,14 @@ static void send_text(
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;
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;
w.put(SC_TextHeader_01_06_11_B0_EE({0, guild_card_number}));
send_text(ch, w, command, 0x00, text, color_mode);
@@ -1098,12 +1077,7 @@ void send_chat_message(
const string& text,
char private_flags) {
string prepared_data = prepare_chat_data(
c->version(),
c->language(),
c->lobby_client_id,
from_name,
text,
private_flags);
c->version(), c->language(), c->lobby_client_id, from_name, text, private_flags);
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>
void send_info_board_t(shared_ptr<Client> c) {
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>
void send_card_search_result_t(
shared_ptr<Client> c,
shared_ptr<Client> result,
shared_ptr<Lobby> result_lobby) {
void send_card_search_result_t(shared_ptr<Client> c, shared_ptr<Client> result, shared_ptr<Lobby> result_lobby) {
auto s = c->require_server_state();
S_GuildCardSearchResultT<CommandHeaderT, Encoding> cmd;
@@ -1263,10 +1231,7 @@ void send_card_search_result_t(
send_command_t(c, 0x41, 0x00, cmd);
}
void send_card_search_result(
shared_ptr<Client> c,
shared_ptr<Client> result,
shared_ptr<Lobby> result_lobby) {
void send_card_search_result(shared_ptr<Client> c, shared_ptr<Client> result, shared_ptr<Lobby> result_lobby) {
switch (c->version()) {
case Version::DC_NTE:
case Version::DC_11_2000:
@@ -1399,8 +1364,7 @@ void send_guild_card(
ch, guild_card_number, name, description, language, section_id, char_class);
break;
case Version::XB_V3:
send_guild_card_xb(
ch, guild_card_number, xb_user_id, name, description, language, section_id, char_class);
send_guild_card_xb(ch, guild_card_number, xb_user_id, name, description, language, section_id, char_class);
break;
case Version::BB_V4:
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);
}
////////////////////////////////////////////////////////////////////////////////
// menus
template <typename EntryT>
void send_menu_t(shared_ptr<Client> c, shared_ptr<const Menu> menu, bool is_info_menu) {
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
// why we do this.
// See the description of the 07 command in CommandFormats.hh for details on why we do this.
if (is_pre_v1(c->version())) {
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>
void send_game_menu_t(
shared_ptr<Client> c,
bool is_spectator_team_list,
bool show_tournaments_only) {
void send_game_menu_t(shared_ptr<Client> c, bool is_spectator_team_list, bool show_tournaments_only) {
auto s = c->require_server_state();
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);
}
void send_game_menu(
shared_ptr<Client> c,
bool is_spectator_team_list,
bool show_tournaments_only) {
void send_game_menu(shared_ptr<Client> c, bool is_spectator_team_list, bool show_tournaments_only) {
if (is_v4(c->version())) {
send_game_menu_t<TextEncoding::UTF16_ALWAYS_MARKED>(c, is_spectator_team_list, show_tournaments_only);
} 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) {
// DC v1 expects 10 lobbies in this list; DC v2 and later accept a variable
// number, but other parts of the code expect there to always be 15 lobbies.
// 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 are sent. (On Episode 3, there
// are 21 entries instead.)
// DC v1 expects 10 lobbies in this list; DC v2 and later accept a variable number, but other parts of the code
// expect there to always be 15 lobbies. 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
// are sent. (On Episode 3, there are 21 entries instead.)
auto s = c->require_server_state();
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);
}
////////////////////////////////////////////////////////////////////////////////
// lobby joining
template <typename EntryT>
void send_player_records_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Client> joining_client) {
vector<EntryT> entries;
@@ -1886,7 +1835,8 @@ void populate_lobby_data_for_client(LobbyDataT& ret, shared_ptr<const Client> c,
}
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.guild_card_number = c->login->account->account_id;
if (c->version() == Version::XB_V3) {
@@ -1900,7 +1850,8 @@ void populate_lobby_data_for_client(PlayerLobbyDataXB& ret, shared_ptr<const Cli
}
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.guild_card_number = c->login->account->account_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];
cmd_p.inventory = other_p->inventory;
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());
uint32_t name_color = s->name_color_for_client(lc);
if (name_color) {
@@ -2211,8 +2163,8 @@ void send_join_lobby_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cli
} else {
lobby_type = l->block - 1;
}
// Allow non-canonical lobby types on GC. They may work on other versions too,
// but I haven't verified which values don't crash on each version.
// Allow non-canonical lobby types on GC. They may work on other versions too, but I haven't verified which values
// don't crash on each version.
switch (c->version()) {
case Version::GC_EP3_NTE:
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));
}
void send_join_lobby_dc_nte(shared_ptr<Client> c, shared_ptr<Lobby> l,
shared_ptr<Client> joining_client = nullptr) {
void send_join_lobby_dc_nte(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Client> joining_client = nullptr) {
uint8_t command;
if (l->is_game()) {
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
// joining any lobby, set the appropriate flag and update the client config
// If the client will stop sending message box close confirmations after joining any lobby, set the appropriate flag
// and update the client config
if (c->check_flag(Client::Flag::NO_D6_AFTER_LOBBY) && !c->check_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();
}
////////////////////////////////////////////////////////////////////////////////
// Trade window
void send_execute_item_trade(shared_ptr<Client> c, const vector<ItemData>& items) {
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);
}
////////////////////////////////////////////////////////////////////////////////
// arrows
void send_arrow_update(shared_ptr<Lobby> l) {
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(
uint16_t client_id, PlayerStatsChange stat, uint32_t amount) {
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};
}
void send_change_player_hp(
std::shared_ptr<Channel> ch, uint16_t client_id, PlayerHPChange what, int16_t amount) {
void send_change_player_hp(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));
}
@@ -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) {
if (lc) {
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());
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);
}
@@ -2826,11 +2770,10 @@ void send_game_item_state(shared_ptr<Client> c) {
for (size_t floor = 0; floor < 0x0F; floor++) {
const auto& m = l->floor_item_managers.at(floor);
// It's important that these are added in increasing order of item_id (hence
// why items is a map and not an unordered_map), since the game uses binary
// search to find floor items when picking them up. If items aren't in the
// correct order, the game may fail to find an item when attempting to pick
// it up, causing "ghost items" which are visible but can't be picked up.
// It's important that these are added in increasing order of item_id (hence why items is a map and not an
// unordered_map), since the game uses binary search to find floor items when picking them up. If items aren't in
// the correct order, the game may fail to find an item when attempting to pick it up, causing "ghost items" which
// are visible but can't be picked up.
for (const auto& it : m.items) {
const auto& item = it.second;
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();
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
// manually send 6x5D commands to create them if needed
// Items on floors 0x0F and above can't be sent in the 6x6D command, so we manually send 6x5D commands to create them
// if needed
phosg::StringWriter w;
for (size_t floor = 0x0F; floor < l->floor_item_managers.size(); 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) {
// DC NTE and 11/2000 don't have this command at all; v1 has it but it doesn't
// include flags for Ultimate.
// DC NTE and 11/2000 don't have this command at all; v1 has it but it doesn't include flags for Ultimate.
if (is_pre_v1(c->version())) {
return;
} 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_tp_from_materials = from_p->inventory.tp_from_materials;
to_send.language = from_c->language();
// TODO: Deal with telepipes. Probably we should track their state via the
// subcommands sent when they're created/destroyed, but currently we don't.
// TODO: Deal with telepipes. Probably we should track their state via the subcommands sent when they're
// created/destroyed, but currently we don't.
to_send.area = from_c->floor;
to_send.technique_levels_v1 = from_p->disp.technique_levels_v1;
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);
}
////////////////////////////////////////////////////////////////////////////////
// ep3 only commands
void send_ep3_card_list_update(shared_ptr<Client> c) {
if (!c->check_flag(Client::Flag::HAS_EP3_CARD_DEFS)) {
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;
entry.item_id = tourn->get_menu_item_id();
// TODO: What does it mean for a tournament to be locked? Should we support
// that?
// TODO: Write appropriate round text (1st, 2nd, 3rd) here. This is
// nontrivial because unlike Sega's implementation, newserv does not require
// a round to completely finish before starting matches in the next round,
// TODO: What does it mean for a tournament to be locked? Should we support that?
// TODO: Write appropriate round text (1st, 2nd, 3rd) here. This is 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.
entry.state =
(tourn->get_state() == Episode3::Tournament::State::REGISTRATION)
? 0x00
: 0x05;
entry.state = (tourn->get_state() == Episode3::Tournament::State::REGISTRATION) ? 0x00 : 0x05;
// TODO: Fill in cmd.start_time here when we implement scheduled starts.
entry.name.encode(tourn->get_name(), c->language());
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(
shared_ptr<Client> c,
shared_ptr<const Episode3::Tournament> tourn,
bool is_for_spectator_team_create) {
shared_ptr<Client> c, shared_ptr<const Episode3::Tournament> tourn, bool is_for_spectator_team_create) {
S_TournamentEntryList_Ep3_E2 cmd;
cmd.players_per_team = (tourn->get_flags() & Episode3::Tournament::Flag::IS_2V2) ? 2 : 1;
size_t z = 0;
@@ -3549,9 +3481,7 @@ void send_ep3_tournament_entry_list(
}
template <typename RulesT>
void send_ep3_tournament_details_t(
shared_ptr<Client> c,
shared_ptr<const Episode3::Tournament> tourn) {
void send_ep3_tournament_details_t(shared_ptr<Client> c, shared_ptr<const Episode3::Tournament> tourn) {
S_TournamentGameDetailsBaseT_Ep3_E3<RulesT> cmd;
auto vm = tourn->get_map()->version(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);
}
void send_ep3_tournament_details(
shared_ptr<Client> c,
shared_ptr<const Episode3::Tournament> tourn) {
void send_ep3_tournament_details(shared_ptr<Client> c, shared_ptr<const Episode3::Tournament> tourn) {
if (c->version() == Version::GC_EP3_NTE) {
send_ep3_tournament_details_t<Episode3::RulesTrial>(c, tourn);
} 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
// empty when sent with E1, because there's no way for E1 to set the
// spectator count in the info window object. To account for this, we send
// a mostly-blank E3 to set the spectator count, followed by an E1 with
// the correct data.
// There is a client bug that causes the spectators list to always be empty when sent with E1, because there's no
// way for E1 to set the spectator count in the info window object. To account for this, we send a mostly-blank
// E3 to set the spectator count, followed by an E1 with the correct data.
S_TournamentGameDetailsBaseT_Ep3_E3<RulesT> cmd_E3;
cmd_E3.num_spectators = num_spectators;
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);
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);
// The value 6 here causes the client to show the "Congratulations" text
// instead of "On to the next round"
// The value 6 here causes the client to show the "Congratulations" text instead of "On to the next round"
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.winner_team_id = (match->preceding_b->winner_team == match->winner_team);
cmd.meseta_amount = meseta_reward;
cmd.meseta_reward_text.encode("You got %s meseta!", Language::ENGLISH);
if ((lc->version() != Version::GC_EP3_NTE) &&
!(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
if ((lc->version() != Version::GC_EP3_NTE) && !(s->ep3_behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
uint8_t mask_key = (phosg::random_object<uint32_t>() % 0xFF) + 1;
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;
}
// If header->mask_key isn't zero when we get here, then the command is
// already masked with a different mask_key, so unmask it first
// If header->mask_key isn't zero when we get here, then the command is already masked with a different mask_key, so
// unmask it first
if ((mask_key != 0) && (header->mask_key != 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
// are either directly masking or unmasking the command. Since this operation
// is symmetric, we don't need to split it into two cases.
// Now, exactly one of header->mask_key and mask_key should be nonzero, and we are either directly masking or
// unmasking the command. Since this operation is symmetric, we don't need to split it into two cases.
if ((header->mask_key == 0) == (mask_key == 0)) {
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");
}
// 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, the client will crash if too much
// quest data is sent at once. This is likely a bug in the TCP stack, since
// the client should apply backpressure to avoid bad situations, but we have
// to deal with it here instead.
// 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,
// the client will crash if too much quest data is sent at once. This is likely a bug in the TCP stack, since 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 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));
}
// If there are still chunks to send, track the file so the chunk
// acknowledgement handler (13 or A7) can know what to send next
// If there are still chunks to send, track the file so the chunk acknowledgement handler (13 or A7) can know what to
// send next
if (chunks_to_send < total_chunks) {
c->sending_files.emplace(filename, contents);
c->log.info_f("Opened file {}", filename);
@@ -4232,7 +4153,7 @@ void send_server_time(shared_ptr<Client> c) {
string time_str(128, 0);
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");
}
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) {
// TODO: Create a collection of all clients on the server (including those not
// in lobbies) and use that here instead
// TODO: Create a collection of all clients on the server (including those not in lobbies) and use that here instead
for (auto& l : s->all_lobbies()) {
send_change_event(l, new_event);
}
}
////////////////////////////////////////////////////////////////////////////////
// BB teams
void send_update_team_membership(shared_ptr<Client> c) {
auto team = c->team();
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");
}
// TODO: At some point we should maintain a sorted index instead of sorting
// these on-demand.
// TODO: At some point we should maintain a sorted index instead of sorting these on-demand.
vector<const TeamIndex::Team::Member*> members;
for (const auto& it : team->members) {
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) {
auto s = c->require_server_state();
// TODO: At some point we should maintain a sorted index instead of sorting
// these on-demand.
// TODO: At some point we should maintain a sorted index instead of sorting these on-demand.
auto teams = s->team_index->all();
auto rank_fn = +[](const shared_ptr<const TeamIndex::Team>& a, const shared_ptr<const TeamIndex::Team>& b) {
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;
for (const auto& reward : s->team_index->reward_definitions()) {
// In the buy menu, hide rewards that can't be bought again (that is, unique
// rewards that the team already has). In the bought menu, hide rewards that
// the team does not have or that can be bought again.
// In the buy menu, hide rewards that can't be bought again (that is, unique rewards that the team already has). In
// the bought menu, hide rewards that the team does not have or that can be bought again.
if (show_purchased != (team->has_reward(reward.key) && reward.is_unique)) {
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;
// TODO: Many of these functions should take a shared_ptr<Channel> instead of a
// shared_ptr<Client>. Refactor functions appropriately.
// TODO: Many of these functions should take a shared_ptr<Channel> instead of a shared_ptr<Client>. Refactor functions
// appropriately.
// 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
// command to one client, sometimes to everyone in a lobby, etc.)
// - For the const void* versions, the data and size arguments should not be
// independently optional - this can lead to bugs where a non-null data
// pointer is given but size is accidentally not given (e.g. if the type of
// data in the calling function is changed from string to void*).
// - There are a lot of different target types (sometimes we want to send a command to one client, sometimes to
// everyone in a lobby, etc.)
// - For the const void* versions, the data and size arguments should not be independently optional - this can lead to
// bugs where a non-null data pointer is given but size is accidentally not given (e.g. if the type of data in the
// calling function is changed from string to void*).
template <typename CmdT>
void send_or_enqueue_command(
std::shared_ptr<Client> c, uint16_t command, uint32_t flag, const CmdT& cmd) {
void send_or_enqueue_command(std::shared_ptr<Client> c, uint16_t command, uint32_t flag, const CmdT& cmd) {
if (c->game_join_command_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();
q_cmd.command = command;
q_cmd.flag = flag;
// 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 can free it when needed?
// 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
// can free it when needed?
q_cmd.data.assign(reinterpret_cast<const char*>(&cmd), sizeof(cmd));
} else {
send_command(c, command, flag, &cmd, sizeof(cmd));
@@ -125,8 +123,7 @@ void send_command_t_vt(
const StructT& data,
const std::vector<EntryT>& array_data) {
std::string all_data(reinterpret_cast<const char*>(&data), sizeof(StructT));
all_data.append(reinterpret_cast<const char*>(array_data.data()),
array_data.size() * sizeof(EntryT));
all_data.append(reinterpret_cast<const char*>(array_data.data()), array_data.size() * sizeof(EntryT));
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>
prepare_server_init_contents_console(
uint32_t server_key, uint32_t client_key, uint8_t flags);
prepare_server_init_contents_console(uint32_t server_key, uint32_t client_key, uint8_t flags);
S_ServerInitWithAfterMessageT_BB_03_9B<0xB4>
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);
void send_server_init(std::shared_ptr<Client> c, uint8_t flags);
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 override_relocations_offset,
bool use_encrypted_format);
// NOTE: The two versions of send_function_call behave differently. The version
// that takes a Channel returns immediately; the version that takes a Client
// does not return until the client has sent the response.
// NOTE: The two versions of send_function_call behave differently. The version that takes a Channel returns
// immediately; the version that takes a Client does not return until the client has sent the response.
void send_function_call(
std::shared_ptr<Channel> ch,
uint64_t client_enabled_flags,
@@ -189,17 +182,13 @@ asio::awaitable<C_ExecuteCodeResult_B3> send_function_call(
uint32_t override_relocations_offset = 0,
bool ignore_actually_runs_code_flag = false);
asio::awaitable<void> send_function_call_multi(
std::shared_ptr<Client> c,
std::unordered_set<std::shared_ptr<const CompiledFunctionCode>> codes);
std::shared_ptr<Client> c, 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<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_pc_console_split_reconnect(
std::shared_ptr<Client> c,
uint32_t address,
uint16_t pc_port,
uint16_t console_port);
std::shared_ptr<Client> c, 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_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<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(
Version version,
@@ -236,18 +226,11 @@ std::string prepare_chat_data(
const std::string& from_name,
const std::string& text,
char private_flags);
void send_chat_message_from_client(
std::shared_ptr<Channel> ch,
const std::string& text,
char private_flags);
void send_chat_message_from_client(std::shared_ptr<Channel> ch, const std::string& text, char private_flags);
void send_prepared_chat_message(
std::shared_ptr<Client> c,
uint32_t from_guild_card_number,
const std::string& prepared_data);
std::shared_ptr<Client> c, uint32_t from_guild_card_number, const std::string& prepared_data);
void send_prepared_chat_message(
std::shared_ptr<Lobby> l,
uint32_t from_guild_card_number,
const std::string& prepared_data);
std::shared_ptr<Lobby> l, uint32_t from_guild_card_number, const std::string& prepared_data);
void send_chat_message(
std::shared_ptr<Client> c,
uint32_t from_guild_card_number,
@@ -255,10 +238,7 @@ void send_chat_message(
const std::string& text,
char private_flags);
void send_simple_mail(
std::shared_ptr<Client> c,
uint32_t from_guild_card_number,
const std::string& from_name,
const std::string& text);
std::shared_ptr<Client> c, uint32_t from_guild_card_number, const std::string& from_name, const std::string& text);
void send_simple_mail(
std::shared_ptr<ServerState> s,
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_card_search_result(
std::shared_ptr<Client> c,
std::shared_ptr<Client> result,
std::shared_ptr<Lobby> result_lobby);
std::shared_ptr<Client> c, std::shared_ptr<Client> result, std::shared_ptr<Lobby> result_lobby);
void send_guild_card(
std::shared_ptr<Channel> ch,
@@ -302,12 +280,8 @@ void send_guild_card(
uint8_t section_id,
uint8_t char_class);
void send_guild_card(std::shared_ptr<Client> c, std::shared_ptr<Client> source);
void send_menu(
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_menu(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_quest_menu(
std::shared_ptr<Client> c,
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_media_update(
std::shared_ptr<Client> c,
uint32_t type,
uint32_t which,
const std::string& compressed_data);
std::shared_ptr<Client> c, uint32_t type, uint32_t which, const std::string& compressed_data);
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_set_context_token(std::shared_ptr<Client> c, uint32_t context_token);
void send_ep3_confirm_tournament_entry(
std::shared_ptr<Client> c,
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_confirm_tournament_entry(std::shared_ptr<Client> c, 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(
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);
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_tournament_match_result(std::shared_ptr<Lobby> l, uint32_t meseta_reward);
void send_ep3_tournament_details(
std::shared_ptr<Client> c,
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_tournament_details(std::shared_ptr<Client> c, 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_card_auction(std::shared_ptr<Lobby> 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;
virtual ~Server() = default;
// Generally subclasses will implement listen(), which should create a
// SocketT object (of their desired type) with a valid endpoint and call
// add_socket to actually listen on that endpoint
// Generally subclasses will implement listen(), which should create a SocketT object (of their desired type) with a
// valid endpoint and call add_socket to actually listen on that endpoint
void add_socket(std::shared_ptr<SocketT> sock) {
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);
@@ -56,19 +55,16 @@ protected:
std::unordered_set<std::shared_ptr<SocketT>> sockets;
std::unordered_set<std::shared_ptr<ClientT>> clients;
// create_client is called when a new socket is opened. It should create (and
// return) the ClientT object, or may close client_sock and return nullptr if
// it decides to reject the connection. create_client should NOT send or
// create_client is called when a new socket is opened. It should create (and return) the ClientT object, or may
// close client_sock and return nullptr if it decides to reject the connection. create_client should NOT send or
// receive any data, hence it is not a coroutine.
[[nodiscard]] virtual std::shared_ptr<ClientT> create_client(
std::shared_ptr<SocketT> sock, asio::ip::tcp::socket&& client_sock) = 0;
// handle_client is called immediately after create_client if create_client
// did not return nullptr. It should handle all sending and receiving of data
// on the client's connection.
// handle_client is called immediately after create_client if create_client did not return nullptr. It should handle
// all sending and receiving of data on the client's connection.
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
// after it has disconnected (hence, it cannot assume that it can send or
// receive any data). Additionally, the client has already been removed from
// destroy_client is called when the client is about to be destroyed, often after it has disconnected (hence, it
// cannot assume that it can send or receive any data). Additionally, the client has already been removed from
// this->clients at the time this is called.
virtual asio::awaitable<void> destroy_client(std::shared_ptr<ClientT> c) {
(void)c;
+3 -6
View File
@@ -16,8 +16,7 @@
using namespace std;
ServerShell::ServerShell(shared_ptr<ServerState> state)
: state(state),
th(&ServerShell::thread_fn, this) {}
: state(state), th(&ServerShell::thread_fn, this) {}
ServerShell::~ServerShell() {
if (this->th.joinable()) {
@@ -34,10 +33,8 @@ void ServerShell::thread_fn() {
try {
command = phosg::fgets(stdin);
} catch (const phosg::io_error& e) {
// Cygwin sometimes causes fgets() to fail with errno -1 when the
// terminal window is resized. We ignore these events unless the read
// failed immediately (which probably means it would fail again if we
// retried immediately).
// Cygwin sometimes causes fgets() to fail with errno -1 when the terminal window is resized. We ignore these
// events unless the read failed immediately (which probably means it would fail again if we retried immediately)
if (phosg::now() - read_start_usecs < 1000000 || e.error != -1) {
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) {
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->base_probability = json.get_int("BaseProbability", 0);
this->probability_upgrade = json.get_int("ProbabilityUpgrade", 0);
@@ -60,7 +61,8 @@ ServerState::QuestF960Result::QuestF960Result(const phosg::JSON& json, shared_pt
try {
this->results[day].emplace_back(name_index->parse_item_description(item_it->as_string()));
} 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) {
try {
auto l = this->find_lobby(c->preferred_lobby_id);
if (l &&
!l->is_game() &&
l->check_flag(Lobby::Flag::PUBLIC) &&
l->version_is_allowed(c->version())) {
if (l && !l->is_game() && l->check_flag(Lobby::Flag::PUBLIC) && l->version_is_allowed(c->version())) {
l->add_client(c);
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)) {
try {
auto l = this->find_lobby(lobby_id);
if (l &&
!l->is_game() &&
l->check_flag(Lobby::Flag::PUBLIC) &&
l->version_is_allowed(c->version())) {
if (l && !l->is_game() && l->check_flag(Lobby::Flag::PUBLIC) && l->version_is_allowed(c->version())) {
l->add_client(c);
added_to_lobby = l;
break;
@@ -135,10 +131,7 @@ void ServerState::remove_client_from_lobby(shared_ptr<Client> c) {
}
bool ServerState::change_client_lobby(
shared_ptr<Client> c,
shared_ptr<Lobby> new_lobby,
bool send_join_notification,
ssize_t required_client_id) {
shared_ptr<Client> c, shared_ptr<Lobby> new_lobby, bool send_join_notification, ssize_t required_client_id) {
uint8_t old_lobby_client_id = c->lobby_client_id;
auto current_lobby = c->lobby.lock();
@@ -161,8 +154,7 @@ bool ServerState::change_client_lobby(
return true;
}
void ServerState::send_lobby_join_notifications(shared_ptr<Lobby> l,
shared_ptr<Client> joining_client) {
void ServerState::send_lobby_join_notifications(shared_ptr<Lobby> l, shared_ptr<Client> joining_client) {
for (auto& other_client : l->clients) {
if (!other_client) {
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) {
// WARNING: There are multiple callsites where we assume this function never
// returns a client that isn't in any lobby. If this behavior changes, we will
// need to audit all callsites to ensure correctness.
// WARNING: There are multiple callsites where we assume this function never returns a client that isn't in any
// lobby. If this behavior changes, we will need to audit all callsites to ensure correctness.
if ((account_id == 0) && identifier) {
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);
if (peer_channel) {
// This is used during replays; the "client" will ignore this and
// reconnect via another PeerChannel
// This is used during replays; the "client" will ignore this and reconnect via another PeerChannel
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)) {
return this->common_item_sets.at("common-table-v3-v4");
} 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) {
auto spc = make_shared<PortConfiguration>(pc);
if (!this->name_to_port_config.emplace(spc->name, spc).second) {
// Note: This is a logic_error instead of a runtime_error because
// port_configs comes from a JSON map, so the names should already all be
// unique. In contrast, the user can define port configurations with the
// same number while still writing valid JSON, so only one of these cases
// can reasonably occur as a result of user behavior.
// Note: This is a logic_error instead of a runtime_error because port_configs comes from a JSON map, so the
// names should already all be unique. In contrast, the user can define port configurations with the 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");
}
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(
const string& patch_index_filename,
const string& gsl_filename,
const string& bb_directory_filename) const {
const string& patch_index_filename, const string& gsl_filename, const string& bb_directory_filename) const {
if (this->bb_patch_file_index) {
// 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
const string& effective_gsl_filename = gsl_filename.empty() ? patch_index_filename : gsl_filename;
try {
// TODO: It's kinda not great that we copy the data here; find a way to
// avoid doing this (also in the below case)
// TODO: It's kinda not great that we copy the data here; find a way to avoid doing this (also in the below case)
return make_shared<string>(this->bb_data_gsl->get_copy(effective_gsl_filename));
} catch (const out_of_range&) {
}
@@ -782,8 +768,7 @@ void ServerState::load_config_early() {
try {
this->local_address = this->all_addresses.at(local_address_str);
string addr_str = string_for_address(this->local_address);
config_log.info_f("Added local address: {} ({})", addr_str,
local_address_str);
config_log.info_f("Added local address: {} ({})", addr_str, local_address_str);
} catch (const out_of_range&) {
this->local_address = address_for_string(local_address_str.c_str());
config_log.info_f("Added local address: {}", local_address_str);
@@ -810,8 +795,7 @@ void ServerState::load_config_early() {
try {
this->external_address = this->all_addresses.at(external_address_str);
string addr_str = string_for_address(this->external_address);
config_log.info_f("Added external address: {} ({})", addr_str,
external_address_str);
config_log.info_f("Added external address: {} ({})", addr_str, external_address_str);
} catch (const out_of_range&) {
this->external_address = address_for_string(external_address_str.c_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);
config_log.warning_f("External address not specified; using {} as default", addr_str);
} 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)
: decompressed_gvm_data.size();
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()) {
compressed_gvm_data = prs_compress_optimal(decompressed_gvm_data);
}
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(
Ep3LobbyBannerEntry{.type = static_cast<uint32_t>(it->at(0).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"));
} catch (const exception& e) {
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");
@@ -1455,7 +1445,8 @@ void ServerState::load_config_late() {
auto& list = it->as_list();
size_t price = list.at(0)->as_int();
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) {
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&) {
}
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")) {
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;
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
// variants. Figure this out and load those maps too.
// TODO: Ep3 NTE loads map_city00_on, but it appears there are 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 enemies_data = this->load_map_file(Version::GC_EP3, "map_city_on_battle_e.dat");
if (objects_data || enemies_data) {
@@ -1965,8 +1956,6 @@ void ServerState::load_drop_tables() {
size_t ext_offset = filename.rfind('.');
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")) {
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))));
+5 -11
View File
@@ -42,10 +42,9 @@ struct PortConfiguration {
};
struct CheatFlags {
// This structure describes which behaviors are considered cheating (that is,
// require cheat mode to be enabled or the user to have the CHEAT_ANYWHERE
// account flag). A false value here means that that particular behavior is
// NOT cheating, so cheat mode is NOT required.
// This structure describes which behaviors are considered cheating (that is, require cheat mode to be enabled or the
// user to have the CHEAT_ANYWHERE account flag). A false value here means that that particular behavior is NOT
// cheating, so cheat mode is NOT required.
bool create_items = true;
bool edit_section_id = 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);
std::shared_ptr<Client> find_client(
const std::string* identifier = nullptr,
uint64_t account_id = 0,
std::shared_ptr<Lobby> l = nullptr);
const std::string* identifier = nullptr, uint64_t account_id = 0, std::shared_ptr<Lobby> l = nullptr);
uint32_t connect_address_for_client(std::shared_ptr<Client> c) 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(
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(
Episode episode,
GameMode mode,
Difficulty difficulty,
const Variations& variations);
Episode episode, GameMode mode, Difficulty difficulty, const Variations& variations);
void create_default_lobbies();
void collect_network_addresses();
+4 -9
View File
@@ -49,13 +49,8 @@ shared_ptr<Client> ShellCommand::Args::get_proxy_client() const {
return c;
}
ShellCommand::ShellCommand(
const char* name,
const char* help_text,
asio::awaitable<deque<string>> (*run)(Args&))
: name(name),
help_text(help_text),
run(run) {
ShellCommand::ShellCommand(const char* name, 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_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));
tokens.erase(tokens.begin());
// Do all the parsing first, then the updates afterward, so we won't
// partially update the account if parsing a later option fails
// Do all the parsing first, then the updates afterward, so we won't partially update the account if parsing a
// later option fails
int64_t new_ep3_current_meseta = -1;
int64_t new_ep3_total_meseta = -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&));
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);
};
+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 = {
"Viridia", "Greennill", "Skyly", "Bluefull", "Purplenum",
"Pinkal", "Redria", "Oran", "Yellowboze", "Whitill"};
"Viridia", "Greennill", "Skyly", "Bluefull", "Purplenum", "Pinkal", "Redria", "Oran", "Yellowboze", "Whitill"};
static const array<const char*, 10> section_id_to_abbreviation = {
"Vir", "Grn", "Sky", "Blu", "Prp", "Pnk", "Red", "Orn", "Ylw", "Wht"};
const unordered_map<string, uint8_t> name_to_section_id({
{"viridia", 0},
{"greennill", 1},
{"greenill", 1},
{"skyly", 2},
{"bluefull", 3},
{"purplenum", 4},
{"pinkal", 5},
{"redria", 6},
{"oran", 7},
{"yellowboze", 8},
{"whitill", 9},
const unordered_map<string, uint8_t> name_to_section_id({{"viridia", 0},
// Greennill is spelled Greenill in some places, so we accept both spellings
{"greennill", 1}, {"greenill", 1}, {"skyly", 2}, {"bluefull", 3}, {"purplenum", 4}, {"pinkal", 5}, {"redria", 6},
{"oran", 7}, {"yellowboze", 8}, {"whitill", 9},
// Shortcuts for chat commands
{"b", 3},
{"g", 1},
{"o", 7},
{"pi", 5},
{"pu", 4},
{"r", 6},
{"s", 2},
{"v", 0},
{"w", 9},
{"y", 8},
});
{"b", 3}, {"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 = {
"none", "xmas", "none", "val", "easter", "hallo", "sonic", "newyear",
"summer", "white", "wedding", "fall", "s-spring", "s-summer", "spring"};
const unordered_map<string, uint8_t> name_to_lobby_event({
{"none", 0},
{"xmas", 1},
{"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<string, uint8_t> name_to_lobby_event = {
{"none", 0}, {"xmas", 1}, {"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({
{0x00, "normal"},
{0x0F, "inormal"},
{0x10, "ipc"},
{0x11, "iball"},
{0x67, "cave2u"},
{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<uint8_t, string> lobby_type_to_name = {
{0x00, "normal"}, {0x0F, "inormal"}, {0x10, "ipc"}, {0x11, "iball"}, {0x67, "cave2u"}, {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({
{"normal", 0x00},
{"inormal", 0x0F},
{"ipc", 0x10},
{"iball", 0x11},
{"cave1", 0xD4},
{"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 unordered_map<string, uint8_t> name_to_lobby_type = {
{"normal", 0x00}, {"inormal", 0x0F}, {"ipc", 0x10}, {"iball", 0x11}, {"cave1", 0xD4}, {"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({
"ninja",
"rico",
"sonic",
"knuckles",
"tails",
"flowen",
"elly",
"momoka",
"irene",
"guild",
"nurse",
});
const vector<string> npc_id_to_name = {
"ninja", "rico", "sonic", "knuckles", "tails", "flowen", "elly", "momoka", "irene", "guild", "nurse"};
const unordered_map<string, uint8_t> name_to_npc_id = {
{"ninja", 0},
{"rico", 1},
{"sonic", 2},
{"knuckles", 3},
{"tails", 4},
{"flowen", 5},
{"elly", 6},
{"momoka", 7},
{"irene", 8},
{"guild", 9},
{"nurse", 10},
};
{"ninja", 0}, {"rico", 1}, {"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) {
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) {
static const array<const char*, 12> names = {
"HUmar",
"HUnewearl",
"HUcast",
"RAmar",
"RAcast",
"RAcaseal",
"FOmarl",
"FOnewm",
"FOnewearl",
"HUcaseal",
"FOmar",
"RAmarl",
};
"HUmar", "HUnewearl", "HUcast", "RAmar", "RAcast", "RAcaseal", "FOmarl", "FOnewm", "FOnewearl", "HUcaseal",
"FOmar", "RAmarl"};
try {
return names.at(cls);
} 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) {
static const array<const char*, 12> names = {
"HUmr",
"HUnl",
"HUct",
"RAmr",
"RAct",
"RAcl",
"FOml",
"FOnm",
"FOnl",
"HUcl",
"FOmr",
"RAml",
};
"HUmr", "HUnl", "HUct", "RAmr", "RAct", "RAcl", "FOml", "FOnm", "FOnl", "HUcl", "FOmr", "RAml"};
try {
return names.at(cls);
} catch (const out_of_range&) {
@@ -489,8 +366,7 @@ bool char_class_is_force(uint8_t cls) {
}
const char* name_for_difficulty(Difficulty difficulty) {
static const array<const char*, 4> names = {
"Normal", "Hard", "Very Hard", "Ultimate"};
static const array<const char*, 4> names = {"Normal", "Hard", "Very Hard", "Ultimate"};
try {
return names.at(static_cast<size_t>(difficulty));
} catch (const out_of_range&) {
@@ -499,8 +375,7 @@ const char* name_for_difficulty(Difficulty difficulty) {
}
const char* token_name_for_difficulty(Difficulty difficulty) {
static const array<const char*, 4> names = {
"Normal", "Hard", "VeryHard", "Ultimate"};
static const array<const char*, 4> names = {"Normal", "Hard", "VeryHard", "Ultimate"};
try {
return names.at(static_cast<size_t>(difficulty));
} catch (const out_of_range&) {
@@ -518,14 +393,8 @@ char abbreviation_for_difficulty(Difficulty difficulty) {
}
const char* name_for_language(Language language) {
array<const char*, 8> names = {{"Japanese",
"English",
"German",
"French",
"Spanish",
"Simplified Chinese",
"Traditional Chinese",
"Korean"}};
array<const char*, 8> names = {
"Japanese", "English", "German", "French", "Spanish", "Simplified Chinese", "Traditional Chinese", "Korean"};
size_t lang_index = static_cast<size_t>(language);
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 = {
"foie", "gifoie", "rafoie",
"barta", "gibarta", "rabarta",
"zonde", "gizonde", "razonde",
"grants", "deband", "jellen", "zalure", "shifta",
"ryuker", "resta", "anti", "reverser", "megid"};
"foie", "gifoie", "rafoie", "barta", "gibarta", "rabarta", "zonde", "gizonde", "razonde", "grants", "deband",
"jellen", "zalure", "shifta", "ryuker", "resta", "anti", "reverser", "megid"};
const unordered_map<string, uint8_t> name_to_tech_id({
{"foie", 0},
{"gifoie", 1},
{"rafoie", 2},
{"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 unordered_map<string, uint8_t> name_to_tech_id = {
{"foie", 0}, {"gifoie", 1}, {"rafoie", 2}, {"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) {
try {
@@ -652,49 +501,15 @@ uint8_t technique_for_name(const string& name) {
return 0xFF;
}
const vector<const char*> name_for_mag_color({
/* 00 */ "red",
/* 01 */ "blue",
/* 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 vector<const char*> name_for_mag_color = {
"red", "blue", "yellow", "green", "purple", "black", "white", "cyan", "brown", "orange", "light-blue", "olive",
"turquoise", "fuchsia", "grey", "cream", "pink", "dark-green", "costume"};
const unordered_map<string, uint8_t> mag_color_for_name({
{"red", 0x00},
{"blue", 0x01},
{"yellow", 0x02},
{"green", 0x03},
{"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},
});
const unordered_map<string, uint8_t> mag_color_for_name = {
{"red", 0x00}, {"blue", 0x01}, {"yellow", 0x02}, {"green", 0x03}, {"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_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) {
static constexpr uint8_t flags[12] = {
0x25, 0x2A, 0x31, 0x45, 0x51, 0x52, 0x86, 0x89, 0x8A, 0x32, 0x85, 0x46};
static constexpr uint8_t flags[12] = {0x25, 0x2A, 0x31, 0x45, 0x51, 0x52, 0x86, 0x89, 0x8A, 0x32, 0x85, 0x46};
if (char_class >= 12) {
throw runtime_error("invalid character class");
}
+6 -12
View File
@@ -14,9 +14,7 @@
using namespace std;
TeamIndex::Team::Member::Member(const phosg::JSON& json)
: flags(json.get_int("Flags", 0)),
points(json.get_int("Points", 0)),
name(json.get_string("Name", "")) {
: flags(json.get_int("Flags", 0)), points(json.get_int("Points", 0)), name(json.get_string("Name", "")) {
try {
this->account_id = json.get_int("AccountID");
} catch (const out_of_range&) {
@@ -26,12 +24,8 @@ TeamIndex::Team::Member::Member(const phosg::JSON& json)
}
phosg::JSON TeamIndex::Team::Member::json() const {
return phosg::JSON::dict({
{"AccountID", this->account_id},
{"Flags", this->flags},
{"Points", this->points},
{"Name", this->name},
});
return phosg::JSON::dict(
{{"AccountID", this->account_id}, {"Flags", this->flags}, {"Points", this->points}, {"Name", this->name}});
}
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);
throw runtime_error("team name is already in use");
}
for (const auto& it : team->members) {
if (!this->account_id_to_team.emplace(it.second.account_id, team).second) {
for (const auto& [_, member] : team->members) {
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",
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 {
// Only 0x00000001 and 0x00000002 are used by the client; the rest are
// free to be used however the server chooses.
// Only 0x00000001 and 0x00000002 are used by the client; the rest are free to be used however the server chooses
NONE = 0x00000000,
TEAM_FLAG = 0x00000001,
DRESSING_ROOM = 0x00000002,
@@ -130,7 +129,8 @@ public:
std::shared_ptr<const Team> get_by_account_id(uint32_t account_id) 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 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 size_t TextTranscoder::FAILURE_RESULT = static_cast<size_t>(-1);
TextTranscoder::TextTranscoder(const char* to, const char* from)
: ic(iconv_open(to, from)) {
TextTranscoder::TextTranscoder(const char* to, const char* from) : ic(iconv_open(to, from)) {
if (ic == this->INVALID_IC) {
string error_str = phosg::string_for_error(errno);
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_written = reinterpret_cast<char*>(dest) - reinterpret_cast<char*>(orig_dest);
return Result{
.bytes_read = bytes_read,
.bytes_written = bytes_written,
};
return Result{.bytes_read = bytes_read, .bytes_written = bytes_written};
}
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 {
// Sega implemented some nonstandard Shift-JIS characters on PSO GC (and
// probably XB as well): the heart symbol, encoded as F040, and the PSO font,
// encoded as F041-F064. Understandably, libiconv doesn't know what to do
// with these because they're not actually part of Shift-JIS, so we have to
// handle them manually here. We convert them to actual UTF-8 symbols:
// F040 (heart symbol) -> U+2665 (heart suit symbol)
// F041 (PSO font number 0) -> 24EA (circled digit zero)
// F042-F04A (PSO font numbers 1-9) -> 2460-2468 (circled digits 1-9)
// F04B-F064 (PSO font letters) -> 1D4D0-1D4E9 (script letters A-Z)
// Sega implemented some nonstandard Shift-JIS characters on PSO GC (and probably XB as well): the heart symbol,
// encoded as F040, and the PSO font, encoded as F041-F064. Understandably, libiconv doesn't know what to do with
// these because they're not actually part of Shift-JIS, so we have to handle them manually here. We convert them to
// actual UTF-8 symbols:
// F040 (heart symbol) -> U+2665 (heart suit symbol)
// F041 (PSO font number 0) -> 24EA (circled digit zero)
// F042-F04A (PSO font numbers 1-9) -> 2460-2468 (circled digits 1-9)
// F04B-F064 (PSO font letters) -> 1D4D0-1D4E9 (script letters A-Z)
const uint8_t* src = reinterpret_cast<const uint8_t*>(*vsrc);
if ((*size < 2) || (src[0] != 0xF0)) {
@@ -394,8 +389,7 @@ size_t add_color_inplace(char* a, size_t max_chars) {
a++;
}
*d = 0;
// TODO: we should clear the chars after the null if the new string is shorter
// than the original
// TODO: we should clear the chars after the null if the new string is shorter than the original
return d - orig_d;
}
+7 -8
View File
@@ -155,8 +155,8 @@ struct parray {
if (index >= Count) {
throw std::out_of_range("array index out of bounds");
}
// Note: This looks really dumb, but apparently works around an issue in GCC
// that causes a "returning address of temporary" error here.
// Note: This looks really dumb, but apparently works around an issue in GCC that causes a "returning address of
// temporary" error here.
return *&this->items[index];
}
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
// similar, because we want to enforce that the correct encoding is used.
// This struct does not inherit from parray, even though it's semantically similar, because we want to enforce that the
// correct encoding is used.
template <
TextEncoding Encoding,
size_t Chars,
@@ -737,8 +737,7 @@ struct pstring {
return this->data[pos];
}
// Note: The contents of a pstring do not have to be null-terminated, so there
// is no function.
// Note: The contents of a pstring do not have to be null-terminated, so there is no .c_str() function.
} __attribute__((packed));
// 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);
void add_color_inplace(std::string& s);
// remove_color does the opposite of add_color (it changes \t into $, for
// example). strip_color is irreversible; it deletes color escape sequences.
// remove_color does the opposite of add_color (it changes \t into $, for example). strip_color is irreversible; it
// deletes color escape sequences.
void remove_color(phosg::StringWriter& w, const char* src, size_t max_input_chars);
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);
phosg::StringReader r(decompressed);
// Annoyingly, there doesn't appear to be any bounds-checking on the language
// functions, so there are no counts of strings in each collection. We have to
// figure out where each collection ends by collecting all the relevant
// Annoyingly, there doesn't appear to be any bounds-checking on the language functions, so there are no counts of
// strings in each collection. We have to figure out where each collection ends by collecting all the relevant
// offsets in the file instead.
::set<uint32_t> used_offsets;
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()) {
auto& collection = this->collections.emplace_back();
uint32_t first_string_offset_offset = collection_offsets_r.get_u32l();
// TODO: Apparently the early formats do actually include keyboards, but
// they're just in the middle of the collections list. Sigh...
// TODO: Apparently the early formats do actually include keyboards, but they're just in the middle of the
// collections list. Sigh...
try {
for (uint32_t string_offset_offset = first_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);
phosg::StringReader r(decompressed);
// Annoyingly, there doesn't appear to be any bounds-checking on the language
// functions, so there are no counts of strings in each collection. We have to
// figure out where each collection ends by collecting all the relevant
// Annoyingly, there doesn't appear to be any bounds-checking on the language functions, so there are no counts of
// strings in each collection. We have to figure out where each collection ends by collecting all the relevant
// offsets in the file instead.
::set<uint32_t> used_offsets;
used_offsets.emplace(r.size() - 8);
@@ -449,8 +447,7 @@ pair<string, string> BinaryTextAndKeyboardsSet::serialize_t(bool is_sjis) const
}
TextIndex::TextIndex(
const string& directory,
function<shared_ptr<const string>(Version, const string&)> get_patch_file)
const string& directory, function<shared_ptr<const string>(Version, const string&)> get_patch_file)
: log("[TextIndex] ", static_game_data_log.min_level) {
if (!directory.empty()) {
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) {
// For versions that don't support send_function_call by default, we need
// to set the specific_version based on sub_version. Fortunately, all
// versions that share sub_version values also support send_function_call,
// so for those versions we get the specific_version later by sending the
// VersionDetectDC, VersionDetectGC, or VersionDetectXB call.
// For versions that don't support send_function_call by default, we need to set the specific_version based on
// sub_version. Fortunately, all versions that share sub_version values also support send_function_call, so for those
// versions we get the specific_version later by sending VersionDetectDC, VersionDetectGC, or VersionDetectXB.
switch (version) {
case Version::DC_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:
switch (sub_version) {
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:
return SPECIFIC_VERSION_DC_V1_US; // 1OEF
case 0x22:
@@ -301,8 +299,7 @@ uint64_t generate_random_hardware_id(Version version) {
case Version::PC_V2:
return 0x0000FFFFFFFFFFFF;
case Version::GC_NTE:
// On GC NTE, the low byte is uninitialized memory from the TProtocol
// constructor's stack
// On GC NTE, the low byte is uninitialized memory from the TProtocol constructor's stack
return phosg::random_object<uint8_t>();
case Version::GC_V3:
case Version::GC_EP3_NTE:
+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");
array<const WordSelectSet*, NUM_NON_PATCH_VERSIONS> ws_sets = {
&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, &gc_ep3_ws, &xb_v3_ws, &bb_v4_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,
&gc_ep3_ws, &xb_v3_ws, &bb_v4_ws};
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);
@@ -297,9 +296,7 @@ void WordSelectTable::validate(const WordSelectMessage& msg, Version version) co
}
WordSelectMessage WordSelectTable::translate(
const WordSelectMessage& msg,
Version from_version,
Version to_version) const {
const WordSelectMessage& msg, Version from_version, Version to_version) const {
const auto& index = this->tokens_for_version(from_version);
WordSelectMessage ret;