reformat remaining files
This commit is contained in:
+14
-28
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+6
-8
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+179
-264
@@ -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{
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user