reformat more files
This commit is contained in:
+2
-2
@@ -67,8 +67,8 @@ string AFSArchive::generate_t(const vector<string>& files) {
|
||||
w.put_u32b(0x41465300); // 'AFS\0'
|
||||
w.put<U32T<BE>>(files.size());
|
||||
|
||||
// It seems entries are aligned to 0x800-byte boundaries, and the file's
|
||||
// header is always 0x80000 (!) bytes, most of which is unused
|
||||
// It seems entries are aligned to 0x800-byte boundaries, and the file's header is always 0x80000 (!) bytes, most of
|
||||
// which is unused
|
||||
uint32_t data_offset = 0x80000;
|
||||
for (const auto& file : files) {
|
||||
w.put<U32T<BE>>(data_offset);
|
||||
|
||||
+13
-20
@@ -32,10 +32,7 @@ shared_ptr<DCNTELicense> DCNTELicense::from_json(const phosg::JSON& json) {
|
||||
}
|
||||
|
||||
phosg::JSON DCNTELicense::json() const {
|
||||
return phosg::JSON::dict({
|
||||
{"SerialNumber", this->serial_number},
|
||||
{"AccessKey", this->access_key},
|
||||
});
|
||||
return phosg::JSON::dict({{"SerialNumber", this->serial_number}, {"AccessKey", this->access_key}});
|
||||
}
|
||||
|
||||
shared_ptr<V1V2License> V1V2License::from_json(const phosg::JSON& json) {
|
||||
@@ -52,10 +49,7 @@ shared_ptr<V1V2License> V1V2License::from_json(const phosg::JSON& json) {
|
||||
}
|
||||
|
||||
phosg::JSON V1V2License::json() const {
|
||||
return phosg::JSON::dict({
|
||||
{"SerialNumber", this->serial_number},
|
||||
{"AccessKey", this->access_key},
|
||||
});
|
||||
return phosg::JSON::dict({{"SerialNumber", this->serial_number}, {"AccessKey", this->access_key}});
|
||||
}
|
||||
|
||||
shared_ptr<GCLicense> GCLicense::from_json(const phosg::JSON& json) {
|
||||
@@ -101,11 +95,7 @@ shared_ptr<XBLicense> XBLicense::from_json(const phosg::JSON& json) {
|
||||
}
|
||||
|
||||
phosg::JSON XBLicense::json() const {
|
||||
return phosg::JSON::dict({
|
||||
{"GamerTag", this->gamertag},
|
||||
{"UserID", this->user_id},
|
||||
{"AccountID", this->account_id},
|
||||
});
|
||||
return phosg::JSON::dict({{"GamerTag", this->gamertag}, {"UserID", this->user_id}, {"AccountID", this->account_id}});
|
||||
}
|
||||
|
||||
shared_ptr<BBLicense> BBLicense::from_json(const phosg::JSON& json) {
|
||||
@@ -128,10 +118,7 @@ shared_ptr<BBLicense> BBLicense::from_json(const phosg::JSON& json) {
|
||||
}
|
||||
|
||||
phosg::JSON BBLicense::json() const {
|
||||
return phosg::JSON::dict({
|
||||
{"UserName", this->username},
|
||||
{"Password", this->password},
|
||||
});
|
||||
return phosg::JSON::dict({{"UserName", this->username}, {"Password", this->password}});
|
||||
}
|
||||
|
||||
Account::Account(const phosg::JSON& json)
|
||||
@@ -412,7 +399,8 @@ string Account::str() const {
|
||||
void Account::save() const {
|
||||
if (!this->is_temporary) {
|
||||
auto json = this->json();
|
||||
string json_data = json.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::HEX_INTEGERS);
|
||||
string json_data = json.serialize(
|
||||
phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::HEX_INTEGERS);
|
||||
string filename = std::format("system/licenses/{:010}.json", this->account_id);
|
||||
phosg::save_file(filename, json_data);
|
||||
}
|
||||
@@ -656,7 +644,11 @@ shared_ptr<Login> AccountIndex::from_gc_credentials_locked(
|
||||
}
|
||||
|
||||
shared_ptr<Login> AccountIndex::from_gc_credentials(
|
||||
uint32_t serial_number, const string& access_key, const string* password, const string& character_name, bool allow_create) {
|
||||
uint32_t serial_number,
|
||||
const string& access_key,
|
||||
const string* password,
|
||||
const string& character_name,
|
||||
bool allow_create) {
|
||||
if (serial_number == 0) {
|
||||
throw no_username();
|
||||
}
|
||||
@@ -750,7 +742,8 @@ shared_ptr<Login> AccountIndex::from_bb_credentials_locked(const string& usernam
|
||||
return login;
|
||||
}
|
||||
|
||||
shared_ptr<Login> AccountIndex::from_bb_credentials(const string& username, const string* password, bool allow_create) {
|
||||
shared_ptr<Login> AccountIndex::from_bb_credentials(
|
||||
const string& username, const string* password, bool allow_create) {
|
||||
if (username.empty() || (password && password->empty())) {
|
||||
throw no_username();
|
||||
}
|
||||
|
||||
+12
-38
@@ -72,8 +72,7 @@ struct Account {
|
||||
ADMINISTRATOR = 0x000000FF,
|
||||
ROOT = 0x7FFFFFFF,
|
||||
IS_SHARED_ACCOUNT = 0x80000000,
|
||||
// NOTE: When adding or changing license flags, don't forget to change the
|
||||
// documentation in the shell's help text.
|
||||
// NOTE: When adding or changing license flags, don't forget to change the documentation in the shell's help text.
|
||||
UNUSED_BITS = 0x70FFFF00,
|
||||
// clang-format on
|
||||
};
|
||||
@@ -149,8 +148,7 @@ struct Login {
|
||||
bool account_was_created = false;
|
||||
// This field will never be null
|
||||
std::shared_ptr<Account> account;
|
||||
// Exactly one of the following will be non-null, representing the license
|
||||
// that the client logged in with
|
||||
// Exactly one of the following will be non-null, representing the license that the client logged in with
|
||||
std::shared_ptr<DCNTELicense> dc_nte_license;
|
||||
std::shared_ptr<V1V2License> dc_license;
|
||||
std::shared_ptr<V1V2License> pc_license;
|
||||
@@ -210,22 +208,12 @@ public:
|
||||
|
||||
std::shared_ptr<Account> from_account_id(uint32_t account_id) const;
|
||||
std::shared_ptr<Login> from_dc_nte_credentials(
|
||||
const std::string& serial_number,
|
||||
const std::string& access_key,
|
||||
bool allow_create);
|
||||
const std::string& serial_number, const std::string& access_key, bool allow_create);
|
||||
std::shared_ptr<Login> from_dc_credentials(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& character_name,
|
||||
bool allow_create);
|
||||
std::shared_ptr<Login> from_pc_nte_credentials(
|
||||
uint32_t guild_card_number,
|
||||
bool allow_create);
|
||||
uint32_t serial_number, const std::string& access_key, const std::string& character_name, bool allow_create);
|
||||
std::shared_ptr<Login> from_pc_nte_credentials(uint32_t guild_card_number, bool allow_create);
|
||||
std::shared_ptr<Login> from_pc_credentials(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& character_name,
|
||||
bool allow_create);
|
||||
uint32_t serial_number, const std::string& access_key, const std::string& character_name, bool allow_create);
|
||||
std::shared_ptr<Login> from_gc_credentials(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
@@ -233,14 +221,9 @@ public:
|
||||
const std::string& character_name,
|
||||
bool allow_create);
|
||||
std::shared_ptr<Login> from_xb_credentials(
|
||||
const std::string& gamertag,
|
||||
uint64_t user_id,
|
||||
uint64_t account_id,
|
||||
bool allow_create);
|
||||
const std::string& gamertag, uint64_t user_id, uint64_t account_id, bool allow_create);
|
||||
std::shared_ptr<Login> from_bb_credentials(
|
||||
const std::string& username,
|
||||
const std::string* password,
|
||||
bool allow_create);
|
||||
const std::string& username, const std::string* password, bool allow_create);
|
||||
|
||||
std::shared_ptr<Account> create_temporary_account_for_shared_account(
|
||||
std::shared_ptr<const Account> src_a, const std::string& variation_data) const;
|
||||
@@ -248,8 +231,6 @@ public:
|
||||
protected:
|
||||
bool force_all_temporary;
|
||||
|
||||
// This class must be thread-safe because it's used by both the patch server
|
||||
// and game server threads
|
||||
mutable std::shared_mutex lock;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<Account>> by_account_id;
|
||||
std::unordered_map<std::string, std::shared_ptr<Account>> by_dc_nte_serial_number;
|
||||
@@ -262,23 +243,16 @@ protected:
|
||||
void add_locked(std::shared_ptr<Account> a);
|
||||
|
||||
std::shared_ptr<Login> from_dc_nte_credentials_locked(
|
||||
const std::string& serial_number,
|
||||
const std::string& access_key);
|
||||
const std::string& serial_number, const std::string& access_key);
|
||||
std::shared_ptr<Login> from_dc_credentials_locked(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& character_name);
|
||||
uint32_t serial_number, const std::string& access_key, const std::string& character_name);
|
||||
std::shared_ptr<Login> from_pc_credentials_locked(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string& character_name);
|
||||
uint32_t serial_number, const std::string& access_key, const std::string& character_name);
|
||||
std::shared_ptr<Login> from_gc_credentials_locked(
|
||||
uint32_t serial_number,
|
||||
const std::string& access_key,
|
||||
const std::string* password,
|
||||
const std::string& character_name);
|
||||
std::shared_ptr<Login> from_xb_credentials_locked(uint64_t user_id);
|
||||
std::shared_ptr<Login> from_bb_credentials_locked(
|
||||
const std::string& username,
|
||||
const std::string* password);
|
||||
std::shared_ptr<Login> from_bb_credentials_locked(const std::string& username, const std::string* password);
|
||||
};
|
||||
|
||||
@@ -267,20 +267,16 @@ public:
|
||||
|
||||
// Returns {type: {constructor_addr: [(start_area, end_area), ...]}}
|
||||
template <typename EntryT>
|
||||
map<uint32_t, map<uint32_t, vector<pair<size_t, size_t>>>>
|
||||
parse_dat_constructor_table_t(
|
||||
shared_ptr<const ResourceDASM::MemoryContext>& mem,
|
||||
const ParseDATConstructorTableSpec& spec) {
|
||||
map<uint32_t, map<uint32_t, vector<pair<size_t, size_t>>>> parse_dat_constructor_table_t(
|
||||
shared_ptr<const ResourceDASM::MemoryContext>& mem, const ParseDATConstructorTableSpec& spec) {
|
||||
if (!mem) {
|
||||
throw runtime_error("no file selected");
|
||||
}
|
||||
|
||||
// On some of the x86 builds of the game (PCv2 and Xbox), the constructor
|
||||
// tables aren't entirely static in the data sections - some parts are
|
||||
// written during static initialization instead. To handle this, we make a
|
||||
// copy of the immutable MemoryContext and run the static initialization
|
||||
// functions using resource_dasm's emulator before parsing the constructor
|
||||
// table.
|
||||
// On some of the x86 builds of the game (PCv2 and Xbox), the constructor tables aren't entirely static in the data
|
||||
// sections - some parts are written during static initialization instead. To handle this, we make a copy of the
|
||||
// immutable MemoryContext and run the static initialization functions using resource_dasm's emulator before
|
||||
// parsing the constructor table.
|
||||
shared_ptr<const ResourceDASM::MemoryContext> effective_mem = mem;
|
||||
if (!spec.x86_constructor_calls.empty()) {
|
||||
auto constructed_mem = make_shared<ResourceDASM::MemoryContext>(mem->duplicate());
|
||||
@@ -455,9 +451,7 @@ public:
|
||||
}
|
||||
}
|
||||
line.push_back(' ');
|
||||
line += is_enemies
|
||||
? MapFile::name_for_enemy_type(type)
|
||||
: MapFile::name_for_object_type(type);
|
||||
line += is_enemies ? MapFile::name_for_enemy_type(type) : MapFile::name_for_object_type(type);
|
||||
|
||||
if ((formatted_lines.size() % 40) == 0) {
|
||||
formatted_lines.emplace_back(header_line);
|
||||
@@ -732,9 +726,7 @@ public:
|
||||
}
|
||||
|
||||
uint32_t find_be_to_le_data_match(
|
||||
shared_ptr<const ResourceDASM::MemoryContext> dest_mem,
|
||||
uint32_t src_addr,
|
||||
uint32_t src_size) const {
|
||||
shared_ptr<const ResourceDASM::MemoryContext> dest_mem, uint32_t src_addr, uint32_t src_size) const {
|
||||
if (src_size == 0) {
|
||||
src_size = 4;
|
||||
}
|
||||
|
||||
@@ -324,8 +324,8 @@ asio::awaitable<WebSocketMessage> HTTPClient::recv_websocket_message(size_t max_
|
||||
|
||||
this->last_communication_time = phosg::now();
|
||||
|
||||
// If the current message is a control message, respond appropriately
|
||||
// (these can be sent in the middle of fragmented messages)
|
||||
// If the current message is a control message, respond appropriately (these can be sent in the middle of
|
||||
// fragmented messages)
|
||||
uint8_t opcode = msg.header[0] & 0x0F;
|
||||
if (opcode & 0x08) {
|
||||
if (opcode == 0x0A) {
|
||||
@@ -347,8 +347,8 @@ asio::awaitable<WebSocketMessage> HTTPClient::recv_websocket_message(size_t max_
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there's an existing fragment, the current message's opcode should be
|
||||
// zero; if there's no pending message, it must not be zero
|
||||
// If there's an existing fragment, the current message's opcode should be zero; if there's no pending message, it
|
||||
// must not be zero
|
||||
if (prev_msg_present == (opcode != 0)) {
|
||||
this->r.close();
|
||||
continue;
|
||||
@@ -372,10 +372,9 @@ asio::awaitable<WebSocketMessage> HTTPClient::recv_websocket_message(size_t max_
|
||||
prev_msg.data += msg.data;
|
||||
}
|
||||
|
||||
// If the FIN bit is set, then the frame is complete - append the payload
|
||||
// to any pending payloads and call the message handler. If the FIN bit
|
||||
// isn't set, we need to receive at least one continuation frame to
|
||||
// complete the message.
|
||||
// If the FIN bit is set, then the frame is complete - append the payload to any pending payloads and call the
|
||||
// message handler. If the FIN bit isn't set, we need to receive at least one continuation frame to complete the
|
||||
// message.
|
||||
if (prev_msg.header[0] & 0x80) {
|
||||
co_return prev_msg;
|
||||
}
|
||||
|
||||
+9
-15
@@ -38,9 +38,8 @@ struct HTTPRequest {
|
||||
std::unordered_multimap<std::string, std::string> query_params;
|
||||
std::string data;
|
||||
|
||||
// Header name should be entirely lowercase for this function. Returns
|
||||
// nullptr if the header doesn't exist; throws http_error(400) if multiple
|
||||
// instances of it exist.
|
||||
// Header name should be entirely lowercase for this function. Returns nullptr if the header doesn't exist; throws
|
||||
// http_error(400) if multiple instances of it exist.
|
||||
const std::string* get_header(const std::string& name) const;
|
||||
|
||||
const std::string* get_query_param(const std::string& name) const;
|
||||
@@ -49,8 +48,7 @@ struct HTTPRequest {
|
||||
struct HTTPResponse {
|
||||
std::string http_version;
|
||||
int response_code = 200;
|
||||
// Content-Length should NOT be specified in headers; it is automatically
|
||||
// added in async_write() if data is not blank.
|
||||
// Content-Length should NOT be specified in headers; it is automatically added in async_write() if data isn't blank.
|
||||
std::unordered_multimap<std::string, std::string> headers;
|
||||
std::string data;
|
||||
};
|
||||
@@ -244,9 +242,8 @@ protected:
|
||||
}
|
||||
}
|
||||
|
||||
// Attempts to switch the client to WebSockets. Returns true if this is done
|
||||
// successfully (and the caller should then receive/send WebSocket messages),
|
||||
// or false if this failed (and the caller should send an HTTP response).
|
||||
// Attempts to switch the client to WebSockets. Returns true if this is done successfully (and the caller should then
|
||||
// receive/send WebSocket messages), or false if this failed (and the caller should send an HTTP response).
|
||||
asio::awaitable<bool> enable_websockets(std::shared_ptr<ClientT> c, const HTTPRequest& req) {
|
||||
if (req.method != HTTPRequest::Method::GET) {
|
||||
co_return false;
|
||||
@@ -287,13 +284,10 @@ protected:
|
||||
|
||||
// handle_request must do one of the following three things:
|
||||
// 1. Return an HTTP response.
|
||||
// 2. Call enable_websockets, and if it returns true, return nullptr. After
|
||||
// this point, handle_request will not be called again for this client;
|
||||
// handle_websocket_message will be called instead when any WebSocket
|
||||
// messages are received. If enable_websockets returns false,
|
||||
// handle_request must still return an HTTP response.
|
||||
// 3. Throw an exception. In this case, the client receives an HTTP 500
|
||||
// response.
|
||||
// 2. Call enable_websockets, and if it returns true, return nullptr. After this point, handle_request will not be
|
||||
// called again for this client; handle_websocket_message will be called instead when any WebSocket messages are
|
||||
// received. If enable_websockets returns false, handle_request must still return an HTTP response.
|
||||
// 3. Throw an exception. In this case, the client receives an HTTP 500 response.
|
||||
virtual asio::awaitable<std::unique_ptr<HTTPResponse>> handle_request(std::shared_ptr<ClientT> c, HTTPRequest&& req) = 0;
|
||||
virtual asio::awaitable<void> handle_websocket_message(std::shared_ptr<ClientT>, WebSocketMessage&&) {
|
||||
co_return;
|
||||
|
||||
+1
-2
@@ -73,8 +73,7 @@ asio::awaitable<string> AsyncSocketReader::read_line(const char* delimiter, size
|
||||
throw runtime_error("line exceeds max length");
|
||||
}
|
||||
|
||||
// TODO: It's not great that we copy the data here. There's probably a more
|
||||
// idiomatic and efficient way to do this.
|
||||
// TODO: It's not great that we copy the data here. There's probably a more idiomatic and efficient way to do this.
|
||||
string ret = this->pending_data.substr(0, delimiter_pos);
|
||||
this->pending_data = this->pending_data.substr(delimiter_pos + delimiter_size);
|
||||
co_return ret;
|
||||
|
||||
+8
-10
@@ -175,16 +175,15 @@ public:
|
||||
AsyncSocketReader& operator=(AsyncSocketReader&&) = delete;
|
||||
~AsyncSocketReader() = default;
|
||||
|
||||
// Reads one line from the socket, buffering any extra data read. The
|
||||
// delimiter is not included in the returned line. max_length = 0 means no
|
||||
// maximum length is enforced.
|
||||
// Reads one line from the socket, buffering any extra data read. The delimiter is not included in the returned line.
|
||||
// max_length = 0 means no maximum length is enforced.
|
||||
asio::awaitable<std::string> read_line(
|
||||
const char* delimiter = "\n", size_t max_length = 0);
|
||||
asio::awaitable<std::string> read_data(size_t size);
|
||||
asio::awaitable<void> read_data_into(void* data, size_t size);
|
||||
|
||||
// The caller cannot know what the socket's read state is, so this should
|
||||
// only be used when the caller intends to write to the socket, not read
|
||||
// The caller cannot know what the socket's read state is, so this should only be used when the caller intends to
|
||||
// write to the socket, not read
|
||||
inline asio::ip::tcp::socket& get_socket() {
|
||||
return this->sock;
|
||||
}
|
||||
@@ -215,8 +214,8 @@ public:
|
||||
|
||||
void add(std::string&& data);
|
||||
|
||||
// When using add_reference, it is the caller's responsibility to ensure that
|
||||
// the buffer is valid until *this is destroyed or write() returns.
|
||||
// When using add_reference, it is the caller's responsibility to ensure that the buffer is valid until *this is
|
||||
// destroyed or write() returns.
|
||||
void add_reference(const void* data, size_t size);
|
||||
|
||||
asio::awaitable<void> write(asio::ip::tcp::socket& sock);
|
||||
@@ -260,9 +259,8 @@ asio::awaitable<std::invoke_result_t<FnT, ArgTs...>> call_on_thread_pool(asio::t
|
||||
using ReturnT = std::invoke_result_t<FnT, ArgTs...>;
|
||||
auto bound = std::bind(std::forward<FnT>(f), std::forward<ArgTs>(args)...);
|
||||
|
||||
// We have to use a shared_ptr here in case call_on_thread_pool is canceled
|
||||
// (in that case, the posted callback will try to use promise after the
|
||||
// call_on_thread_pool coroutine has been destroyed)
|
||||
// We have to use a shared_ptr here in case call_on_thread_pool is canceled (in that case, the posted callback will
|
||||
// try to use promise after the call_on_thread_pool coroutine has been destroyed)
|
||||
auto promise = std::make_shared<AsyncPromise<ReturnT>>();
|
||||
asio::post(pool, [bound = std::move(bound), promise]() mutable {
|
||||
try {
|
||||
|
||||
@@ -15,7 +15,6 @@ struct BMLHeaderT {
|
||||
U32T<BE> num_entries;
|
||||
parray<uint8_t, 0x38> unknown_a2;
|
||||
} __attribute__((packed));
|
||||
|
||||
using BMLHeader = BMLHeaderT<false>;
|
||||
using BMLHeaderBE = BMLHeaderT<true>;
|
||||
check_struct_size(BMLHeader, 0x40);
|
||||
@@ -31,7 +30,6 @@ struct BMLHeaderEntryT {
|
||||
U32T<BE> decompressed_gvm_size;
|
||||
parray<uint8_t, 0x0C> unknown_a2;
|
||||
} __attribute__((packed));
|
||||
|
||||
using BMLHeaderEntry = BMLHeaderEntryT<false>;
|
||||
using BMLHeaderEntryBE = BMLHeaderEntryT<true>;
|
||||
check_struct_size(BMLHeaderEntry, 0x40);
|
||||
|
||||
+15
-23
@@ -32,7 +32,8 @@ void Channel::send(uint16_t cmd, uint32_t flag, bool silent) {
|
||||
this->send(cmd, flag, nullptr, 0, silent);
|
||||
}
|
||||
|
||||
void Channel::send(uint16_t cmd, uint32_t flag, const std::vector<std::pair<const void*, size_t>> blocks, bool silent) {
|
||||
void Channel::send(
|
||||
uint16_t cmd, uint32_t flag, const std::vector<std::pair<const void*, size_t>> blocks, bool silent) {
|
||||
if (!this->connected()) {
|
||||
channel_exceptions_log.warning_f("Attempted to send command on closed channel; dropping data");
|
||||
return;
|
||||
@@ -57,10 +58,7 @@ void Channel::send(uint16_t cmd, uint32_t flag, const std::vector<std::pair<cons
|
||||
case Version::GC_EP3:
|
||||
case Version::XB_V3: {
|
||||
PSOCommandHeaderDCV3 header;
|
||||
if (this->crypt_out.get() &&
|
||||
(this->version != Version::DC_NTE) &&
|
||||
(this->version != Version::DC_11_2000) &&
|
||||
(this->version != Version::DC_V1)) {
|
||||
if (this->crypt_out.get() && !is_v1(this->version)) {
|
||||
send_data_size = (sizeof(header) + size + 3) & ~3;
|
||||
} else {
|
||||
send_data_size = (sizeof(header) + size);
|
||||
@@ -90,13 +88,11 @@ void Channel::send(uint16_t cmd, uint32_t flag, const std::vector<std::pair<cons
|
||||
break;
|
||||
}
|
||||
case Version::BB_V4: {
|
||||
// BB has an annoying behavior here: command lengths must be multiples of
|
||||
// 4, but the actual data length must be a multiple of 8. If the size
|
||||
// field is not divisible by 8, 4 extra bytes are sent anyway. This
|
||||
// behavior only applies when encryption is enabled - any commands sent
|
||||
// before encryption is enabled have no size restrictions (except they
|
||||
// must include a full header and must fit in the client's receive
|
||||
// buffer), and no implicit extra bytes are sent.
|
||||
// BB has an annoying behavior here: command lengths must be multiples of 4, but the actual data length must be a
|
||||
// multiple of 8. If the size field is not divisible by 8, 4 extra bytes are sent anyway. This behavior only
|
||||
// applies when encryption is enabled - any commands sent before encryption is enabled have no size restrictions
|
||||
// (except they must include a full header and must fit in the client's receive buffer), and no implicit extra
|
||||
// bytes are sent.
|
||||
PSOCommandHeaderBB header;
|
||||
if (this->crypt_out.get()) {
|
||||
send_data_size = (sizeof(header) + size + 7) & ~7;
|
||||
@@ -115,8 +111,7 @@ void Channel::send(uint16_t cmd, uint32_t flag, const std::vector<std::pair<cons
|
||||
throw logic_error("unimplemented game version in send_command");
|
||||
}
|
||||
|
||||
// All versions of PSO I've seen (so far) have a receive buffer 0x7C00
|
||||
// bytes in size
|
||||
// All versions of PSO I've seen (so far) have a receive buffer 0x7C00 bytes in size
|
||||
if (send_data_size > 0x7C00) {
|
||||
throw runtime_error("outbound command too large");
|
||||
}
|
||||
@@ -132,8 +127,7 @@ void Channel::send(uint16_t cmd, uint32_t flag, const std::vector<std::pair<cons
|
||||
print_color_escape(stderr, phosg::TerminalFormat::FG_YELLOW, phosg::TerminalFormat::BOLD, phosg::TerminalFormat::END);
|
||||
}
|
||||
if (version == Version::BB_V4) {
|
||||
command_data_log.info_f("Sending to {} (version=BB command={:04X} flag={:08X})",
|
||||
this->name, cmd, flag);
|
||||
command_data_log.info_f("Sending to {} (version=BB command={:04X} flag={:08X})", this->name, cmd, flag);
|
||||
} else {
|
||||
command_data_log.info_f("Sending to {} (version={} command={:02X} flag={:02X})",
|
||||
this->name, phosg::name_for_enum(version), cmd, flag);
|
||||
@@ -187,9 +181,8 @@ asio::awaitable<Channel::Message> Channel::recv() {
|
||||
throw runtime_error("header size field is smaller than header");
|
||||
}
|
||||
|
||||
// If encryption is enabled, BB pads commands to 8-byte boundaries, and this
|
||||
// is not reflected in the size field. This logic does not occur if encryption
|
||||
// is not yet enabled.
|
||||
// If encryption is enabled, BB pads commands to 8-byte boundaries, and this is not reflected in the size field. This
|
||||
// logic does not occur if encryption is not yet enabled.
|
||||
size_t command_physical_size = (this->crypt_in.get() && (version == Version::BB_V4))
|
||||
? ((command_logical_size + 7) & ~7)
|
||||
: command_logical_size;
|
||||
@@ -198,10 +191,9 @@ asio::awaitable<Channel::Message> Channel::recv() {
|
||||
co_await this->recv_raw(command_data.data(), command_data.size());
|
||||
|
||||
if (this->crypt_in.get()) {
|
||||
// Some versions of PSO DC can send commands whose sizes are not a multiple
|
||||
// of 4, but the server is expected to always use a multiple of 4 bytes when
|
||||
// decrypting (the extra cipher bytes are lost). To emulate this behavior,
|
||||
// we have to round up the size for DC commands here.
|
||||
// Some versions of PSO DC can send commands whose sizes are not a multiple of 4, but the server is expected to
|
||||
// always use a multiple of 4 bytes when decrypting (the extra cipher bytes are lost). To emulate this behavior, we
|
||||
// have to round up the size for DC commands here.
|
||||
size_t orig_size = command_data.size();
|
||||
command_data.resize((orig_size + 3) & (~3), 0);
|
||||
this->crypt_in->decrypt(command_data.data(), command_data.size());
|
||||
|
||||
+9
-16
@@ -60,9 +60,8 @@ public:
|
||||
// Returns whether the channel is connected or not.
|
||||
virtual bool connected() const = 0;
|
||||
|
||||
// Disconnects the channel. Any pending data will still be sent before the
|
||||
// underlying transport (e.g. socket) is closed, but further send calls will
|
||||
// do nothing.
|
||||
// Disconnects the channel. Any pending data will still be sent before the underlying transport (e.g. socket) is
|
||||
// closed, but further send calls will do nothing.
|
||||
virtual void disconnect() = 0;
|
||||
|
||||
// Sends a message with an automatically-constructed header.
|
||||
@@ -76,8 +75,7 @@ public:
|
||||
this->send(cmd, flag, &data, sizeof(data), silent);
|
||||
}
|
||||
|
||||
// Sends a message with a pre-existing header (as the first few bytes in the
|
||||
// data)
|
||||
// Sends a message with a pre-existing header (as the first few bytes in the data)
|
||||
void send(const void* data, size_t size, bool silent = false);
|
||||
void send(const std::string& data, bool silent = false);
|
||||
|
||||
@@ -96,27 +94,22 @@ protected:
|
||||
Channel& operator=(const Channel& other) = delete;
|
||||
Channel& operator=(Channel&& other) = delete;
|
||||
|
||||
// Sends raw data on the underlying transport. If the channel is already
|
||||
// disconnected, silently drops the data.
|
||||
// Sends raw data on the underlying transport. If the channel is already disconnected, silently drops the data.
|
||||
virtual void send_raw(std::string&& data) = 0;
|
||||
// Receives raw data on the underlying transport. Raises when the channel is
|
||||
// disconnected.
|
||||
// Receives raw data on the underlying transport. Raises when the channel is disconnected.
|
||||
virtual asio::awaitable<void> recv_raw(void* data, size_t size) = 0;
|
||||
};
|
||||
|
||||
// Standard channel type, used for most PSO clients. Represents an open TCP
|
||||
// socket.
|
||||
// Standard channel type, used for most PSO clients. Represents an open TCP socket.
|
||||
class SocketChannel : public Channel, public std::enable_shared_from_this<SocketChannel> {
|
||||
public:
|
||||
std::unique_ptr<asio::ip::tcp::socket> sock;
|
||||
asio::ip::tcp::endpoint local_addr;
|
||||
asio::ip::tcp::endpoint remote_addr;
|
||||
|
||||
// SocketChannel has a static constructor because it has an internal task,
|
||||
// which is necessary to support flushing before disconnection (for example)
|
||||
// and also to make send_raw not a coroutine, which keeps the rest of the
|
||||
// code cleaner. The task needs to hold a shared_ptr to the SocketChannel
|
||||
// whilc it's open
|
||||
// SocketChannel has a static constructor because it has an internal task, which is necessary to support flushing
|
||||
// before disconnection (for example) and also to make send_raw not a coroutine, which keeps the rest of the code
|
||||
// cleaner.
|
||||
static std::shared_ptr<SocketChannel> create(std::shared_ptr<asio::io_context> io_context,
|
||||
std::unique_ptr<asio::ip::tcp::socket>&& sock,
|
||||
Version version,
|
||||
|
||||
+56
-85
@@ -25,7 +25,7 @@
|
||||
|
||||
using namespace std;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Tools
|
||||
|
||||
string str_for_flag_ranges(const vector<bool>& flags) {
|
||||
@@ -60,7 +60,7 @@ string str_for_flag_ranges(const vector<bool>& flags) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Checks
|
||||
|
||||
class precondition_failed {
|
||||
@@ -165,7 +165,7 @@ struct Args {
|
||||
}
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Command definitions
|
||||
|
||||
struct ChatCommandDefinition {
|
||||
@@ -188,7 +188,7 @@ struct ChatCommandDefinition {
|
||||
|
||||
unordered_map<string, const ChatCommandDefinition*> ChatCommandDefinition::all_defs;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// All commands (in alphabetical order)
|
||||
|
||||
ChatCommandDefinition cc_allevent(
|
||||
@@ -265,8 +265,8 @@ ChatCommandDefinition cc_announce_rares(
|
||||
|
||||
a.c->login->account->toggle_user_flag(Account::UserFlag::DISABLE_DROP_NOTIFICATION_BROADCAST);
|
||||
a.c->login->account->save();
|
||||
send_text_message_fmt(a.c, "$C6Rare announcements\n{} for your\nitems",
|
||||
a.c->login->account->check_user_flag(Account::UserFlag::DISABLE_DROP_NOTIFICATION_BROADCAST) ? "disabled" : "enabled");
|
||||
bool enabled = a.c->login->account->check_user_flag(Account::UserFlag::DISABLE_DROP_NOTIFICATION_BROADCAST);
|
||||
send_text_message_fmt(a.c, "$C6Rare announcements\n{} for your\nitems", enabled ? "disabled" : "enabled");
|
||||
co_return;
|
||||
});
|
||||
|
||||
@@ -370,9 +370,8 @@ ChatCommandDefinition cc_ban(
|
||||
throw precondition_failed("$C6You do not have\nsufficient privileges.");
|
||||
}
|
||||
if (a.c == target) {
|
||||
// This shouldn't be possible because you need BAN_USER to get here,
|
||||
// but the target can't have BAN_USER if we get here, so if a.c and
|
||||
// target are the same, one of the preceding conditions must be false.
|
||||
// This shouldn't be possible because you need BAN_USER to get here, but the target can't have BAN_USER if we
|
||||
// get here, so if a.c and target are the same, one of the preceding conditions must be false.
|
||||
throw logic_error("client attempts to ban themself");
|
||||
}
|
||||
|
||||
@@ -482,8 +481,7 @@ static asio::awaitable<void> server_command_bbchar_savechar(const Args& a, bool
|
||||
dest_account = a.c->login->account;
|
||||
}
|
||||
|
||||
// If the client isn't BB, request the player info. (If they are BB, the
|
||||
// server already has it)
|
||||
// If the client isn't BB, request the player info. (If they are BB, the server already has it)
|
||||
GetPlayerInfoResult ch;
|
||||
if (a.c->version() == Version::BB_V4) {
|
||||
ch.character = a.c->character_file();
|
||||
@@ -516,8 +514,7 @@ static asio::awaitable<void> server_command_bbchar_savechar(const Args& a, bool
|
||||
}
|
||||
|
||||
} else {
|
||||
// Client sent 61; generate a BB-format player from the information we have
|
||||
// and save that instead
|
||||
// Client sent 61; generate a BB-format player from the information we have and save that instead
|
||||
if (ch.character) {
|
||||
auto bb_player = PSOBBCharacterFile::create_from_config(
|
||||
a.c->login->account->account_id,
|
||||
@@ -528,9 +525,8 @@ static asio::awaitable<void> server_command_bbchar_savechar(const Args& a, bool
|
||||
bb_player->disp.visual.version = 4;
|
||||
bb_player->disp.visual.name_color_checksum = 0x00000000;
|
||||
bb_player->inventory = ch.character->inventory;
|
||||
// Before V3, player stats can't be correctly computed from other fields
|
||||
// because material usage isn't stored anywhere. For these versions, we
|
||||
// have to trust the stats field from the player's data.
|
||||
// Before V3, player stats can't be correctly computed from other fields because material usage isn't stored
|
||||
// anywhere. For these versions, we have to trust the stats field from the player's data.
|
||||
auto level_table = s->level_table(a.c->version());
|
||||
if (is_v1_or_v2(a.c->version())) {
|
||||
bb_player->disp.stats = ch.character->disp.stats;
|
||||
@@ -1074,9 +1070,7 @@ ChatCommandDefinition cc_event(
|
||||
ChatCommandDefinition cc_exit(
|
||||
{"$exit"},
|
||||
+[](const Args& a) -> asio::awaitable<void> {
|
||||
if (!(a.c->proxy_session
|
||||
? a.c->proxy_session->is_in_game
|
||||
: a.c->require_lobby()->is_game())) {
|
||||
if (!(a.c->proxy_session ? a.c->proxy_session->is_in_game : a.c->require_lobby()->is_game())) {
|
||||
// Client is in the lobby; send them to the login server (main menu)
|
||||
if (a.c->proxy_session) {
|
||||
if (is_v4(a.c->version())) {
|
||||
@@ -1181,8 +1175,6 @@ ChatCommandDefinition cc_infhp(
|
||||
ChatCommandDefinition cc_inftime(
|
||||
{"$inftime"},
|
||||
+[](const Args& a) -> asio::awaitable<void> {
|
||||
// TODO: We could implement this in proxy sessions by rewriting the rules
|
||||
// struct from the server in various 6xB4 commands.
|
||||
a.check_is_proxy(false);
|
||||
a.check_is_game(true);
|
||||
a.check_is_ep3(true);
|
||||
@@ -1301,9 +1293,8 @@ ChatCommandDefinition cc_kick(
|
||||
throw precondition_failed("$C6You do not have\nsufficient privileges.");
|
||||
}
|
||||
if (a.c == target) {
|
||||
// This shouldn't be possible because you need KICK_USER to get here,
|
||||
// but the target can't have KICK_USER if we get here, so if a.c and
|
||||
// target are the same, one of the preceding conditions must be false.
|
||||
// This shouldn't be possible because you need KICK_USER to get here, but the target can't have KICK_USER if we
|
||||
// get here, so if a.c and target are the same, one of the preceding conditions must be false.
|
||||
throw logic_error("client attempts to kick themself off");
|
||||
}
|
||||
|
||||
@@ -1332,13 +1323,10 @@ ChatCommandDefinition cc_killcount(
|
||||
throw precondition_failed("No equipped items\nhave kill counts");
|
||||
|
||||
} else {
|
||||
// Kill counts are only accurate on the server side at all times on BB.
|
||||
// On other versions, we update the server's view of the client's
|
||||
// inventory during games, but we can't track kills because the client
|
||||
// doesn't inform the server whether it counted a kill for any
|
||||
// individual enemy. So, on non-BB versions, the kill count is accurate
|
||||
// at all times in the lobby (since kills can't occur there), or at the
|
||||
// beginning of a game.
|
||||
// Kill counts are only accurate on the server side at all times on BB. On other versions, we update the
|
||||
// server's view of the client's inventory during games, but we can't track kills because the client doesn't
|
||||
// inform the server whether it counted a kill for any individual enemy. So, on non-BB versions, the kill count
|
||||
// is accurate at all times in the lobby (since kills can't occur there), or at the beginning of a game.
|
||||
if ((a.c->version() == Version::BB_V4) || !a.c->require_lobby()->is_game()) {
|
||||
send_text_message(a.c, "As of now:");
|
||||
} else {
|
||||
@@ -1361,9 +1349,8 @@ ChatCommandDefinition cc_lobby_info(
|
||||
+[](const Args& a) -> asio::awaitable<void> {
|
||||
if (a.c->proxy_session) {
|
||||
string msg;
|
||||
// On non-masked-GC sessions (BB), there is no remote Guild Card number, so we
|
||||
// don't show it. (The user can see it in the pause menu, unlike in masked-GC
|
||||
// sessions like GC.)
|
||||
// On non-masked-GC sessions (BB), there is no remote Guild Card number, so we don't show it. (The user can see
|
||||
// it in the pause menu, unlike in masked-GC sessions like GC.)
|
||||
if (a.c->proxy_session->remote_guild_card_number >= 0) {
|
||||
msg = std::format("$C7GC: $C6{}$C7\n", a.c->proxy_session->remote_guild_card_number);
|
||||
}
|
||||
@@ -1612,8 +1599,8 @@ ChatCommandDefinition cc_loadchar(
|
||||
}
|
||||
|
||||
} else {
|
||||
// On v1 and v2, the client will assign its character data from the lobby
|
||||
// join command, so it suffices to just resend the join notification.
|
||||
// On v1 and v2, the client will assign its character data from the lobby join command, so it suffices to just
|
||||
// resend the join notification.
|
||||
auto s = a.c->require_server_state();
|
||||
send_player_leave_notification(l, a.c->lobby_client_id);
|
||||
s->send_lobby_join_notifications(l, a.c);
|
||||
@@ -1788,9 +1775,7 @@ ChatCommandDefinition cc_next(
|
||||
auto s = a.c->require_server_state();
|
||||
a.check_cheats_enabled_or_allowed(s->cheat_flags.warp);
|
||||
|
||||
auto episode = a.c->proxy_session
|
||||
? a.c->proxy_session->lobby_episode
|
||||
: a.c->require_lobby()->episode;
|
||||
auto episode = a.c->proxy_session ? a.c->proxy_session->lobby_episode : a.c->require_lobby()->episode;
|
||||
size_t limit = FloorDefinition::limit_for_episode(episode);
|
||||
if (limit > 0) {
|
||||
send_warp(a.c, (a.c->floor + 1) % limit, true);
|
||||
@@ -1835,8 +1820,7 @@ ChatCommandDefinition cc_patch(
|
||||
co_await prepare_client_for_patches(a.c);
|
||||
try {
|
||||
auto s = a.c->require_server_state();
|
||||
// Note: We can't look this up before prepare_client_for_patches
|
||||
// because specific_version may not be set at that point
|
||||
// Note: We can't look this up before prepare_client_for_patches because specific_version may not be set
|
||||
auto fn = s->function_code_index->get_patch(patch_name, a.c->specific_version);
|
||||
co_await send_function_call(a.c, fn, label_writes);
|
||||
} catch (const out_of_range&) {
|
||||
@@ -1874,9 +1858,7 @@ ChatCommandDefinition cc_ping(
|
||||
if (a.c->proxy_session) {
|
||||
a.c->proxy_session->server_ping_start_time = a.c->ping_start_time;
|
||||
C_GuildCardSearch_40 cmd = {
|
||||
0x00010000,
|
||||
a.c->proxy_session->remote_guild_card_number,
|
||||
a.c->proxy_session->remote_guild_card_number};
|
||||
0x00010000, a.c->proxy_session->remote_guild_card_number, a.c->proxy_session->remote_guild_card_number};
|
||||
a.c->proxy_session->server_channel->send(0x40, 0x00, &cmd, sizeof(cmd));
|
||||
}
|
||||
co_return;
|
||||
@@ -1914,7 +1896,8 @@ ChatCommandDefinition cc_playrec(
|
||||
data = phosg::load_file(file_path_for_recording(filename, a.c->login->account->account_id, false));
|
||||
} catch (const phosg::cannot_open_file&) {
|
||||
try {
|
||||
data = prs_decompress(phosg::load_file(file_path_for_recording(filename, a.c->login->account->account_id, true)));
|
||||
data = prs_decompress(phosg::load_file(file_path_for_recording(
|
||||
filename, a.c->login->account->account_id, true)));
|
||||
} catch (const phosg::cannot_open_file&) {
|
||||
throw precondition_failed("$C4The recording does\nnot exist");
|
||||
}
|
||||
@@ -2304,8 +2287,7 @@ ChatCommandDefinition cc_savechar(
|
||||
ChatCommandDefinition cc_saverec(
|
||||
{"$saverec"},
|
||||
+[](const Args& a) -> asio::awaitable<void> {
|
||||
// TODO: We can probably support this on the proxy server, but it would
|
||||
// only include CA commands from the local player
|
||||
// TODO: We can support this on the proxy server, but it would only include CA commands from the local player
|
||||
a.check_is_proxy(false);
|
||||
if (!a.c->ep3_prev_battle_record) {
|
||||
throw precondition_failed("$C4No finished\nrecording is\npresent");
|
||||
@@ -2452,7 +2434,7 @@ ChatCommandDefinition cc_silence(
|
||||
auto s = a.c->require_server_state();
|
||||
auto target = s->find_client(&a.text);
|
||||
if (!target->login) {
|
||||
// this should be impossible, but I'll bet it's not actually
|
||||
// This should be impossible, but I'll bet it's not actually
|
||||
throw precondition_failed("$C6Client not logged in");
|
||||
}
|
||||
|
||||
@@ -2512,9 +2494,8 @@ ChatCommandDefinition cc_spec(
|
||||
throw logic_error("Episode 3 client in non-Episode 3 game");
|
||||
}
|
||||
|
||||
// In non-tournament games, only the leader can do this; in a tournament
|
||||
// match, the players don't have control over who the leader is, so we allow
|
||||
// all players to use this command
|
||||
// In non-tournament games, only the leader can do this; in a tournament match, the players don't have control
|
||||
// over who the leader is, so we allow all players to use this command
|
||||
if (!l->tournament_match) {
|
||||
a.check_is_leader();
|
||||
}
|
||||
@@ -2662,8 +2643,8 @@ ChatCommandDefinition cc_swa(
|
||||
a.check_is_game(true);
|
||||
|
||||
a.c->toggle_flag(Client::Flag::SWITCH_ASSIST_ENABLED);
|
||||
send_text_message_fmt(a.c, "$C6Switch assist {}",
|
||||
a.c->check_flag(Client::Flag::SWITCH_ASSIST_ENABLED) ? "enabled" : "disabled");
|
||||
bool enabled = a.c->check_flag(Client::Flag::SWITCH_ASSIST_ENABLED);
|
||||
send_text_message_fmt(a.c, "$C6Switch assist {}", enabled ? "enabled" : "disabled");
|
||||
co_return;
|
||||
});
|
||||
|
||||
@@ -2775,15 +2756,12 @@ ChatCommandDefinition cc_switchchar(
|
||||
a.c->bb_character_index = index;
|
||||
a.c->bb_bank_character_index = index;
|
||||
|
||||
// TODO: This can trigger a client bug where the previous character's
|
||||
// name label object isn't deleted if the leave and join notifications
|
||||
// are received on the same frame. This results in the receiving player
|
||||
// seeing both labels over the new character, with the latest one
|
||||
// appearing on top. We could fix this by requiring each recipient to
|
||||
// reply to a ping between the two commands, similar to how the 64 and
|
||||
// 6x6D commands are split during game joining, but implementing that
|
||||
// here seems not worth the effort given the low likelihood and impact of
|
||||
// this bug.
|
||||
// TODO: This can trigger a client bug where the previous character's name label object isn't deleted if the
|
||||
// leave and join notifications are received on the same frame. This results in the receiving player seeing both
|
||||
// labels over the new character, with the latest one appearing on top. We could fix this by requiring each
|
||||
// recipient to reply to a ping between the two commands, similar to how the 64 and 6x6D commands are split
|
||||
// during game joining, but implementing that here seems not worth the effort given the low likelihood and impact
|
||||
// of this bug.
|
||||
send_complete_player_bb(a.c);
|
||||
send_player_leave_notification(l, a.c->lobby_client_id);
|
||||
s->send_lobby_join_notifications(l, a.c);
|
||||
@@ -2822,9 +2800,7 @@ ChatCommandDefinition cc_unset(
|
||||
ChatCommandDefinition cc_variations(
|
||||
{"$variations"},
|
||||
+[](const Args& a) -> asio::awaitable<void> {
|
||||
// Note: This command is intentionally undocumented, since it's primarily used
|
||||
// for testing. If we ever make it public, we should add some kind of user
|
||||
// feedback (currently it sends no message when it runs).
|
||||
// Note: This command is intentionally undocumented, since it's primarily used for testing
|
||||
a.check_is_proxy(false);
|
||||
a.check_is_game(false);
|
||||
auto s = a.c->require_server_state();
|
||||
@@ -2854,9 +2830,7 @@ static void command_warp(const Args& a, bool is_warpall) {
|
||||
return;
|
||||
}
|
||||
|
||||
Episode episode = a.c->proxy_session
|
||||
? a.c->proxy_session->lobby_episode
|
||||
: a.c->require_lobby()->episode;
|
||||
Episode episode = a.c->proxy_session ? a.c->proxy_session->lobby_episode : a.c->require_lobby()->episode;
|
||||
size_t limit = FloorDefinition::limit_for_episode(episode);
|
||||
if (limit == 0) {
|
||||
return;
|
||||
@@ -2922,15 +2896,16 @@ ChatCommandDefinition cc_what(
|
||||
throw precondition_failed("$C4No items are near you");
|
||||
} else {
|
||||
auto s = a.c->require_server_state();
|
||||
string name = s->describe_item(a.c->version(), nearest_fi->data, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES);
|
||||
string name = s->describe_item(
|
||||
a.c->version(), nearest_fi->data, ItemNameIndex::Flag::INCLUDE_PSO_COLOR_ESCAPES);
|
||||
send_text_message(a.c, name);
|
||||
}
|
||||
co_return;
|
||||
});
|
||||
|
||||
static void whatobj_whatene_fn(const Args& a, bool include_objs, bool include_enes) {
|
||||
// TODO: This probably wouldn't be too hard to implement for proxy sessions.
|
||||
// We already have the map and most of the lobby metadata (episode, etc.)
|
||||
// TODO: This probably wouldn't be too hard to implement for proxy sessions. We already have the map and most of the
|
||||
// lobby metadata (episode, etc.)
|
||||
a.check_is_proxy(false);
|
||||
a.check_is_game(true);
|
||||
auto l = a.c->require_lobby();
|
||||
@@ -2997,9 +2972,8 @@ static void whatobj_whatene_fn(const Args& a, bool include_objs, bool include_en
|
||||
}
|
||||
}
|
||||
|
||||
// Since we check all objects first, nearest_ene will only be set if
|
||||
// there is an enemy closer than all objects. So, we print that if it's
|
||||
// set, and print the object if not.
|
||||
// Since we check all objects first, nearest_ene will only be set if there is an enemy closer than all objects. So,
|
||||
// we print that if it's set, and print the object if not.
|
||||
if (nearest_ene) {
|
||||
const auto* set_entry = nearest_ene->super_ene->version(a.c->version()).set_entry;
|
||||
string type_name = MapFile::name_for_enemy_type(set_entry->base_type, a.c->version(), area);
|
||||
@@ -3116,10 +3090,9 @@ ChatCommandDefinition cc_nativecall(
|
||||
+[](const Args& a) -> asio::awaitable<void> {
|
||||
a.check_debug_enabled();
|
||||
|
||||
// TODO: $nativecall is not implemented on x86 (yet) because there are
|
||||
// multiple calling conventions used within the executable (at least on
|
||||
// Xbox and BB), so we would need a way to specify which calling
|
||||
// convention to use, which would be annoying
|
||||
// TODO: $nativecall is not implemented on x86 (yet) because there are multiple calling conventions used within
|
||||
// the executable (at least on Xbox and BB), so we would need a way to specify which calling convention to use,
|
||||
// which would be annoying
|
||||
if (is_x86(a.c->version())) {
|
||||
throw precondition_failed("Command not supported\non x86 clients");
|
||||
}
|
||||
@@ -3156,7 +3129,7 @@ ChatCommandDefinition cc_nativecall(
|
||||
co_return;
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Dispatch methods
|
||||
|
||||
struct SplitCommand {
|
||||
@@ -3174,15 +3147,13 @@ struct SplitCommand {
|
||||
}
|
||||
};
|
||||
|
||||
// This function is called every time any player sends a chat beginning with a
|
||||
// dollar sign. It is this function's responsibility to see if the chat is a
|
||||
// command, and to execute the command and block the chat if it is.
|
||||
// This function is called every time any player sends a chat message beginning with $. It is this function's
|
||||
// responsibility to see if the chat is a command, and to execute the command and block the chat if it is.
|
||||
asio::awaitable<void> on_chat_command(std::shared_ptr<Client> c, const std::string& text, bool check_permissions) {
|
||||
SplitCommand cmd(text);
|
||||
|
||||
// This function is only called by on_06 if it looks like a chat command
|
||||
// (starts with $, or @ on 11/2000), so we just normalize all commands to $
|
||||
// here
|
||||
// This function is only called by on_06 if it looks like a chat command (starts with $, or @ on 11/2000), so we just
|
||||
// normalize all commands to $ here
|
||||
if (!cmd.name.empty() && cmd.name[0] == '@') {
|
||||
cmd.name[0] = '$';
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ struct ChoiceSearchConfigT {
|
||||
return ret;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
|
||||
using ChoiceSearchConfig = ChoiceSearchConfigT<false>;
|
||||
using ChoiceSearchConfigBE = ChoiceSearchConfigT<true>;
|
||||
check_struct_size(ChoiceSearchConfig, 0x18);
|
||||
|
||||
+29
-36
@@ -25,8 +25,7 @@ static atomic<uint64_t> next_id(1);
|
||||
void Client::set_flags_for_version(Version version, int64_t sub_version) {
|
||||
this->set_flag(Flag::PROXY_CHAT_COMMANDS_ENABLED);
|
||||
|
||||
// BB shares some sub_version values with GC Episode 3, so we handle it
|
||||
// separately first.
|
||||
// BB shares some sub_version values with GC Episode 3, so we handle it separately first.
|
||||
if (version == Version::BB_V4) {
|
||||
this->set_flag(Flag::NO_D6);
|
||||
this->set_flag(Flag::SAVE_ENABLED);
|
||||
@@ -72,8 +71,8 @@ void Client::set_flags_for_version(Version version, int64_t sub_version) {
|
||||
break;
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3:
|
||||
// Some of these versions have send_function_call and some don't; we
|
||||
// have to set these flags later when we get sub_version
|
||||
// Some of these versions have send_function_call and some don't; we have to set these flags later when we
|
||||
// get sub_version
|
||||
break;
|
||||
case Version::XB_V3:
|
||||
// TODO: Do all versions of XB need this flag? US does, at least.
|
||||
@@ -142,8 +141,8 @@ void Client::set_flags_for_version(Version version, int64_t sub_version) {
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE);
|
||||
this->set_flag(Flag::ENCRYPTED_SEND_FUNCTION_CALL);
|
||||
this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH);
|
||||
// sub_version can't be used to tell JP final and Trial Edition apart; we
|
||||
// instead look at header.flag in the 61 command and set the version then.
|
||||
// sub_version can't be used to tell JP final and Trial Edition apart; we instead look at header.flag in the 61
|
||||
// command and set the version then.
|
||||
break;
|
||||
case 0x41: // GC Ep3 US (and BB, but BB is handled above)
|
||||
case 0x42: // GC Ep3 EU 50Hz
|
||||
@@ -191,9 +190,8 @@ Client::Client(
|
||||
should_update_play_time(false) {
|
||||
this->update_channel_name();
|
||||
|
||||
// Don't print data sent to patch clients to the logs. The patch server
|
||||
// protocol is fully understood and data logs for patch clients are generally
|
||||
// more annoying than helpful at this point.
|
||||
// Don't print data sent to patch clients to the logs. The patch server protocol is fully understood and data logs
|
||||
// for patch clients are generally more annoying than helpful at this point.
|
||||
auto s = server->get_state();
|
||||
if (is_patch(this->version()) && s->hide_download_commands) {
|
||||
this->channel->terminal_recv_color = phosg::TerminalFormat::END;
|
||||
@@ -212,9 +210,8 @@ Client::Client(
|
||||
this->reschedule_save_game_data_timer();
|
||||
this->reschedule_ping_and_timeout_timers();
|
||||
|
||||
// Don't print data sent to patch clients to the logs. The patch server
|
||||
// protocol is fully understood and data logs for patch clients are generally
|
||||
// more annoying than helpful at this point.
|
||||
// Don't print data sent to patch clients to the logs. The patch server protocol is fully understood and data logs
|
||||
// for patch clients are generally more annoying than helpful at this point.
|
||||
if ((s->hide_download_commands) &&
|
||||
((this->version() == Version::PC_PATCH) || (this->version() == Version::BB_PATCH))) {
|
||||
this->channel->terminal_recv_color = phosg::TerminalFormat::END;
|
||||
@@ -298,9 +295,8 @@ void Client::reschedule_ping_and_timeout_timers() {
|
||||
}
|
||||
|
||||
void Client::convert_account_to_temporary_if_nte() {
|
||||
// If the session is a prototype version and the account was created and we
|
||||
// should use a temporary account instead, delete the permanent account and
|
||||
// replace it with a temporary account.
|
||||
// If the session is a prototype version and the account was created and we should use a temporary account instead,
|
||||
// delete the permanent account and replace it with a temporary account.
|
||||
auto s = this->require_server_state();
|
||||
if (s->use_temp_accounts_for_prototypes && this->login->account_was_created && is_any_nte(this->version())) {
|
||||
this->log.info_f("Client is a prototype version and the account was created during this session; converting permanent account to temporary account");
|
||||
@@ -353,8 +349,7 @@ shared_ptr<const TeamIndex::Team> Client::team() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The team membership is valid, but the player name may be different; update
|
||||
// the team membership if needed
|
||||
// The team membership is valid, but the player name may be different; update the team membership if needed
|
||||
if (p) {
|
||||
auto& m = member_it->second;
|
||||
string name = p->disp.name.decode(this->language());
|
||||
@@ -601,8 +596,8 @@ void Client::save_character_file() {
|
||||
throw logic_error("no character file loaded");
|
||||
}
|
||||
if (this->should_update_play_time) {
|
||||
// This is slightly inaccurate, since fractions of a second are truncated
|
||||
// off each time we save. I'm lazy, so insert shrug emoji here.
|
||||
// This is slightly inaccurate, since fractions of a second are truncated off each time we save. I'm lazy, so
|
||||
// insert shrug emoji here
|
||||
uint64_t t = phosg::now();
|
||||
uint64_t seconds = (t - this->last_play_time_update) / 1000000;
|
||||
this->character_data->play_time_seconds += seconds;
|
||||
@@ -642,8 +637,7 @@ void Client::create_battle_overlay(shared_ptr<const BattleRules> rules, shared_p
|
||||
this->overlay_character_data->inventory.remove_all_items_of_type(3);
|
||||
}
|
||||
if (rules->replace_char) {
|
||||
// TODO: Shouldn't we clear other material usage here? It looks like the
|
||||
// original code doesn't, but that seems wrong.
|
||||
// TODO: Shouldn't we clear other material usage here? Looks like the original code doesn't, but that seems wrong.
|
||||
this->overlay_character_data->inventory.hp_from_materials = 0;
|
||||
this->overlay_character_data->inventory.tp_from_materials = 0;
|
||||
|
||||
@@ -678,7 +672,8 @@ void Client::create_battle_overlay(shared_ptr<const BattleRules> rules, shared_p
|
||||
}
|
||||
}
|
||||
|
||||
void Client::create_challenge_overlay(Version version, size_t template_index, shared_ptr<const LevelTable> level_table) {
|
||||
void Client::create_challenge_overlay(
|
||||
Version version, size_t template_index, shared_ptr<const LevelTable> level_table) {
|
||||
auto p = this->character_file(true, false);
|
||||
const auto& tpl = get_challenge_template_definition(version, p->disp.visual.class_flags, template_index);
|
||||
|
||||
@@ -700,9 +695,11 @@ void Client::create_challenge_overlay(Version version, size_t template_index, sh
|
||||
level_table->reset_to_base(overlay->disp.stats, overlay->disp.visual.char_class);
|
||||
level_table->advance_to_level(overlay->disp.stats, tpl.level, overlay->disp.visual.char_class);
|
||||
|
||||
const auto& stats_delta = level_table->stats_delta_for_level(
|
||||
overlay->disp.visual.char_class, overlay->disp.stats.level);
|
||||
overlay->disp.stats.esp = 40;
|
||||
overlay->disp.stats.unknown_a3 = 10.0;
|
||||
overlay->disp.stats.experience = level_table->stats_delta_for_level(overlay->disp.visual.char_class, overlay->disp.stats.level).experience;
|
||||
overlay->disp.stats.experience = stats_delta.experience;
|
||||
overlay->disp.stats.meseta = 0;
|
||||
overlay->clear_all_material_usage();
|
||||
for (size_t z = 0; z < 0x13; z++) {
|
||||
@@ -762,10 +759,9 @@ std::shared_ptr<PlayerBank> Client::bank_file(bool allow_load) {
|
||||
this->bank_data->load(f.get());
|
||||
this->log.info_f("Loaded bank data from {}", filename);
|
||||
} catch (const phosg::cannot_open_file&) {
|
||||
// If there isn't a psobank file, use the loaded character data if the
|
||||
// bank character index matches the current character index (that is, we
|
||||
// should use the current character's bank); otherwise, load the
|
||||
// corresponding character and parse the bank from that character file
|
||||
// If there isn't a psobank file, use the loaded character data if the bank character index matches the current
|
||||
// character index (that is, we should use the current character's bank); otherwise, load the corresponding
|
||||
// character and parse the bank from that character file
|
||||
if (this->bb_bank_character_index == this->bb_character_index) {
|
||||
this->bank_data = std::make_shared<PlayerBank>(this->character_file(true, false)->bank);
|
||||
this->log.info_f("Using bank data from loaded character");
|
||||
@@ -891,8 +887,7 @@ void Client::load_all_files() {
|
||||
this->character_data = psochar.character_file;
|
||||
this->log.info_f("Loaded character data from {}", char_filename);
|
||||
|
||||
// If there was no .psosys file, use the system file from the .psochar
|
||||
// file instead
|
||||
// If there was no .psosys file, use the system file from the .psochar file instead
|
||||
if (!this->system_data) {
|
||||
if (!psochar.system_file) {
|
||||
throw logic_error("account system data not present, and also not loaded from psochar file");
|
||||
@@ -920,8 +915,7 @@ void Client::load_all_files() {
|
||||
}
|
||||
}
|
||||
|
||||
// If any of the above files are still missing, try to load from .nsa/.nsc
|
||||
// files instead
|
||||
// If any of the above files are still missing, try to load from .nsa/.nsc files instead
|
||||
if (!this->system_data || (!this->character_data && (this->bb_character_index >= 0)) || !this->guild_card_data) {
|
||||
string nsa_filename = this->legacy_account_filename();
|
||||
shared_ptr<LegacySavedAccountDataBB> nsa_data;
|
||||
@@ -987,9 +981,8 @@ void Client::load_all_files() {
|
||||
}
|
||||
}
|
||||
|
||||
// The system and Guild Card files can be auto-created if they can't be
|
||||
// loaded. After this, system_data and guild_card_data are always non-null,
|
||||
// but character_data may still be null
|
||||
// The system and Guild Card files can be auto-created if they can't be loaded. After this, system_data and
|
||||
// guild_card_data are always non-null, but character_data may still be null
|
||||
if (!this->system_data) {
|
||||
this->system_data = make_shared<PSOBBBaseSystemFile>();
|
||||
auto s = this->require_server_state();
|
||||
@@ -1024,8 +1017,8 @@ void Client::load_all_files() {
|
||||
this->login->account->save();
|
||||
this->last_play_time_update = phosg::now();
|
||||
if (this->bb_character_index >= 0) {
|
||||
// Note that bank_file() can't recur infinitely here because
|
||||
// character_file is already set; it will not call load_all_files() again
|
||||
// Note that bank_file() can't recur infinitely here because character_file is already set; it will not call
|
||||
// load_all_files() again
|
||||
this->bank_file()->enforce_stack_limits(stack_limits);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-17
@@ -189,8 +189,7 @@ public:
|
||||
std::unordered_set<uint32_t> blocked_senders;
|
||||
std::unique_ptr<PlayerDispDataDCPCV3> v1_v2_last_reported_disp;
|
||||
std::shared_ptr<Parsed6x70Data> last_reported_6x70;
|
||||
// These are null unless the client is within the trade sequence (D0-D4 or EE
|
||||
// commands)
|
||||
// These are null unless the client is within the trade sequence (D0-D4 or EE commands)
|
||||
std::unique_ptr<PendingItemTrade> pending_item_trade;
|
||||
std::unique_ptr<PendingCardTrade> pending_card_trade;
|
||||
uint32_t telepipe_lobby_id = 0;
|
||||
@@ -203,12 +202,10 @@ public:
|
||||
uint8_t schtserv_response_register = 0;
|
||||
uint32_t next_exp_value = 0;
|
||||
bool can_chat = true;
|
||||
// NOTE: If you add any new optional promises here, make sure to also add
|
||||
// them to cancel_pending_promises.
|
||||
// NOTE: Entries in this queue can be nullptr; that represents a B2 command
|
||||
// sent by the remote server during a proxy session. We can't just omit those
|
||||
// from the queue entirely, because if we did, we could end up sending the
|
||||
// wrong B3 response back.
|
||||
// NOTE: If you add any new optional promises here, make sure to also add them to cancel_pending_promises.
|
||||
// NOTE: Entries in this queue can be nullptr; that represents a B2 command sent by the remote server during a proxy
|
||||
// session. We can't just omit those from the queue entirely, because if we did, we could end up sending the wrong B3
|
||||
// response back.
|
||||
std::deque<std::shared_ptr<AsyncPromise<C_ExecuteCodeResult_B3>>> function_call_response_queue;
|
||||
std::shared_ptr<AsyncPromise<GetPlayerInfoResult>> character_data_ready_promise;
|
||||
std::shared_ptr<AsyncPromise<void>> enable_save_promise;
|
||||
@@ -216,10 +213,7 @@ public:
|
||||
// File loading state
|
||||
std::unordered_map<std::string, std::shared_ptr<const std::string>> sending_files;
|
||||
|
||||
Client(
|
||||
std::shared_ptr<GameServer> server,
|
||||
std::shared_ptr<Channel> channel,
|
||||
ServerBehavior server_behavior);
|
||||
Client(std::shared_ptr<GameServer> server, std::shared_ptr<Channel> channel, ServerBehavior server_behavior);
|
||||
~Client();
|
||||
|
||||
void update_channel_name();
|
||||
@@ -258,8 +252,6 @@ public:
|
||||
|
||||
void convert_account_to_temporary_if_nte();
|
||||
|
||||
void sync_config();
|
||||
|
||||
std::shared_ptr<ServerState> require_server_state() const;
|
||||
std::shared_ptr<Lobby> require_lobby() const;
|
||||
|
||||
@@ -353,9 +345,8 @@ public:
|
||||
void cancel_pending_promises();
|
||||
|
||||
private:
|
||||
// The overlay character data is used in battle and challenge modes, when
|
||||
// character data is temporarily replaced in-game. In other play modes and in
|
||||
// lobbies, overlay_character_data is null.
|
||||
// The overlay character data is used in battle and challenge modes, when character data is temporarily replaced
|
||||
// in-game. In other play modes and in lobbies, overlay_character_data is null.
|
||||
std::shared_ptr<PSOBBBaseSystemFile> system_data;
|
||||
std::shared_ptr<PSOBBCharacterFile> overlay_character_data;
|
||||
std::shared_ptr<PSOBBCharacterFile> character_data;
|
||||
|
||||
+14
-26
@@ -86,26 +86,17 @@ struct VectorXYZF {
|
||||
inline VectorXYZF rotate_x(double angle) const {
|
||||
double s = sin(angle);
|
||||
double c = cos(angle);
|
||||
return VectorXYZF{
|
||||
this->x,
|
||||
this->y * c - this->z * s,
|
||||
this->y * s + this->z * c};
|
||||
return VectorXYZF{this->x, this->y * c - this->z * s, this->y * s + this->z * c};
|
||||
}
|
||||
inline VectorXYZF rotate_y(double angle) const {
|
||||
double s = sin(angle);
|
||||
double c = cos(angle);
|
||||
return VectorXYZF{
|
||||
this->x * c + this->z * s,
|
||||
this->y,
|
||||
-this->x * s + this->z * c};
|
||||
return VectorXYZF{this->x * c + this->z * s, this->y, -this->x * s + this->z * c};
|
||||
}
|
||||
inline VectorXYZF rotate_z(double angle) const {
|
||||
double s = sin(angle);
|
||||
double c = cos(angle);
|
||||
return VectorXYZF{
|
||||
this->x * c - this->y * s,
|
||||
this->x * s + this->y * c,
|
||||
this->z};
|
||||
return VectorXYZF{this->x * c - this->y * s, this->x * s + this->y * c, this->z};
|
||||
}
|
||||
|
||||
inline std::string str() const {
|
||||
@@ -141,23 +132,20 @@ check_struct_size(ArrayRefBE, 8);
|
||||
template <bool BE>
|
||||
struct RELFileFooterT {
|
||||
static constexpr bool IsBE = BE;
|
||||
// Relocations is a list of words (le_uint16_t on DC/PC/XB/BB, be_uint16_t on
|
||||
// GC) containing the number of doublewords (uint32_t) to skip for each
|
||||
// relocation. The relocation pointer starts at the beginning of the file
|
||||
// data, and advances by the value of one relocation word (times 4) before
|
||||
// each relocation. At each relocated doubleword, the address of the first
|
||||
// byte of the file is added to the existing value.
|
||||
// For example, if the file data contains the following data (where R
|
||||
// specifies doublewords to relocate):
|
||||
// Relocations is a list of words (le_uint16_t on DC/PC/XB/BB, be_uint16_t on GC) containing the number of
|
||||
// doublewords (uint32_t) to skip for each relocation. The relocation pointer starts at the beginning of the file
|
||||
// data, and advances by the value of one relocation word (times 4) before each relocation. At each relocated
|
||||
// doubleword, the address of the first byte of the file is added to the existing value.
|
||||
//
|
||||
// For example, if the file data contains the following data (where R specifies doublewords to relocate):
|
||||
// RR RR RR RR ?? ?? ?? ?? ?? ?? ?? ?? RR RR RR RR
|
||||
// RR RR RR RR ?? ?? ?? ?? RR RR RR RR
|
||||
// then the relocation words should be 0000, 0003, 0001, and 0002.
|
||||
// If there is a small number of relocations, they may be placed in the unused
|
||||
// fields of this structure to save space and/or confuse reverse engineers.
|
||||
// The game never accesses the last 12 bytes of this structure unless
|
||||
// relocations_offset points there, so those 12 bytes may also be omitted
|
||||
// entirely in situations (e.g. in the B2 command, without changing code_size,
|
||||
// so code_size would technically extend beyond the end of the B2 command).
|
||||
//
|
||||
// If there is a small number of relocations, they may be placed in the unused fields of this structure to save space
|
||||
// and/or confuse reverse engineers. The game never accesses the last 12 bytes of this structure unless
|
||||
// relocations_offset points there, so those 12 bytes may also be omitted entirely in some situations (e.g. in the B2
|
||||
// command, without changing code_size, so code_size would technically extend beyond the end of the B2 command).
|
||||
U32T<BE> relocations_offset = 0;
|
||||
U32T<BE> num_relocations = 0;
|
||||
parray<U32T<BE>, 2> unused1;
|
||||
|
||||
+34
-24
@@ -266,10 +266,10 @@ void CommonItemSet::Table::print(FILE* stream) const {
|
||||
this->special_mult[z], this->special_percent[z]);
|
||||
}
|
||||
|
||||
phosg::fwrite_fmt(stream, " Tool class table:\n");
|
||||
phosg::fwrite_fmt(stream, " CS A1 A2 A3 A4 A5 A6 A7 A8 A9 A10\n");
|
||||
phosg::fwrite_fmt(stream, "Tool class table:\n");
|
||||
phosg::fwrite_fmt(stream, " CS A1 A2 A3 A4 A5 A6 A7 A8 A9 A10\n");
|
||||
for (size_t tool_class = 0; tool_class < this->tool_class_prob_table.size(); tool_class++) {
|
||||
phosg::fwrite_fmt(stream, " {:02X}", tool_class);
|
||||
phosg::fwrite_fmt(stream, " {:02X}", tool_class);
|
||||
for (size_t area_norm = 0; area_norm < 10; area_norm++) {
|
||||
phosg::fwrite_fmt(stream, " {:5}", this->tool_class_prob_table[tool_class][area_norm]);
|
||||
}
|
||||
@@ -298,10 +298,10 @@ void CommonItemSet::Table::print(FILE* stream) const {
|
||||
"MEGID ",
|
||||
};
|
||||
|
||||
phosg::fwrite_fmt(stream, " Technique table:\n");
|
||||
phosg::fwrite_fmt(stream, " TECH A1 A2 A3 A4 A5 A6 A7 A8 A9 A10\n");
|
||||
phosg::fwrite_fmt(stream, "Technique table:\n");
|
||||
phosg::fwrite_fmt(stream, " TECH A1 A2 A3 A4 A5 A6 A7 A8 A9 A10\n");
|
||||
for (size_t tech_num = 0; tech_num < this->technique_index_prob_table.size(); tech_num++) {
|
||||
phosg::fwrite_fmt(stream, " {:02X}:{}", tech_num, technique_names[tech_num]);
|
||||
phosg::fwrite_fmt(stream, " {:02X}:{}", tech_num, technique_names[tech_num]);
|
||||
for (size_t area_norm = 0; area_norm < 10; area_norm++) {
|
||||
uint16_t prob = this->technique_index_prob_table[tech_num][area_norm];
|
||||
if (prob) {
|
||||
@@ -316,24 +316,24 @@ void CommonItemSet::Table::print(FILE* stream) const {
|
||||
fputc('\n', stream);
|
||||
}
|
||||
|
||||
phosg::fwrite_fmt(stream, " Armor/shield type bias: {}\n", this->armor_or_shield_type_bias);
|
||||
phosg::fwrite_fmt(stream, "Armor/shield type bias: {}\n", this->armor_or_shield_type_bias);
|
||||
|
||||
phosg::fwrite_fmt(stream, " Armor/shield type index table:\n");
|
||||
phosg::fwrite_fmt(stream, " TY PROB\n");
|
||||
phosg::fwrite_fmt(stream, "Armor/shield type index table:\n");
|
||||
phosg::fwrite_fmt(stream, " TY PROB\n");
|
||||
for (size_t z = 0; z < 5; z++) {
|
||||
phosg::fwrite_fmt(stream, " {:02X} {:3}%\n", z, this->armor_shield_type_index_prob_table[z]);
|
||||
phosg::fwrite_fmt(stream, " {:02X} {:3}%\n", z, this->armor_shield_type_index_prob_table[z]);
|
||||
}
|
||||
|
||||
phosg::fwrite_fmt(stream, " Armor/shield slot count table:\n");
|
||||
phosg::fwrite_fmt(stream, " #S PROB\n");
|
||||
phosg::fwrite_fmt(stream, "Armor/shield slot count table:\n");
|
||||
phosg::fwrite_fmt(stream, " #S PROB\n");
|
||||
for (size_t z = 0; z < 5; z++) {
|
||||
phosg::fwrite_fmt(stream, " {:02X} {:3}%\n", z, this->armor_slot_count_prob_table[z]);
|
||||
phosg::fwrite_fmt(stream, " {:02X} {:3}%\n", z, this->armor_slot_count_prob_table[z]);
|
||||
}
|
||||
|
||||
phosg::fwrite_fmt(stream, " Unit maximum stars table:\n");
|
||||
phosg::fwrite_fmt(stream, " AR #*\n");
|
||||
phosg::fwrite_fmt(stream, "Unit maximum stars table:\n");
|
||||
phosg::fwrite_fmt(stream, " AR #*\n");
|
||||
for (size_t z = 0; z < 10; z++) {
|
||||
phosg::fwrite_fmt(stream, " {:02X} {:3}\n", z, this->unit_max_stars_table[z]);
|
||||
phosg::fwrite_fmt(stream, " {:02X} {:3}\n", z, this->unit_max_stars_table[z]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,7 +534,10 @@ void CommonItemSet::print(FILE* stream) const {
|
||||
try {
|
||||
auto table = this->get_table(episode, mode, difficulty, section_id);
|
||||
phosg::fwrite_fmt(stream, "============ {} {} {} {}\n",
|
||||
name_for_mode(mode), name_for_episode(episode), name_for_difficulty(difficulty), name_for_section_id(section_id));
|
||||
name_for_mode(mode),
|
||||
name_for_episode(episode),
|
||||
name_for_difficulty(difficulty),
|
||||
name_for_section_id(section_id));
|
||||
table->print(stream);
|
||||
} catch (const runtime_error&) {
|
||||
}
|
||||
@@ -564,13 +567,22 @@ void CommonItemSet::print_diff(FILE* stream, const CommonItemSet& other) const {
|
||||
continue;
|
||||
} else if (!this_table) {
|
||||
phosg::fwrite_fmt(stream, "> Table present in other but not this: {} {} {} {}\n",
|
||||
name_for_mode(mode), name_for_episode(episode), name_for_difficulty(difficulty), name_for_section_id(section_id));
|
||||
name_for_mode(mode),
|
||||
name_for_episode(episode),
|
||||
name_for_difficulty(difficulty),
|
||||
name_for_section_id(section_id));
|
||||
} else if (!other_table) {
|
||||
phosg::fwrite_fmt(stream, "> Table present in this but not other: {} {} {} {}\n",
|
||||
name_for_mode(mode), name_for_episode(episode), name_for_difficulty(difficulty), name_for_section_id(section_id));
|
||||
name_for_mode(mode),
|
||||
name_for_episode(episode),
|
||||
name_for_difficulty(difficulty),
|
||||
name_for_section_id(section_id));
|
||||
} else if (*this_table != *other_table) {
|
||||
phosg::fwrite_fmt(stream, "> Tables do not match: {} {} {} {}\n",
|
||||
name_for_mode(mode), name_for_episode(episode), name_for_difficulty(difficulty), name_for_section_id(section_id));
|
||||
name_for_mode(mode),
|
||||
name_for_episode(episode),
|
||||
name_for_difficulty(difficulty),
|
||||
name_for_section_id(section_id));
|
||||
this_table->print_diff(stream, *other_table);
|
||||
}
|
||||
}
|
||||
@@ -665,8 +677,7 @@ shared_ptr<const CommonItemSet::Table> CommonItemSet::get_table(
|
||||
|
||||
AFSV2CommonItemSet::AFSV2CommonItemSet(
|
||||
std::shared_ptr<const std::string> pt_afs_data, std::shared_ptr<const std::string> ct_afs_data) {
|
||||
// Each AFS file has 40 entries (30 on v1); the first 10 are for Normal, then
|
||||
// Hard, etc.
|
||||
// Each AFS file has 40 entries (30 on v1); the first 10 are for Normal, then Hard, etc.
|
||||
{
|
||||
AFSArchive pt_afs(pt_afs_data);
|
||||
bool include_ultimate;
|
||||
@@ -692,8 +703,7 @@ AFSV2CommonItemSet::AFSV2CommonItemSet(
|
||||
}
|
||||
}
|
||||
|
||||
// ItemCT AFS files also have 40 entries, but only the 0th, 10th, 20th, and
|
||||
// 30th are used (section_id is ignored)
|
||||
// ItemCT AFS files also have 40 entries, but only the 0th, 10th, 20th, and 30th are used (section_id is ignored)
|
||||
if (ct_afs_data) {
|
||||
AFSArchive ct_afs(ct_afs_data);
|
||||
bool include_ultimate;
|
||||
|
||||
+97
-128
@@ -64,54 +64,43 @@ public:
|
||||
|
||||
template <bool BE>
|
||||
struct OffsetsT {
|
||||
// This data structure uses index probability tables in multiple places. An
|
||||
// index probability table is a table where each entry holds the probability
|
||||
// that that entry's index is used. For example, if the armor slot count
|
||||
// probability table contains [77, 17, 5, 1, 0], this means there is a 77%
|
||||
// chance of no slots, 17% chance of 1 slot, 5% chance of 2 slots, 1% chance
|
||||
// of 3 slots, and no chance of 4 slots. The values in index probability
|
||||
// tables do not have to add up to 100; the game sums all of them and
|
||||
// chooses a random number less than that maximum.
|
||||
// This data structure uses index probability tables in multiple places. An index probability table is a table
|
||||
// where each entry holds the probability that that entry's index is used. For example, if the armor slot count
|
||||
// probability table contains [77, 17, 5, 1, 0], this means there is a 77% chance of no slots, 17% chance of 1
|
||||
// slot, 5% chance of 2 slots, 1% chance of 3 slots, and no chance of 4 slots. The values in index probability
|
||||
// tables do not have to add up to 100; the game sums all of them and chooses a random number less than that
|
||||
// maximum.
|
||||
|
||||
// The area (floor) number is used in many places as well. Unlike the normal
|
||||
// area numbers, which start with Pioneer 2, the area numbers in this
|
||||
// structure start with Forest 1, and boss areas are treated as the first
|
||||
// area of the next section (so De Rol Le has Mines 1 drops, for example).
|
||||
// Final boss areas are treated as the last non-boss area (so Dark Falz
|
||||
// boxes are like Ruins 3 boxes). We refer to these adjusted area numbers as
|
||||
// (area - 1).
|
||||
// The area (floor) number is used in many places as well. Unlike the normal area numbers, which start with
|
||||
// Pioneer 2, the area numbers in this structure start with Forest 1, and boss areas are treated as the first
|
||||
// area of the next section (so De Rol Le has Mines 1 drops, for example). Final boss areas are treated as the
|
||||
// last non-boss area (so Dark Falz boxes are like Ruins 3 boxes). We refer to these adjusted area numbers as
|
||||
// (area - 1), or area_norm.
|
||||
|
||||
// This index probability table determines the types of non-rare weapons.
|
||||
// The indexes in this table correspond to the non-rare weapon types 01
|
||||
// through 0C (Saber through Wand).
|
||||
// This index probability table determines the types of non-rare weapons. The indexes in this table correspond to
|
||||
// the non-rare weapon types 01 through 0C (Saber through Wand).
|
||||
// V2/V3: -> parray<uint8_t, 0x0C>
|
||||
/* 00 */ U32T<BE> base_weapon_type_prob_table_offset;
|
||||
|
||||
// This table specifies the base subtype for each weapon type. Negative
|
||||
// values here mean that the weapon cannot be found in the first N areas (so
|
||||
// -2, for example, means that the weapon never appears in Forest 1 or 2 at
|
||||
// all). Nonnegative values here mean the subtype can be found in all areas,
|
||||
// and specify the base subtype (usually in the range [0, 4]). The subtype
|
||||
// of weapon that actually appears depends on this value and a value from
|
||||
// the following table.
|
||||
// This table specifies the base subtype for each weapon type. Negative values here mean that the weapon cannot
|
||||
// be found in the first N areas (so -2, for example, means that the weapon never appears in Forest 1 or 2 at
|
||||
// all). Nonnegative values here mean the subtype can be found in all areas, and specify the base subtype
|
||||
// (usually in the range [0, 4]). The subtype of weapon that actually appears depends on this value and a value
|
||||
// from the following table.
|
||||
// V2/V3: -> parray<int8_t, 0x0C>
|
||||
/* 04 */ U32T<BE> subtype_base_table_offset;
|
||||
|
||||
// This table specifies how many areas each weapon subtype appears in. For
|
||||
// example, if Sword (subtype 02, which is index 1 in this table and the
|
||||
// table above) has a subtype base of -2 and a subtype area length of 4,
|
||||
// then Sword items can be found when area - 1 is 2, 3, 4, or 5 (Cave 1
|
||||
// through Mine 1), and Gigush (the next sword subtype) can be found in Mine
|
||||
// 1 through Ruins 3.
|
||||
// This table specifies how many areas each weapon subtype appears in. For example, if Sword (subtype 02, which
|
||||
// is index 1 in this table and the table above) has a subtype base of -2 and a subtype area length of 4, then
|
||||
// Sword items can be found when area - 1 is 2, 3, 4, or 5 (Cave 1 through Mine 1), and Gigush (the next sword
|
||||
// subtype) can be found in Mine 1 through Ruins 3.
|
||||
// V2/V3: -> parray<uint8_t, 0x0C>
|
||||
/* 08 */ U32T<BE> subtype_area_length_table_offset;
|
||||
|
||||
// This index probability table specifies how likely each possible grind
|
||||
// value is. The table is indexed as [grind][subtype_area_index], where the
|
||||
// subtype area index is how many areas the player is beyond the first area
|
||||
// in which the subtype can first be found (clamped to [0, 3]). To continue
|
||||
// the example above, in Cave 3, subtype_area_index would be 2, since Swords
|
||||
// can first be found two areas earlier in Cave 1.
|
||||
// This index probability table specifies how likely each possible grind value is. The table is indexed as
|
||||
// [grind][subtype_area_index], where the subtype area index is how many areas the player is beyond the first
|
||||
// area in which the subtype can first be found (clamped to [0, 3]). To continue the example above, in Cave 3,
|
||||
// subtype_area_index would be 2, since Swords can first be found two areas earlier in Cave 1.
|
||||
// For example, this table could look like this:
|
||||
// [64 1E 19 14] // Chance of getting a grind +0
|
||||
// [00 1E 17 0F] // Chance of getting a grind +1
|
||||
@@ -121,74 +110,66 @@ public:
|
||||
// V2/V3: -> parray<parray<uint8_t, 4>, 9>
|
||||
/* 0C */ U32T<BE> grind_prob_table_offset;
|
||||
|
||||
// TODO: Figure out exactly how this table is used. Anchor: 80106D34
|
||||
// This index probability table specifies how likely each type of armor or shield is. The general formula is:
|
||||
// data1[2] = max((area_norm + (result from this table) + armor_or_shield_type_bias - 3), 0)
|
||||
// In this way, (armor_or_shield_type_bias + area_norm - 3) can be thought of as the "base" value for each area,
|
||||
// and this table specifies how likely the armor/shield is to be "upgraded" from that value.
|
||||
// V2/V3: -> parray<uint8_t, 0x05>
|
||||
/* 10 */ U32T<BE> armor_shield_type_index_prob_table_offset;
|
||||
|
||||
// This index probability table specifies how common each possible slot
|
||||
// count is for armor drops.
|
||||
// This index probability table specifies how common each possible slot count is for armor drops.
|
||||
// V2/V3: -> parray<uint8_t, 0x05>
|
||||
/* 14 */ U32T<BE> armor_slot_count_prob_table_offset;
|
||||
|
||||
// This array (indexed by enemy_type) specifies the range of meseta values
|
||||
// that each enemy can drop.
|
||||
// This array (indexed by enemy_type) specifies the range of meseta values that each enemy can drop.
|
||||
// V2/V3: -> parray<Range<U16T>, 0x64>
|
||||
/* 18 */ U32T<BE> enemy_meseta_ranges_offset;
|
||||
|
||||
// Each byte in this table (indexed by enemy_type) represents the percent
|
||||
// chance that the enemy drops anything at all. (This check is done before
|
||||
// the rare drop check, so the chance of getting a rare item from an enemy
|
||||
// is essentially this probability multiplied by the rare drop rate.)
|
||||
// Each byte in this table (indexed by enemy_type) represents the percent chance that the enemy drops anything at
|
||||
// all. (This check is done before the rare drop check, so the chance of getting a rare item from an enemy is
|
||||
// essentially this probability multiplied by the rare drop rate.)
|
||||
// V2/V3: -> parray<uint8_t, 0x64>
|
||||
/* 1C */ U32T<BE> enemy_type_drop_probs_offset;
|
||||
|
||||
// Each byte in this table (indexed by enemy_type) represents the class of
|
||||
// item that the enemy can drop. The values are:
|
||||
// 00 = weapon
|
||||
// 01 = armor
|
||||
// 02 = shield
|
||||
// 03 = unit
|
||||
// 04 = tool
|
||||
// 05 = meseta
|
||||
// Anything else = no item
|
||||
// Each byte in this table (indexed by enemy_type) represents the class of item that can drop. The values are:
|
||||
// 00 = weapon
|
||||
// 01 = armor
|
||||
// 02 = shield
|
||||
// 03 = unit
|
||||
// 04 = tool
|
||||
// 05 = meseta
|
||||
// Anything else = no item
|
||||
// V2/V3: -> parray<uint8_t, 0x64>
|
||||
/* 20 */ U32T<BE> enemy_item_classes_offset;
|
||||
|
||||
// This table (indexed by area - 1) specifies the ranges of meseta values
|
||||
// that can drop from boxes.
|
||||
// This table (indexed by area - 1) specifies the ranges of meseta values that can drop from boxes.
|
||||
// V2/V3: -> parray<Range<U16T>, 0x0A>
|
||||
/* 24 */ U32T<BE> box_meseta_ranges_offset;
|
||||
|
||||
// This array specifies the chance that a rare weapon will have each
|
||||
// possible bonus value. This is indexed as [(bonus_value - 10 / 5)][spec],
|
||||
// so the first row refers the probability of getting a -10% bonus, the next
|
||||
// row is the chance of getting -5%, etc., all the way up to +100%. For
|
||||
// non-rare items, spec is determined randomly based on the following field;
|
||||
// for rare items, spec is always 5.
|
||||
// This array specifies the chance that a rare weapon will have each possible bonus value. This is indexed as
|
||||
// [(bonus_value - 10 / 5)][spec], so the first row refers the probability of getting a -10% bonus, the next row
|
||||
// is the chance of getting -5%, etc., all the way up to +100%. For non-rare items (or all items on v1/v2), spec
|
||||
// is determined randomly based on the following field; for rare items on v3+, spec is always 5.
|
||||
// V2: -> parray<parray<uint8_t, 5>, 0x17>
|
||||
// V3: -> parray<parray<U16T, 6>, 0x17>
|
||||
/* 28 */ U32T<BE> bonus_value_prob_table_offset;
|
||||
|
||||
// This array specifies the value of spec to be used in the above lookup for
|
||||
// non-rare items. This is NOT an index probability table; this is a direct
|
||||
// lookup with indexes [bonus_index][area - 1]. A value of 0xFF in any byte
|
||||
// of this array prevents any weapon from having a bonus in that slot.
|
||||
// For example, the array might look like this:
|
||||
// This array specifies the value of spec to be used in the above lookup for non-rare items. This is NOT an index
|
||||
// probability table; this is a direct lookup with indexes [bonus_index][area - 1]. A value of 0xFF in any byte
|
||||
// of this array prevents any weapon from having a bonus in that slot. An example table might look like this:
|
||||
// [00 00 00 01 01 01 01 02 02 02]
|
||||
// [FF FF FF 00 00 00 01 01 01 01]
|
||||
// [FF FF FF FF FF FF FF FF FF 00]
|
||||
// F1 F2 C1 C2 C3 M1 M2 R1 R2 R3 // (Episode 1 areas, for reference)
|
||||
// In this example, spec is 0, 1, or 2 in all cases where a weapon can have
|
||||
// a bonus. In Forest 1 and 2 and Cave 1, weapons may have at most one
|
||||
// bonus; in all other areas except Ruins 3, they can have at most two
|
||||
// bonuses, and in Ruins 3, they can have up to three bonuses.
|
||||
// In this example, spec is 0, 1, or 2 in all cases where a weapon can have a bonus. In Forest 1 and 2 and Cave
|
||||
// 1, weapons may have at most one bonus; in all other areas except Ruins 3, they can have at most two bonuses,
|
||||
// and in Ruins 3, they can have up to three bonuses.
|
||||
// V2/V3: // -> parray<parray<uint8_t, 10>, 3>
|
||||
/* 2C */ U32T<BE> nonrare_bonus_prob_spec_offset;
|
||||
|
||||
// This array specifies the chance that a weapon will have each bonus type.
|
||||
// The table is indexed as [bonus_type][area - 1] for non-rare items; for
|
||||
// rare items, a random value in the range [0, 9] is used instead of
|
||||
// (area - 1).
|
||||
// This array specifies the chance that a weapon will have each bonus type. The table is indexed as
|
||||
// [bonus_type][area - 1] for non-rare items; for rare items, a random value in the range [0, 9] is used instead
|
||||
// of (area - 1).
|
||||
// For example, the table might look like this:
|
||||
// [46 46 3F 3E 3E 3D 3C 3C 3A 3A] // Chance of getting no bonus
|
||||
// [14 14 0A 0A 09 02 02 04 05 05] // Chance of getting Native bonus
|
||||
@@ -200,54 +181,50 @@ public:
|
||||
// V2/V3: -> parray<parray<uint8_t, 10>, 6>
|
||||
/* 30 */ U32T<BE> bonus_type_prob_table_offset;
|
||||
|
||||
// This array (indexed by area - 1) specifies a multiplier of used in
|
||||
// special ability determination. It seems this uses the star values from
|
||||
// ItemPMT, but not yet clear exactly in what way.
|
||||
// TODO: Figure out exactly what this does. Anchor: 80106FEC
|
||||
// This array (indexed by area - 1) specifies a parameter used in weapon special generation. If the sampled value
|
||||
// from this table is 0, no special is generated. Otherwise, a random floating-point value W in the range [0,
|
||||
// special_mult] is generated and truncated to an integer. If this value is greater than 3, no special is
|
||||
// generated; otherwise, a random special worth (W + 1) stars is chosen. It seems Sega only intended special_mult
|
||||
// to be in the range [0, 4], but values greater than 4 will work, and will simply increase the probability of
|
||||
// getting no special.
|
||||
// V2/V3: -> parray<uint8_t, 0x0A>
|
||||
/* 34 */ U32T<BE> special_mult_offset;
|
||||
|
||||
// This array (indexed by area - 1) specifies the probability that any
|
||||
// non-rare weapon will have a special ability.
|
||||
// This array (indexed by area - 1) specifies the probability that a non-rare weapon will have a special ability.
|
||||
// V2/V3: -> parray<uint8_t, 0x0A>
|
||||
/* 38 */ U32T<BE> special_percent_offset;
|
||||
|
||||
// This index probability table is indexed by [tool_class][area - 1]. The
|
||||
// tool class refers to an entry in ItemPMT, which links it to the actual
|
||||
// item code.
|
||||
// This index probability table is indexed by [tool_class][area - 1]. The tool class refers to an entry in
|
||||
// ItemPMT, which links it to the actual item code.
|
||||
// V2/V3: -> parray<parray<U16T, 0x0A>, 0x1C>
|
||||
/* 3C */ U32T<BE> tool_class_prob_table_offset;
|
||||
|
||||
// This index probability table determines how likely each technique is to
|
||||
// appear. The table is indexed as [technique_num][area - 1].
|
||||
// This index probability table determines how likely each technique is to appear. The table is indexed as
|
||||
// [technique_num][area - 1].
|
||||
// V2/V3: -> parray<parray<uint8_t, 0x0A>, 0x13>
|
||||
/* 40 */ U32T<BE> technique_index_prob_table_offset;
|
||||
|
||||
// This table specifies the ranges for technique disk levels. The table is
|
||||
// indexed as [technique_num][area - 1]. If either min or max in the range
|
||||
// is 0xFF, or if max < min, technique disks are not dropped for that
|
||||
// technique and area pair.
|
||||
// This table specifies the ranges for technique disk levels. The table is indexed as [technique_num][area - 1].
|
||||
// If either min or max in the range is 0xFF, or if max < min, technique disks are not dropped for that technique
|
||||
// and area pair.
|
||||
// V2/V3: -> parray<parray<Range<uint8_t>, 0x0A>, 0x13>
|
||||
/* 44 */ U32T<BE> technique_level_ranges_offset;
|
||||
|
||||
// See comments on armor_shield_type_index_prob_table_offset for how this is used.
|
||||
/* 48 */ uint8_t armor_or_shield_type_bias;
|
||||
/* 49 */ parray<uint8_t, 3> unused1;
|
||||
|
||||
// These values specify the maximum number of stars any generated unit can
|
||||
// have in each area. The values here are not inclusive; that is, a value
|
||||
// of 7 means that only units with 1-6 stars can drop in that area. The
|
||||
// game uniformly chooses a random number of stars in the acceptable
|
||||
// range, then uniformly chooses a random unit with that many stars.
|
||||
// These values specify the maximum number of stars any generated unit can have in each area. The values here are
|
||||
// not inclusive; that is, a value of 7 means that only units with 1-6 stars can drop in that area. The game
|
||||
// uniformly chooses a random number of stars in the acceptable range, then uniformly chooses a random unit with
|
||||
// that many stars.
|
||||
// V2/V3: -> parray<uint8_t, 0x0A>
|
||||
/* 4C */ U32T<BE> unit_max_stars_offset;
|
||||
|
||||
// This index probability table determines which type of items drop from
|
||||
// boxes. The table is indexed as [item_class][area - 1], with item_class
|
||||
// as the result value (that is, in the example below, the game looks at a
|
||||
// single column and sums the values going down, then the chosen item
|
||||
// class is one of the row indexes based on the weight values in the
|
||||
// column.) The resulting item_class value has the same meaning as in
|
||||
// enemy_item_classes above.
|
||||
// This index probability table determines which type of items drop from boxes. The table is indexed as
|
||||
// [item_class][area - 1], with item_class as the result value (that is, in the example below, the game looks at
|
||||
// a single column and sums the values going down, then the chosen item class is one of the row indexes based on
|
||||
// the weight values in the column.) The resulting value has the same meaning as in enemy_item_classes above.
|
||||
// For example, this array might look like the following:
|
||||
// [07 07 08 08 06 07 08 09 09 0A] // Chances per area of a weapon drop
|
||||
// [02 02 02 02 03 02 02 02 03 03] // Chances per area of an armor drop
|
||||
@@ -299,8 +276,8 @@ public:
|
||||
explicit JSONCommonItemSet(const phosg::JSON& json);
|
||||
};
|
||||
|
||||
// Note: There are clearly better ways of doing this, but this implementation
|
||||
// closely follows what the original code in the client does.
|
||||
// Note: There are clearly better ways of doing this, but this implementation closely follows what the original code in
|
||||
// the client does.
|
||||
template <typename ItemT, size_t MaxCount>
|
||||
struct ProbabilityTable {
|
||||
ItemT items[MaxCount];
|
||||
@@ -368,11 +345,9 @@ protected:
|
||||
RELFileSet(std::shared_ptr<const std::string> data);
|
||||
|
||||
template <typename T>
|
||||
std::pair<const T*, size_t> get_table(
|
||||
const TableSpec& spec, size_t index) const {
|
||||
std::pair<const T*, size_t> get_table(const TableSpec& spec, size_t index) const {
|
||||
const T* entries = &r.pget<T>(
|
||||
spec.offset + index * spec.entries_per_table * sizeof(T),
|
||||
spec.entries_per_table * sizeof(T));
|
||||
spec.offset + index * spec.entries_per_table * sizeof(T), spec.entries_per_table * sizeof(T));
|
||||
return std::make_pair(entries, spec.entries_per_table);
|
||||
}
|
||||
};
|
||||
@@ -485,17 +460,14 @@ private:
|
||||
} __packed_ws__(LuckTableEntry, 2);
|
||||
|
||||
struct Offsets {
|
||||
// Each section ID's favored weapon class has different probabilities than
|
||||
// those used for all other weapons. The tables are labeled with (D) for the
|
||||
// default values and (F) for the favored-class values.
|
||||
// Each section ID's favored weapon class has different probabilities than those used for all other weapons. The
|
||||
// tables are labeled with (D) for the default values and (F) for the favored-class values.
|
||||
|
||||
// Note that the favored bonuses for Redria are all zero; these values are
|
||||
// unused because Redria does not have a favored weapon type. Curiously,
|
||||
// Yellowboze also does not have a favored weapon type, but the values for
|
||||
// Note that the favored bonuses for Redria are all zero; these values are unused because Redria does not have a
|
||||
// favored weapon type. Curiously, Yellowboze also does not have a favored weapon type, but the values for
|
||||
// Yellowboze are not all zero.
|
||||
|
||||
// This table specifies how likely a special is to be upgraded or
|
||||
// downgraded by one level.
|
||||
// This table specifies how likely a special is to be upgraded or downgraded by one level.
|
||||
// In PSO V3, the special upgrade table is:
|
||||
// Viridia => (D) +1=10%, 0=60%, -1=30%
|
||||
// Viridia => (F) +1=25%, 0=50%, -1=25%
|
||||
@@ -519,9 +491,8 @@ private:
|
||||
// Whitill => (F) +1=25%, 0=50%, -1=25%
|
||||
be_uint32_t special_upgrade_prob_table_offset; // [{c, o -> (DeltaProbabilityEntry)[10][c]})
|
||||
|
||||
// This table specifies how likely a weapon's grind is to be upgraded or
|
||||
// downgraded, and by how much. The final grind value is clamped to the
|
||||
// range between 0 and the weapon's maximum grind from ItemPMT, inclusive.
|
||||
// This table specifies how likely a weapon's grind is to be upgraded or downgraded, and by how much. The final
|
||||
// grind value is clamped to the range between 0 and the weapon's maximum grind from ItemPMT, inclusive.
|
||||
// In PSO V3, the grind delta table is:
|
||||
// Viridia => (D) +3=3%, +2=7%, +1=13%, 0=60%, -1=10%, -2=7%, -3=0%
|
||||
// Viridia => (F) +3=5%, +2=13%, +1=25%, 0=50%, -1=7%, -2=0%, -3=0%
|
||||
@@ -545,9 +516,8 @@ private:
|
||||
// Whitill => (F) +3=5%, +2=13%, +1=25%, 0=50%, -1=7%, -2=0%, -3=0%
|
||||
be_uint32_t grind_delta_prob_table_offset; // [{c, o -> (DeltaProbabilityEntry)[10][c]})
|
||||
|
||||
// This table specifies how likely a weapon's bonuses are to be upgraded
|
||||
// or downgraded, and by how much. The final bonuses are capped above at
|
||||
// 100, but there is no lower limit (so negative results are possible).
|
||||
// This table specifies how likely a weapon's bonuses are to be upgraded or downgraded, and by how much. The final
|
||||
// bonuses are capped above at 100, but there is no lower limit (so negative results are possible).
|
||||
// In PSO V3, the bonus delta table is:
|
||||
// Viridia => (D) +10=5%, +5=15%, 0=60%, -5=15%, -10=5%
|
||||
// Viridia => (F) +10=8%, +5=20%, 0=60%, -5=10%, -10=2%
|
||||
@@ -571,11 +541,10 @@ private:
|
||||
// Whitill => (F) +10=8%, +5=20%, 0=60%, -5=10%, -10=2%
|
||||
be_uint32_t bonus_delta_prob_table_offset; // [{c, o -> (DeltaProbabilityEntry)[10][c]})
|
||||
|
||||
// There is a secondary computation done during weapon adjustment that
|
||||
// appears to determine how "good" the resulting weapon is compared to its
|
||||
// original state. If the result of this computation is positive, the game
|
||||
// plays a jingle when the tekker result is accepted. These tables describe
|
||||
// how much each delta affects this value, which we call luck.
|
||||
// There is a secondary computation done during weapon adjustment that appears to determine how "good" the
|
||||
// resulting weapon is compared to its original state. If the result of this computation is positive, the game
|
||||
// plays a jingle when the tekker result is accepted. These tables describe how much each delta affects this value,
|
||||
// which we call luck.
|
||||
|
||||
// In PSO V3, the special upgrade luck table is:
|
||||
// +1 => +20, 0 => 0, -1 => -20
|
||||
|
||||
+89
-141
@@ -63,14 +63,11 @@ struct WindowIndex {
|
||||
return match_iter - match_offset;
|
||||
};
|
||||
|
||||
// The data structure we want is a binary-searchable set of all strings
|
||||
// starting at all possible offsets within the sliding window, and we need
|
||||
// to be able to search lexicographically but insert and delete by offset.
|
||||
// A std::map<std::string, size_t> would accomplish this, but would be
|
||||
// horrendously inefficient: we'd have to copy strings far too much. We can
|
||||
// solve this by instead storing the offset of each string as keys in a set
|
||||
// and using a custom comparator to treat them as references to binary
|
||||
// strings within the data.
|
||||
// The data structure we want is a binary-searchable set of all strings starting at all possible offsets within the
|
||||
// sliding window, and we need to be able to search lexicographically but insert and delete by offset. A
|
||||
// std::map<std::string, size_t> would accomplish this, but would be horrendously inefficient: we'd have to copy
|
||||
// strings far too much. We can solve this by instead storing the offset of each string as keys in a set and using a
|
||||
// custom comparator to treat them as references to binary strings within the data.
|
||||
bool set_comparator(size_t a, size_t b) const {
|
||||
size_t max_length = min<size_t>(MaxMatchLength, this->size - max<size_t>(a, b));
|
||||
size_t end_a = a + max_length;
|
||||
@@ -87,11 +84,9 @@ struct WindowIndex {
|
||||
};
|
||||
|
||||
pair<size_t, size_t> get_best_match() const {
|
||||
// Find the best match from the index. It's unlikely that we'll get an
|
||||
// exact match, so check the entry before the upper_bound result too.
|
||||
// Note: We use upper_bound rather than lower_bound because in PRS, a
|
||||
// backreference can be encoded with fewer bits if it's close to the
|
||||
// decompression offset, and this makes us pick the latest match by
|
||||
// Find the best match from the index. It's unlikely that we'll get an exact match, so check the entry before the
|
||||
// upper_bound result too. Note: We use upper_bound rather than lower_bound because in PRS, a backreference can be
|
||||
// encoded with fewer bits if it's close to the decompression offset, and this makes us pick the latest match by
|
||||
// default.
|
||||
size_t match_offset = 0;
|
||||
size_t match_size = 0;
|
||||
@@ -123,9 +118,7 @@ struct LZSSInterleavedWriter {
|
||||
uint8_t next_control_bit;
|
||||
uint8_t buf[0x19];
|
||||
|
||||
LZSSInterleavedWriter()
|
||||
: buf_offset(1),
|
||||
next_control_bit(1) {
|
||||
LZSSInterleavedWriter() : buf_offset(1), next_control_bit(1) {
|
||||
this->buf[0] = 0;
|
||||
}
|
||||
|
||||
@@ -166,9 +159,7 @@ struct LZSSInterleavedWriter {
|
||||
|
||||
class ControlStreamReader {
|
||||
public:
|
||||
ControlStreamReader(phosg::StringReader& r)
|
||||
: r(r),
|
||||
bits(0x0000) {}
|
||||
ControlStreamReader(phosg::StringReader& r) : r(r), bits(0x0000) {}
|
||||
|
||||
bool read() {
|
||||
if (!(this->bits & 0x0100)) {
|
||||
@@ -285,8 +276,7 @@ string prs_compress_optimal(const void* in_data_v, size_t in_size, ProgressCallb
|
||||
long_window_thread.join();
|
||||
extended_window_thread.join();
|
||||
|
||||
// For each node, populate the literal value, and the best ways to get to the
|
||||
// following nodes
|
||||
// For each node, populate the literal value, and the best ways to get to the following nodes
|
||||
for (size_t z = 0; z < in_size; z++) {
|
||||
if ((z & 0xFFF) == 0 && progress_fn) {
|
||||
progress_fn(CompressPhase::CONSTRUCT_PATHS, z, in_size, 0);
|
||||
@@ -441,9 +431,8 @@ string prs_compress_optimal(const string& data, ProgressCallback progress_fn) {
|
||||
string prs_compress_pessimal(const void* vdata, size_t size) {
|
||||
const uint8_t* in_data = reinterpret_cast<const uint8_t*>(vdata);
|
||||
|
||||
// The worst possible encoding we can do is a literal byte when no byte with
|
||||
// the same value is within the window, or an extended copy if there is a byte
|
||||
// with the same value in the window.
|
||||
// The worst possible encoding we can do is a literal byte when no byte with the same value is within the window, or
|
||||
// an extended copy if there is a byte with the same value in the window.
|
||||
WindowIndex<0x1FFF, 1> window(in_data, size);
|
||||
LZSSInterleavedWriter w;
|
||||
for (size_t z = 0; z < size; z++) {
|
||||
@@ -539,9 +528,8 @@ void PRSCompressor::advance() {
|
||||
match_size++;
|
||||
}
|
||||
|
||||
// If there are multiple matches of the longest length, use the latest one,
|
||||
// since it's more likely that it can be expressed as a short copy instead
|
||||
// of a long copy.
|
||||
// If there are multiple matches of the longest length, use the latest one, since it's more likely that it can be
|
||||
// expressed as a short copy instead of a long copy.
|
||||
if (match_size >= (best_match_size + best_match_literals)) {
|
||||
best_match_offset = match_offset;
|
||||
best_match_size = match_size;
|
||||
@@ -558,15 +546,13 @@ void PRSCompressor::advance() {
|
||||
this->advance_literal();
|
||||
}
|
||||
|
||||
// If there is a suitable match, write a backreference; otherwise, write a
|
||||
// literal. The backreference should be encoded:
|
||||
// If there is a match, write a backreference; otherwise, write a literal. The backreference should be encoded:
|
||||
// - As a short copy if offset in [-0x100, -1] and size in [2, 5]
|
||||
// - As a long copy if offset in [-0x1FFF, -1] and size in [3, 9]
|
||||
// - As an extended copy if offset in [-0x1FFF, -1] and size in [10, 0x100]
|
||||
// Technically an extended copy can be used for sizes 1-9 as well, but if
|
||||
// size is 1 or 2, writing literals is better (since it uses fewer data
|
||||
// bytes and control bits), and a long copy can cover sizes 3-9 (and also
|
||||
// uses fewer data bytes and control bits).
|
||||
// Technically an extended copy can be used for sizes 1-9 as well, but if size is 1 or 2, writing literals is better
|
||||
// (since it uses fewer data bytes and control bits), and a long copy can cover sizes 3-9 (and also uses fewer data
|
||||
// bytes and control bits).
|
||||
ssize_t backreference_offset = best_match_offset - this->reverse_log.end_offset();
|
||||
if (best_match_size < 2) {
|
||||
// The match is too small; a literal would use fewer bits
|
||||
@@ -576,8 +562,8 @@ void PRSCompressor::advance() {
|
||||
this->advance_short_copy(backreference_offset, best_match_size);
|
||||
|
||||
} else if (best_match_size < 3) {
|
||||
// We can't use a long copy for size 2, and it's not worth it to use an
|
||||
// extended copy for this either (as noted above), so write a literal
|
||||
// We can't use a long copy for size 2, and it's not worth it to use an extended copy for this either (as noted
|
||||
// above), so write a literal
|
||||
this->advance_literal();
|
||||
|
||||
} else if ((backreference_offset >= -0x1FFF) && (best_match_size <= 9)) {
|
||||
@@ -655,14 +641,12 @@ string& PRSCompressor::close() {
|
||||
|
||||
void PRSCompressor::write_control(bool z) {
|
||||
if (this->pending_control_bits & 0x0100) {
|
||||
this->output.pput_u8(
|
||||
this->control_byte_offset, this->pending_control_bits & 0xFF);
|
||||
this->output.pput_u8(this->control_byte_offset, this->pending_control_bits & 0xFF);
|
||||
this->control_byte_offset = this->output.size();
|
||||
this->output.put_u8(0);
|
||||
this->pending_control_bits = z ? 0x8080 : 0x8000;
|
||||
} else {
|
||||
this->pending_control_bits =
|
||||
(this->pending_control_bits >> 1) | (z ? 0x8080 : 0x8000);
|
||||
this->pending_control_bits = (this->pending_control_bits >> 1) | (z ? 0x8080 : 0x8000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,8 +655,7 @@ void PRSCompressor::flush_control() {
|
||||
while (!(this->pending_control_bits & 0x0100)) {
|
||||
this->pending_control_bits >>= 1;
|
||||
}
|
||||
this->output.pput_u8(
|
||||
this->control_byte_offset, this->pending_control_bits & 0xFF);
|
||||
this->output.pput_u8(this->control_byte_offset, this->pending_control_bits & 0xFF);
|
||||
} else {
|
||||
if (this->control_byte_offset != this->output.size() - 1) {
|
||||
throw logic_error("data written without control bits");
|
||||
@@ -681,25 +664,17 @@ void PRSCompressor::flush_control() {
|
||||
}
|
||||
}
|
||||
|
||||
string prs_compress(
|
||||
const void* vdata,
|
||||
size_t size,
|
||||
ssize_t compression_level,
|
||||
ProgressCallback progress_fn) {
|
||||
string prs_compress(const void* vdata, size_t size, ssize_t compression_level, ProgressCallback progress_fn) {
|
||||
PRSCompressor prs(compression_level, progress_fn);
|
||||
prs.add(vdata, size);
|
||||
return std::move(prs.close());
|
||||
}
|
||||
|
||||
string prs_compress(
|
||||
const string& data,
|
||||
ssize_t compression_level,
|
||||
ProgressCallback progress_fn) {
|
||||
string prs_compress(const string& data, ssize_t compression_level, ProgressCallback progress_fn) {
|
||||
return prs_compress(data.data(), data.size(), compression_level, progress_fn);
|
||||
}
|
||||
|
||||
string prs_compress_indexed(
|
||||
const void* in_data_v, size_t in_size, ProgressCallback progress_fn) {
|
||||
string prs_compress_indexed(const void* in_data_v, size_t in_size, ProgressCallback progress_fn) {
|
||||
const uint8_t* in_data = reinterpret_cast<const uint8_t*>(in_data_v);
|
||||
|
||||
LZSSInterleavedWriter w;
|
||||
@@ -718,14 +693,11 @@ string prs_compress_indexed(
|
||||
auto m_long = w_long.get_best_match();
|
||||
auto m_extended = w_extended.get_best_match();
|
||||
|
||||
// Write the match that achieves the best ratio of output bytes to
|
||||
// compressed bits used. To do this without floating-point math, we multiply
|
||||
// the output byte count for each type of command by 468 / (command_bits),
|
||||
// since 468 is the least common multiple of the number of bits for each
|
||||
// command type. The command type with the highest score is the one we'll
|
||||
// use, breaking ties by choosing the shorter command type. Note that the
|
||||
// size of any copy type can be zero if no match was found; if no matches
|
||||
// were found at all, then we can always write a literal.
|
||||
// Write the match that achieves the best ratio of output bytes to compressed bits used. To do this without
|
||||
// floating-point math, we multiply the output byte count for each type of command by 468 / (command_bits), since
|
||||
// 468 is the least common multiple of the number of bits for each command type. The command type with the highest
|
||||
// score is the one we'll use, breaking ties by choosing the shorter command type. Note that the size of any copy
|
||||
// type can be zero if no match was found; if no matches were found at all, then we can always write a literal.
|
||||
size_t score_literal = 52;
|
||||
size_t score_short = m_short.second * 39;
|
||||
size_t score_long = m_long.second * 26;
|
||||
@@ -838,41 +810,30 @@ string prs_compress_indexed(const string& data, ProgressCallback progress_fn) {
|
||||
|
||||
PRSDecompressResult prs_decompress_with_meta(
|
||||
const void* data, size_t size, size_t max_output_size, bool allow_unterminated) {
|
||||
// PRS is an LZ77-based compression algorithm. Compressed data is split into
|
||||
// two streams: a control stream and a data stream. The control stream is read
|
||||
// one bit at a time, and the data stream is read one byte at a time. The
|
||||
// streams are interleaved such that the decompressor never has to move
|
||||
// backward in the input stream - when the decompressor needs a control bit
|
||||
// and there are no unused bits from the previous byte of the control stream,
|
||||
// it reads a byte from the input and treats it as the next 8 control bits.
|
||||
// PRS is an LZ77-based compression algorithm. Compressed data is split into two streams: a control stream and a data
|
||||
// stream. The control stream is read one bit at a time, and the data stream is read one byte at a time. The streams
|
||||
// are interleaved such that the decompressor never has to move backward in the input stream - when the decompressor
|
||||
// needs a control bit and there are no unused bits from the previous byte of the control stream, it reads a byte
|
||||
// from the input and treats it as the next 8 control bits.
|
||||
|
||||
// There are 3 distinct commands in PRS, labeled here with their control bits:
|
||||
// 1 - Literal byte. The decompressor copies one byte from the input data
|
||||
// stream to the output.
|
||||
// 00 - Short backreference. The decompressor reads two control bits and adds
|
||||
// 2 to this value to determine the number of bytes to copy, then reads
|
||||
// one byte from the data stream to determine how far back in the output
|
||||
// to copy from. This byte is treated as an 8-bit negative number - so
|
||||
// 0xF7, for example, means to start copying data from 9 bytes before the
|
||||
// end of the output. The range must start before the end of the output,
|
||||
// but the end of the range may be beyond the end of the output. In this
|
||||
// case, the bytes between the beginning of the range and original end of
|
||||
// the output are simply repeated.
|
||||
// 01 - Long backreference. The decompressor reads two bytes from the data and
|
||||
// byteswaps the resulting 16-bit value (that is, the low byte is read
|
||||
// first). The start offset (again, as a negative number) is the top 13
|
||||
// bits of this value; the size is the low 3 bits of this value, plus 2.
|
||||
// If the size bits are all zero, an additional byte is read from the
|
||||
// data stream and 1 is added to it to determine the backreference size
|
||||
// (we call this an extended backreference). Therefore, the maximum
|
||||
// backreference size is 256 bytes.
|
||||
// Decompression ends when either there are no more input bytes to read, or
|
||||
// when a long backreference is read with all zeroes in its offset field. The
|
||||
// original implementation stops decompression successfully when any attempt
|
||||
// to read from the input encounters the end of the stream, but newserv's
|
||||
// implementation only allows this at the end of an opcode - if end-of-stream
|
||||
// is encountered partway through an opcode, we throw instead, because it's
|
||||
// likely the input has been truncated or is malformed in some way.
|
||||
// 1 - Literal byte. The decompressor copies one byte from the input data stream to the output.
|
||||
// 00 - Short backreference. The decompressor reads two control bits and adds 2 to this value to determine the number
|
||||
// of bytes to copy, then reads one byte from the data stream to determine how far back in the output to copy
|
||||
// from. This byte is treated as an 8-bit negative number - so 0xF7, for example, means to start copying data
|
||||
// from 9 bytes before the end of the output. The range must start before the end of the output, but the end of
|
||||
// the range may be beyond the end of the output. In this case, the bytes between the beginning of the range and
|
||||
// original end of the output are simply repeated.
|
||||
// 01 - Long backreference. The decompressor reads two bytes from the data and byteswaps the resulting 16-bit value
|
||||
// (that is, the low byte is read first). The start offset (again, as a negative number) is the top 13 bits of
|
||||
// this value; the size is the low 3 bits of this value, plus 2. If the size bits are all zero, an additional
|
||||
// byte is read from the data stream and 1 is added to it to determine the backreference size (we call this an
|
||||
// extended backreference). Therefore, the maximum backreference size is 256 bytes.
|
||||
// Decompression ends when either there are no more input bytes to read, or when a long backreference is read with
|
||||
// all zeroes in its offset field. The original implementation stops decompression successfully when any attempt to
|
||||
// read from the input encounters the end of the stream, but newserv's implementation only allows this at the end of
|
||||
// an opcode - if end-of-stream is encountered partway through an opcode, we throw instead, because it's likely the
|
||||
// input has been truncated or is malformed in some way.
|
||||
|
||||
phosg::StringWriter w;
|
||||
phosg::StringReader r(data, size);
|
||||
@@ -894,10 +855,9 @@ PRSDecompressResult prs_decompress_with_meta(
|
||||
ssize_t offset;
|
||||
size_t count;
|
||||
|
||||
// Control 01 = long backreference
|
||||
if (cr.read()) {
|
||||
// The bits stored in the data stream are AAAAABBBCCCCCCCC, which we
|
||||
// rearrange into offset = CCCCCCCCAAAAA and size = BBB.
|
||||
// Control 01 = long backreference
|
||||
// The bits from the data stream are AAAAABBBCCCCCCCC, which we rearrange as offset=CCCCCCCCAAAAA and size=BBB.
|
||||
uint16_t a = r.get_u8();
|
||||
a |= (r.get_u8() << 8);
|
||||
offset = (a >> 3) | (~0x1FFF);
|
||||
@@ -905,24 +865,21 @@ PRSDecompressResult prs_decompress_with_meta(
|
||||
if (offset == ~0x1FFF) {
|
||||
break;
|
||||
}
|
||||
// If the size field is zero, it's an extended backreference (size comes
|
||||
// from another byte in the data stream)
|
||||
// If the size field is zero, it's an extended backreference (size comes from another byte in the data stream)
|
||||
count = (a & 7) ? ((a & 7) + 2) : (r.get_u8() + 1);
|
||||
|
||||
// Control 00 = short backreference
|
||||
} else {
|
||||
// Count comes from 2 bits in the control stream instead of from the
|
||||
// data stream (and 2 is added). Importantly, the control stream bits
|
||||
// are read first - this may involve reading another control stream
|
||||
// byte, which happens before the offset is read from the data stream.
|
||||
// Control 00 = short backreference
|
||||
// Count comes from 2 bits in the control stream instead of from the data stream (and 2 is added). Importantly,
|
||||
// the control stream bits are read first - this may involve reading another control stream byte, which happens
|
||||
// before the offset is read from the data stream.
|
||||
count = cr.read() << 1;
|
||||
count = (count | cr.read()) + 2;
|
||||
offset = r.get_u8() | (~0xFF);
|
||||
}
|
||||
|
||||
// Copy bytes from the referenced location in the output. Importantly,
|
||||
// copy only one byte at a time, in order to support ranges that cover the
|
||||
// current end of the output.
|
||||
// Copy bytes from the referenced location in the output. Importantly, copy only one byte at a time, in order to
|
||||
// support ranges that cover the current end of the output.
|
||||
size_t read_offset = w.size() + offset;
|
||||
if (read_offset >= w.size()) {
|
||||
throw runtime_error("backreference offset beyond beginning of output");
|
||||
@@ -1069,11 +1026,10 @@ void prs_disassemble(FILE* stream, const std::string& data) {
|
||||
return prs_disassemble(stream, data.data(), data.size());
|
||||
}
|
||||
|
||||
// BC0 is a compression algorithm fairly similar to PRS, but with a simpler set
|
||||
// of commands. Like PRS, there is a control stream, indicating when to copy a
|
||||
// literal byte from the input and when to copy from a backreference; unlike
|
||||
// PRS, there is only one type of backreference. Also, there is no stop opcode;
|
||||
// the decompressor simply stops when there are no more input bytes to read.
|
||||
// BC0 is a compression algorithm fairly similar to PRS, but with a simpler set of commands. Like PRS, there is a
|
||||
// control stream, indicating when to copy a literal byte from the input and when to copy from a backreference; unlike
|
||||
// PRS, there is only one type of backreference. Also, there is no stop opcode; the decompressor simply stops when
|
||||
// there are no more input bytes to read.
|
||||
|
||||
struct BC0PathNode {
|
||||
uint16_t memo_offset = 0;
|
||||
@@ -1112,8 +1068,7 @@ string bc0_compress_optimal(
|
||||
}
|
||||
}
|
||||
|
||||
// For each node, populate the literal value, and the best ways to get to the
|
||||
// following nodes
|
||||
// For each node, populate the literal value, and the best ways to get to the following nodes
|
||||
for (size_t z = 0; z < in_size; z++) {
|
||||
if ((z & 0xFFF) == 0 && progress_fn) {
|
||||
progress_fn(CompressPhase::CONSTRUCT_PATHS, z, in_size, 0);
|
||||
@@ -1238,11 +1193,9 @@ string bc0_encode(const void* in_data_v, size_t in_size) {
|
||||
return std::move(w.close());
|
||||
}
|
||||
|
||||
// The BC0 decompression implementation in PSO GC is vulnerable to overflow
|
||||
// attacks - there is no bounds checking on the output buffer. It is unlikely
|
||||
// that this can be usefully exploited (e.g. for RCE) because the output pointer
|
||||
// is loaded from memory before every byte is written, so we cannot change the
|
||||
// output pointer to any arbitrary address.
|
||||
// The BC0 decompression implementation in PSO GC is vulnerable to overflow attacks - there is no bounds checking on
|
||||
// the output buffer. It is unlikely that this can be usefully exploited (e.g. for RCE) because the output pointer is
|
||||
// loaded from memory before every byte is written, so we cannot change the output pointer to any arbitrary address.
|
||||
|
||||
string bc0_decompress(const string& data) {
|
||||
return bc0_decompress(data.data(), data.size());
|
||||
@@ -1252,22 +1205,18 @@ string bc0_decompress(const void* data, size_t size) {
|
||||
phosg::StringReader r(data, size);
|
||||
phosg::StringWriter w;
|
||||
|
||||
// Unlike PRS, BC0 uses a memo which "rolls over" every 0x1000 bytes. The
|
||||
// boundaries of these "memo pages" are offset by -0x12 bytes for some reason,
|
||||
// so the first output byte corresponds to position 0xFEE on the first memo
|
||||
// page. Backreferences refer to offsets based on the start of memo pages; for
|
||||
// example, if the current output offset is 0x1234, a backreference with
|
||||
// offset 0x123 refers to the byte that was written at offset 0x1111 (because
|
||||
// that byte is at offset 0x111 in the memo, because the memo rolls over every
|
||||
// 0x1000 bytes and the first memo byte was 0x12 bytes before the beginning of
|
||||
// the next page). The memo is initially zeroed from 0 to 0xFEE; it seems PSO
|
||||
// GC doesn't initialize the last 0x12 bytes of the first memo page.
|
||||
// Unlike PRS, BC0 uses a memo which "rolls over" every 0x1000 bytes. The boundaries of these "memo pages" are offset
|
||||
// by -0x12 bytes for some reason, so the first output byte corresponds to position 0xFEE on the first memo page.
|
||||
// Backreferences refer to offsets based on the start of memo pages; for example, if the current output offset is
|
||||
// 0x1234, a backreference with offset 0x123 refers to the byte that was written at offset 0x1111 (because that byte
|
||||
// is at offset 0x111 in the memo, because the memo rolls over every 0x1000 bytes and the first memo byte was 0x12
|
||||
// bytes before the beginning of the next page). The memo is initially zeroed from 0 to 0xFEE; it seems PSO GC
|
||||
// doesn't initialize the last 0x12 bytes of the first memo page.
|
||||
parray<uint8_t, 0x1000> memo;
|
||||
uint16_t memo_offset = 0x0FEE;
|
||||
|
||||
// The low byte of this value contains the control stream data; the high bits
|
||||
// specify which low bits are valid. When the last 1 is shifted out of the
|
||||
// high byte, we need to read a new control stream byte to get the next set of
|
||||
// The low byte of this value contains the control stream data; the high bits specify which low bits are valid. When
|
||||
// the last 1 is shifted out of the high byte, we need to read a new control stream byte to get the next set of
|
||||
// control bits.
|
||||
uint16_t control_stream_bits = 0x0000;
|
||||
|
||||
@@ -1282,14 +1231,13 @@ string bc0_decompress(const void* data, size_t size) {
|
||||
}
|
||||
|
||||
if ((control_stream_bits & 1) == 0) {
|
||||
// Control bit 0 means to perform a backreference copy. The offset and
|
||||
// size are stored in two bytes in the input stream, laid out as follows:
|
||||
// a1 = 0bBBBBBBBB
|
||||
// a2 = 0bAAAACCCC
|
||||
// The offset is the concatenation of bits AAAABBBBBBBB, which refers to
|
||||
// a position in the memo; the number of bytes to copy is (CCCC + 3). The
|
||||
// decompressor copies that many bytes from that offset in the memo, and
|
||||
// writes them to the output and to the current position in the memo.
|
||||
// Control bit 0 means to perform a backreference copy. The offset and size are stored in two bytes in the input
|
||||
// stream, laid out as follows:
|
||||
// a1 = 0bBBBBBBBB
|
||||
// a2 = 0bAAAACCCC
|
||||
// The offset is the concatenation of bits AAAABBBBBBBB, which refers to a position in the memo; the number of
|
||||
// bytes to copy is (CCCC + 3). The decompressor copies that many bytes from that offset in the memo, and writes
|
||||
// them to the output and to the current position in the memo.
|
||||
uint8_t a1 = r.get_u8();
|
||||
if (r.eof()) {
|
||||
break;
|
||||
@@ -1305,8 +1253,8 @@ string bc0_decompress(const void* data, size_t size) {
|
||||
}
|
||||
|
||||
} else {
|
||||
// Control bit 1 means to write a byte directly from the input to the
|
||||
// output. As above, the byte is also written to the memo.
|
||||
// Control bit 1 means to write a byte directly from the input to the output. As above, the byte is also written
|
||||
// to the memo.
|
||||
uint8_t v = r.get_u8();
|
||||
w.put_u8(v);
|
||||
memo[memo_offset] = v;
|
||||
|
||||
+35
-58
@@ -22,39 +22,32 @@ const char* phosg::name_for_enum<CompressPhase>(CompressPhase v);
|
||||
|
||||
typedef std::function<void(CompressPhase phase, size_t input_progress, size_t input_size, size_t output_size)> ProgressCallback;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PRS compression
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Use this class if you need to compress from multiple input buffers, or need
|
||||
// to compress multiple chunks and don't want to copy their contents
|
||||
// unnecessarily. (For most common use cases, use prs_compress, below, instead.)
|
||||
// To use this class, instantiate it, then call .add() one or more times, then
|
||||
// call .close() and use the returned string as the compressed result.
|
||||
// Use this class if you need to compress from multiple input buffers, or need to compress multiple chunks and don't
|
||||
// want to copy their contents unnecessarily. (For most common use cases, use prs_compress, below, instead.) To use
|
||||
// this class, instantiate it, then call .add() one or more times, then call .close() and use the returned string as
|
||||
// the compressed result.
|
||||
class PRSCompressor {
|
||||
public:
|
||||
// compression_level specifies how aggressively to search for alternate paths:
|
||||
// -1: Don't perform any compression at all, but produce output that can be
|
||||
// understood by prs_decompress. The output will be about 9/8 the size
|
||||
// of the input.
|
||||
// 0: Greedily search for the longest backreference at every point. Don't
|
||||
// consider any alternate paths. Generally offers a good balance between
|
||||
// speed and output size.
|
||||
// 1: Consider two paths at each point when a backreference is found: using
|
||||
// the backreference or ignoring it.
|
||||
// 2+: Consider further chains of paths at each point. Using values 2 or
|
||||
// greater for compression_level generally yields diminishing returns.
|
||||
// -1: Don't perform any compression at all, but produce output that can be understood by prs_decompress. The
|
||||
// output will be about 9/8 the size of the input.
|
||||
// 0: Greedily search for the longest backreference at every point. Don't consider any alternate paths. Generally
|
||||
// offers a good balance between speed and output size.
|
||||
// 1: Consider two paths at each point when a backreference is found: using the backreference or ignoring it.
|
||||
// 2+: Consider further chains of paths at each point. Using values 2 or greater for compression_level generally
|
||||
// yields diminishing returns.
|
||||
explicit PRSCompressor(ssize_t compression_level = 0, ProgressCallback progress_fn = nullptr);
|
||||
~PRSCompressor() = default;
|
||||
|
||||
// Adds more input data to be compressed, which logically comes after all
|
||||
// previous data provided via add() calls. Cannot be called after close() is
|
||||
// called.
|
||||
// Adds more input data to be compressed, which logically comes after all previous data provided via add() calls.
|
||||
// Cannot be called after close() is called.
|
||||
void add(const void* data, size_t size);
|
||||
void add(const std::string& data);
|
||||
|
||||
// Ends compression and returns the complete compressed result. It's OK to
|
||||
// std::move() from the returned string reference.
|
||||
// Ends compression and returns the complete compressed result. It's OK to std::move() from the returned reference.
|
||||
std::string& close();
|
||||
|
||||
// Returns the total number of bytes passed to add() calls so far.
|
||||
@@ -149,36 +142,24 @@ private:
|
||||
phosg::StringWriter output;
|
||||
};
|
||||
|
||||
// These functions use PRSCompressor to compress a buffer of data. This is
|
||||
// essentially a shortcut for constructing a PRSCompressor, calling .add() on
|
||||
// it once, then calling .close().
|
||||
// These functions use PRSCompressor to compress a buffer of data. This is essentially a shortcut for constructing a
|
||||
// PRSCompressor, calling .add() on it once, then calling .close().
|
||||
std::string prs_compress(
|
||||
const void* vdata,
|
||||
size_t size,
|
||||
ssize_t compression_level = 0,
|
||||
ProgressCallback progress_fn = nullptr);
|
||||
const void* vdata, size_t size, ssize_t compression_level = 0, ProgressCallback progress_fn = nullptr);
|
||||
std::string prs_compress(
|
||||
const std::string& data,
|
||||
ssize_t compression_level = 0,
|
||||
ProgressCallback progress_fn = nullptr);
|
||||
const std::string& data, ssize_t compression_level = 0, ProgressCallback progress_fn = nullptr);
|
||||
|
||||
// A faster form of prs_compress that doesn't have a tunable compression level.
|
||||
std::string prs_compress_indexed(
|
||||
const void* vdata,
|
||||
size_t size,
|
||||
ProgressCallback progress_fn = nullptr);
|
||||
std::string prs_compress_indexed(
|
||||
const std::string& data,
|
||||
ProgressCallback progress_fn = nullptr);
|
||||
std::string prs_compress_indexed(const void* vdata, size_t size, ProgressCallback progress_fn = nullptr);
|
||||
std::string prs_compress_indexed(const std::string& data, ProgressCallback progress_fn = nullptr);
|
||||
|
||||
// Compresses data using PRS to the smallest possible output size. This function
|
||||
// is slow, but produces results significantly smaller than even Sega's original
|
||||
// compressor.
|
||||
// Compresses data using PRS to the smallest possible output size. This function is slow, but produces results
|
||||
// significantly smaller than even Sega's original compressor.
|
||||
std::string prs_compress_optimal(const void* vdata, size_t size, ProgressCallback progress_fn = nullptr);
|
||||
std::string prs_compress_optimal(const std::string& data, ProgressCallback progress_fn = nullptr);
|
||||
|
||||
// Compresses data using PRS to the LARGEST possible output size. There is no
|
||||
// practical use for this function except for amusement.
|
||||
// Compresses data using PRS to the LARGEST possible output size. There is no practical use for this function except
|
||||
// for amusement.
|
||||
std::string prs_compress_pessimal(const void* vdata, size_t size);
|
||||
|
||||
// Decompresses PRS-compressed data.
|
||||
@@ -186,13 +167,14 @@ struct PRSDecompressResult {
|
||||
std::string data;
|
||||
size_t input_bytes_used;
|
||||
};
|
||||
PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false);
|
||||
PRSDecompressResult prs_decompress_with_meta(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false);
|
||||
PRSDecompressResult prs_decompress_with_meta(
|
||||
const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false);
|
||||
PRSDecompressResult prs_decompress_with_meta(
|
||||
const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false);
|
||||
std::string prs_decompress(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false);
|
||||
std::string prs_decompress(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false);
|
||||
|
||||
// Returns the decompressed size of PRS-compressed data, without actually
|
||||
// decompressing it.
|
||||
// Returns the decompressed size of PRS-compressed data, without actually decompressing it.
|
||||
size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false);
|
||||
size_t prs_decompress_size(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false);
|
||||
|
||||
@@ -200,21 +182,16 @@ size_t prs_decompress_size(const std::string& data, size_t max_output_size = 0,
|
||||
void prs_disassemble(FILE* stream, const void* data, size_t size);
|
||||
void prs_disassemble(FILE* stream, const std::string& data);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// BC0 compression
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Compresses data using the BC0 algorithm. Like with PRS, the optimal variant
|
||||
// is slow, but produces the smallest possible output.
|
||||
std::string bc0_compress_optimal(
|
||||
const void* in_data_v,
|
||||
size_t in_size,
|
||||
ProgressCallback progress_fn = nullptr);
|
||||
// Compresses data using the BC0 algorithm. Like with PRS, the optimal variant is slow, but produces the smallest
|
||||
// possible output.
|
||||
std::string bc0_compress(const std::string& data, ProgressCallback progress_fn = nullptr);
|
||||
std::string bc0_compress(const void* in_data_v, size_t in_size, ProgressCallback progress_fn = nullptr);
|
||||
std::string bc0_compress_optimal(const void* in_data_v, size_t in_size, ProgressCallback progress_fn = nullptr);
|
||||
|
||||
// Encodes data in a BC0-compatible format without compression (similar to using
|
||||
// compression_level=-1 with prs_compress).
|
||||
// Encodes data in a BC0-compatible format without compression (similar to compression_level=-1 in prs_compress).
|
||||
std::string bc0_encode(const void* in_data_v, size_t in_size);
|
||||
|
||||
// Decompresses BC0-compressed data.
|
||||
|
||||
+1
-1
@@ -458,7 +458,7 @@ public:
|
||||
|
||||
// If the map file has no random sections, does nothing and returns a shared_ptr to this. If it has any random
|
||||
// sections, returns a new map with all non-random sections copied verbatim, and random sections replaced with
|
||||
// non-random sections according to the challenge mode generation algorithm.
|
||||
// non-random sections according to the challenge mode enemy generation algorithm.
|
||||
std::shared_ptr<MapFile> materialize_random_sections(uint32_t random_seed);
|
||||
std::shared_ptr<const MapFile> materialize_random_sections(uint32_t random_seed) const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user