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;
|
||||
|
||||
@@ -7,11 +7,10 @@
|
||||
.long_desc ""
|
||||
|
||||
start:
|
||||
// Create quest opcode handlers for F9FE to call flush_code and F9FF to call
|
||||
// the copied code. Fortunately, quest_call_l leaves the byteswapped value of
|
||||
// the opcode argument in r4, so as long as the address ends with 00, it will
|
||||
// be valid as the size argument to flush_code. We'll end up flushing many
|
||||
// more bytes than needed, but this isn't a problem.
|
||||
// Create quest opcode handlers for F9FE to call flush_code and F9FF to call the copied code. Fortunately,
|
||||
// quest_call_l leaves the byteswapped value of the opcode argument in r4, so as long as the address ends with 00,
|
||||
// it will be valid as the size argument to flush_code. We'll end up flushing many more bytes than needed, but this
|
||||
// isn't a problem.
|
||||
leti r3, 0x80004000 // dest addr
|
||||
write4 0x804C81C8, 0x801F2A14 // quest_call_l
|
||||
write4 0x804C81CC, 0x8000C274 // flush_code
|
||||
@@ -40,10 +39,9 @@ copy_done:
|
||||
// Call the copied native code
|
||||
.data F9FF
|
||||
|
||||
// This script runs on the first frame during the quest loading procedure,
|
||||
// but this procedure is started from the lobby overview, not from a game!
|
||||
// To make the result of loading a quest sane, we need to set some extra
|
||||
// state that will take effect when loading is done.
|
||||
// This script runs on the first frame during the quest loading procedure, but this procedure is started from the
|
||||
// lobby overview, not from a game! To make the result of loading a quest sane, we need to set some extra state that
|
||||
// will take effect when loading is done.
|
||||
ba_initial_floor 17 // Make player spawn in lobby (for one frame)
|
||||
write2 0x805D5CE8, 1 // Leave "game" immediately (sends 98)
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
.long_desc ""
|
||||
|
||||
start:
|
||||
// This script is identical to q88530-gc-e.bin.txt, except the addresses are
|
||||
// changed to be suitable for JP v1.5.
|
||||
// This script is identical to q88530-gc-e.bin.txt, except the addresses are changed to be suitable for JP v1.5.
|
||||
leti r3, 0x80004000
|
||||
|
||||
write4 0x804C88F0, 0x801F29C0
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
.long_desc ""
|
||||
|
||||
start:
|
||||
// This script is identical to q88530-gc-e.bin.txt, except the addresses are
|
||||
// changed to be suitable for US Ep3.
|
||||
// This script is identical to q88530-gc-e.bin.txt, except the addresses are changed to be suitable for US Ep3.
|
||||
leti r3, 0x80004000
|
||||
|
||||
write4 0x80452A4C, 0x80109B28
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
.long_desc ""
|
||||
|
||||
start:
|
||||
// This script is identical to q88530-gc-e.bin.txt, except the addresses are
|
||||
// changed to be suitable for EU Ep3.
|
||||
// This script is identical to q88530-gc-e.bin.txt, except the addresses are changed to be suitable for EU Ep3.
|
||||
leti r3, 0x80004000
|
||||
|
||||
write4 0x80454E04, 0x80109FB4
|
||||
|
||||
@@ -1,32 +1,26 @@
|
||||
// This file documents newserv's quest assembler syntax and format. This is a
|
||||
// slightly modified copy of the English version of Sega's Lost HEAT SWORD quest
|
||||
// for PSO GC.
|
||||
// This file documents newserv's quest assembler syntax and format. This is a slightly modified copy of the English
|
||||
// version of Sega's Lost HEAT SWORD quest for PSO GC.
|
||||
|
||||
// Generally the metadata directives should appear before the quest's code.
|
||||
// These specify the quest's name, description, and other information.
|
||||
// Generally the metadata directives should appear before the quest's code. These specify the quest's name,
|
||||
// description, and other information.
|
||||
|
||||
// The .version directive specifies which version of the game the quest is for.
|
||||
// The values are DC_NTE, DC_11_2000, DC_V1, DC_V2, PC_V2, GC_NTE, GC_V3,
|
||||
// GC_EP3_NTE, GC_EP3, XB_V3, and BB_V4. This determines which set of opcodes
|
||||
// to use during compilation, and also specifies the header format and string
|
||||
// encoding. This does not affect where the quest appears in menus, so for
|
||||
// versions that use the same opcodes, headers, and string encodings, it is OK
|
||||
// to use a symbolic link (hence q058-xb-e.bin.txt is a link to this file).
|
||||
// The .version directive specifies which version of the game the quest is for. The values are DC_NTE, DC_11_2000,
|
||||
// DC_V1, DC_V2, PC_V2, GC_NTE, GC_V3, GC_EP3_NTE, GC_EP3, XB_V3, and BB_V4. This determines which set of opcodes to
|
||||
// use during compilation, and also specifies the header format and string encoding. This does not affect where the
|
||||
// quest appears in menus, so for versions that use the same opcodes, headers, and string encodings, it is OK to use a
|
||||
// symbolic link (hence q058-xb-e.bin.txt is a link to this file).
|
||||
.version GC_V3
|
||||
|
||||
// The .quest_num directive specifies the internal number of the quest. This
|
||||
// has no meaning for online quests, though it's recommended for this value to
|
||||
// match the number in the filename. For download quests, the game deduplicates
|
||||
// quest files with the same number, so download quests should all have unique
|
||||
// numbers in this field. On Episodes 1&2, this field must be in the range
|
||||
// 0-255; on other versions, it can be 0-65535, but generally numbers less than
|
||||
// 1000 are recommended.
|
||||
// The .quest_num directive specifies the internal number of the quest. This has no meaning for online quests, though
|
||||
// it's recommended for this value to match the number in the filename. For download quests, the game deduplicates
|
||||
// quest files with the same number, so download quests should all have unique numbers in this field. On Episodes 1&2,
|
||||
// this field must be in the range 0-255; on other versions, it can be 0-65535, but generally numbers less than 1000
|
||||
// are recommended.
|
||||
.quest_num 58
|
||||
|
||||
// The .language field specifies the internal language of the quest. On console
|
||||
// versions (DC, GC, and XB), this affects how strings are encoded - Japanese
|
||||
// uses Shift-JIS and other languages use ISO8859. (On PC V2 and BB, UTF-16 is
|
||||
// used for strings in all languages.) The language values are:
|
||||
// The .language field specifies the internal language of the quest. On console versions (DC, GC, and XB), this affects
|
||||
// how strings are encoded - Japanese uses Shift-JIS and other languages use ISO8859. (On PC V2 and BB, UTF-16 is used
|
||||
// for strings in all languages.) The language values are:
|
||||
// J = Japanese
|
||||
// E = English
|
||||
// G = German
|
||||
@@ -37,96 +31,89 @@
|
||||
// K = Korean
|
||||
.language E
|
||||
|
||||
// The .episode directive specifies the quest's episode. The server ignores this
|
||||
// if a set_episode or set_episode2 opcode is present in the code following the
|
||||
// start label.
|
||||
// The .episode directive specifies the quest's episode. The server ignores this if a set_episode or set_episode2
|
||||
// opcode is present in the code following the start label.
|
||||
.episode Episode1
|
||||
|
||||
// These directives specify the quest's name, short description, and long
|
||||
// description. Non-ASCII characters can be used here and in the script below;
|
||||
// this entire file is encoded as UTF-8 and strings are transcoded to the
|
||||
// encoding the client expects based on the .version and .language directives.
|
||||
// Common escape codes (e.g. \n for a newline) are supported in these strings.
|
||||
// These directives specify the quest's name, short description, and long description. Non-ASCII characters can be used
|
||||
// here and in the script below; this entire file is encoded as UTF-8 and strings are transcoded to the encoding the
|
||||
// client expects based on the .version and .language directives. Common escape codes (e.g. \n for a newline) are
|
||||
// supported in these strings.
|
||||
.name "Lost HEAT SWORD"
|
||||
.short_desc "Retrieve a\nweapon from\na Dragon!"
|
||||
.long_desc "Client: Hopkins, hunter\nQuest:\n My weapon was taken\n from me when I was\n fighting a Dragon.\nReward: ??? Meseta\n\n\n"
|
||||
|
||||
// On BB, quests may specify a maximum number of players with this directive. If
|
||||
// not given, the default is 4. On non-BB versions, this directive is ignored.
|
||||
// On BB, quests may specify a maximum number of players with this directive. If not given, the default is 4. On non-BB
|
||||
// versions, this directive is ignored.
|
||||
// .max_players 4
|
||||
|
||||
// On BB, quests may be joinable while in progress. This directive enables that
|
||||
// capability.
|
||||
// On BB, quests may be joinable while in progress. This directive enables that capability.
|
||||
// .joinable
|
||||
|
||||
// On BB, quests that create items via the script must specify which items are
|
||||
// allowed to be created. To do so, use this directive one or more times, which
|
||||
// instructs the server to allow creation of that item. These masks can specify
|
||||
// each byte of the item's data1 as ranges, to allow for parameters in the item
|
||||
// data. For example, this directive allows the quest to create Trifluids
|
||||
// (030102) with stack sizes of 1-10:
|
||||
// On BB, quests that create items via the script must specify which items are allowed to be created. To do so, use
|
||||
// this directive one or more times, which instructs the server to allow creation of that item. These masks can specify
|
||||
// each byte of the item's data1 as ranges, to allow for parameters in the item data. For example, this directive
|
||||
// allows the quest to create Trifluids (030102) with stack sizes of 1-10:
|
||||
// .allow_create_item 0301020000[01-0A]000000000000
|
||||
// Another example: this directive allows the quest to create any weapon in the
|
||||
// basic rifle series, not including the rares in that series, with a grind
|
||||
// value of up to 5 and up to two bonuses between 0 and 30% each:
|
||||
// Another example: this directive allows the quest to create any weapon in the basic rifle series, not including the
|
||||
// rares in that series, with a grind value of up to 5 and up to two bonuses between 0 and 30% each:
|
||||
// .allow_create_item 0007[00-04][00-05]0000[00-05][00-1E][00-05][00-1E]0000
|
||||
|
||||
// The quest script begins after the header directives. A quest script is a
|
||||
// sequence of opcodes, and labels denoting positions within that sequence that
|
||||
// can be jumped to or called like a function. All labels have names, and some
|
||||
// have numbers. (In the compiled format, labels have only numbers and no names;
|
||||
// during compilation, each label that doesn't have a number is assigned a
|
||||
// number that isn't in use by another label.) To explicitly specify a label
|
||||
// number (for example, if an object or NPC refers to a label by number), use an
|
||||
// @ sign followed by the desired number. Note that numbers can be specified in
|
||||
// decimal or hexadecimal; see on_talk_to_npc1 and on_talk_to_npc2 for examples.
|
||||
// The quest script begins after the header directives. A quest script is a sequence of opcodes, and labels denoting
|
||||
// positions within that sequence that can be jumped to or called like a function. All labels have names, and some have
|
||||
// numbers. (In the compiled format, labels have only numbers and no names; during compilation, each label that doesn't
|
||||
// have a number is assigned a number that isn't in use by another label.) To explicitly specify a label number (for
|
||||
// example, if an object or NPC refers to a label by number), use an @ sign followed by the desired number. Note that
|
||||
// numbers can be specified in decimal or hexadecimal; see on_talk_to_npc1 and on_talk_to_npc2 for examples.
|
||||
|
||||
// Registers may be named as well as labels. (In the compiled script, registers
|
||||
// do not have names, so disassembling a quest script always produces only
|
||||
// numbered registers.) When compiling, all of the following are valid:
|
||||
// Registers may be named as well as labels. (In the compiled script, registers do not have names, so disassembling a
|
||||
// quest script always produces only numbered registers.) When compiling, all of the following are valid:
|
||||
// r83 (explicitly numbered register)
|
||||
// r:difficulty_level (the compiler will assign an unused register number)
|
||||
// r:difficulty_level@83 (named and explicitly numbered)
|
||||
// You don't always have to use the same form for each register; for example,
|
||||
// if you use r:difficulty_level@83 anywhere in the quest script, you can also
|
||||
// use r:difficulty_level and r83 in other places and they will all refer to the
|
||||
// same register. (However, if you don't use r:difficulty_level@83 anywhere, but
|
||||
// you do use r83 and r:difficulty_level, the compiler will assign these to two
|
||||
// different registers since there is nothing linking the name to the number.)
|
||||
// You don't always have to use the same form for each register; for example, if you use r:difficulty_level@83 anywhere
|
||||
// in the quest script, you can also use r:difficulty_level and r83 in other places and they will all refer to the same
|
||||
// register. (However, if you don't use r:difficulty_level@83 anywhere, but you do use r83 and r:difficulty_level, the
|
||||
// compiler will assign these to two different registers since there is nothing linking the name to the number.)
|
||||
|
||||
// Using opcodes that take a consecutive sequence of registers, such as
|
||||
// map_designate which takes 4, introduces constraints on which registers may be
|
||||
// assigned to which numbers. For example, before one of the map_designate
|
||||
// opcodes after the start label, we explicitly assign one register's number,
|
||||
// but leave the nearby registers' numbers unassigned. The compiler assigns
|
||||
// those four registers to r60-r63, because they are used in a map_designate
|
||||
// call. If we didn't explicitly number any of those registers, the compiler
|
||||
// would instead choose a consecutive sequence of register numbers that aren't
|
||||
// used anywhere else in the script.
|
||||
// There are a few registers that have predefined names, since they have hardcoded behaviors on the client. These are:
|
||||
// r:quest_board_item1 = r74
|
||||
// r:quest_board_item2 = r75
|
||||
// r:quest_board_item3 = r76
|
||||
// r:quest_board_item4 = r77
|
||||
// r:quest_board_item5 = r78
|
||||
// r:quest_board_item6 = r79
|
||||
// r:quest_board_item7 = r80
|
||||
// r:quest_failed = r253
|
||||
// r:quest_succeeded = r255
|
||||
|
||||
// This quest does not contain any examples of non-script data, but such data
|
||||
// can be included in the quest script using the .data directive, like this:
|
||||
// Using opcodes that take a consecutive sequence of registers, such as map_designate which takes 4, introduces
|
||||
// constraints on which registers may be assigned to which numbers. For example, before one of the map_designate
|
||||
// opcodes after the start label, we explicitly assign one register's number, but leave the nearby registers' numbers
|
||||
// unassigned. The compiler assigns those four registers to r60-r63, because they are used in a map_designate call. If
|
||||
// we didn't explicitly number any of those registers, the compiler would instead choose a consecutive sequence of
|
||||
// register numbers that aren't used anywhere else in the script.
|
||||
|
||||
// This quest does not contain any examples of non-script data, but such data can be included in the quest script using
|
||||
// the .data directive, like this:
|
||||
// hello_symbol_chat:
|
||||
// .data 28000000 FFFF 0D00 FFFF FFFF 05 18 1D 00 05 28 1D 01 36 20 2A 00 3C 00 32 00 FF 00 00 00 FF 00 00 00 FF 00 00 00 FF 00 00 02 FF 00 00 02 FF 00 00 02 FF 00 00 02 FF 00 00 02
|
||||
// You can also include binary data from another file in the same directory
|
||||
// (the contents of the file are "pasted" into the assembled script, as if you
|
||||
// had pasted in the hex along with a .data directive):
|
||||
// You can also include binary data from another file in the same directory (the contents of the file are "pasted" into
|
||||
// the assembled script, as if you had pasted in the hex along with a .data directive):
|
||||
// movement_data:
|
||||
// .include_bin movement_data.bin
|
||||
// There is also a directive for including a large number of zero bytes:
|
||||
// lots_of_zeroes:
|
||||
// .zero 0x400 // 1024 bytes of zeroes
|
||||
|
||||
// There is also a way for quest scripts to include other files. This works by
|
||||
// simply "pasting" the contents of the file in place of the include directive,
|
||||
// so all labels in the included file will be accessible from the file that
|
||||
// included it. newserv looks for the included file in the same directory as
|
||||
// the quest file, then looks in the system/quest/includes directory. Here's
|
||||
// the syntax:
|
||||
// .include my-function.txt
|
||||
// There is also a way for quest scripts to include other files. This works by simply "pasting" the contents of the
|
||||
// file in place of the include directive, so all labels in the included file will be accessible from the file that
|
||||
// included it. newserv looks for the included file in the same directory as the quest file, then looks in the
|
||||
// system/quest/includes directory. The syntax for this is:
|
||||
// .include my-function.txt
|
||||
|
||||
// Every quest must have a start label; this is the main thread that starts when
|
||||
// the quest begins. The start label is always assigned number 0.
|
||||
// Every quest must have a start label; this is the main thread that starts when the quest begins. The start label is
|
||||
// always assigned number 0.
|
||||
start:
|
||||
gget 0x0091, r:flag_0091_value@252
|
||||
set_floor_handler 0, floor_handler_pioneer_2
|
||||
@@ -135,26 +122,26 @@ start:
|
||||
set_floor_handler 11, floor_handler_dragon
|
||||
set_qt_success on_quest_success
|
||||
get_difficulty_level_v2 r:difficulty_level@83
|
||||
leti r:op_arg1, 0 // Pioneer 2
|
||||
leti r:op_arg1, 0
|
||||
leti r:op_arg2, 0
|
||||
leti r:op_arg3@62, 0 // See comment above about register assignment
|
||||
leti r:op_arg4, 0
|
||||
map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4)
|
||||
leti r:op_arg1, 1 // Forest 1
|
||||
map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4) // Pioneer 2
|
||||
leti r:op_arg1, 1
|
||||
leti r:op_arg2, 0
|
||||
leti r:op_arg3, 0
|
||||
leti r:op_arg4, 0
|
||||
map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4)
|
||||
leti r:op_arg1, 2 // Forest 2
|
||||
map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4) // Forest 1
|
||||
leti r:op_arg1, 2
|
||||
leti r:op_arg2, 0
|
||||
leti r:op_arg3, 0
|
||||
leti r:op_arg4, 0
|
||||
map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4)
|
||||
leti r:op_arg1, 11 // Dragon
|
||||
map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4) // Forest 2
|
||||
leti r:op_arg1, 11
|
||||
leti r:op_arg2, 0
|
||||
leti r:op_arg3, 0
|
||||
leti r:op_arg4, 0
|
||||
map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4)
|
||||
map_designate (r:op_arg1, r:op_arg2, r:op_arg3, r:op_arg4) // Dragon
|
||||
ret
|
||||
|
||||
return_immediately:
|
||||
|
||||
Reference in New Issue
Block a user