make replays useful on BB
This commit is contained in:
@@ -21,6 +21,7 @@ Current known issues / missing features:
|
||||
- Find a way to silence audio in RunDOL.s. Some old DOLs don't reset audio systems at load time and it's annoying to hear the crash buzz when the GC hasn't actually crashed.
|
||||
- Implement private and overflow lobbies.
|
||||
- Enforce client-side size limits (e.g. for 60/62 commands) on the server side as well. (For 60/62 specifically, perhaps transform them to 6C/6D if needed.)
|
||||
- Encapsulate BB server-side random state and make replays deterministic.
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
+55
-24
@@ -8,36 +8,67 @@
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
FileContentsCache::FileContentsCache(uint64_t ttl_usecs) : ttl_usecs(ttl_usecs) { }
|
||||
|
||||
FileContentsCache::File::File(const string& name, shared_ptr<const string> contents,
|
||||
uint64_t load_time) : name(name), contents(contents), load_time(load_time) { }
|
||||
|
||||
shared_ptr<const string> FileContentsCache::get(const std::string& name) {
|
||||
return this->get(name, [name]() -> string { return load_file(name); });
|
||||
}
|
||||
|
||||
shared_ptr<const string> FileContentsCache::get(const char* name) {
|
||||
return this->get(string(name));
|
||||
}
|
||||
|
||||
shared_ptr<const string> FileContentsCache::get(const std::string& name,
|
||||
std::function<std::string()> generate) {
|
||||
uint64_t t = now();
|
||||
try {
|
||||
auto& entry = this->name_to_file.at(name);
|
||||
if (t - entry.load_time < 300000000) { // not 5 minutes old? return it
|
||||
return entry.contents;
|
||||
}
|
||||
} catch (const out_of_range& e) { }
|
||||
|
||||
shared_ptr<const string> contents(new string(generate()));
|
||||
this->name_to_file.erase(name);
|
||||
this->name_to_file.emplace(piecewise_construct, forward_as_tuple(name),
|
||||
shared_ptr<const string> FileContentsCache::replace(
|
||||
const string& name, string&& data, uint64_t t) {
|
||||
if (t == 0) {
|
||||
t = now();
|
||||
}
|
||||
shared_ptr<const string> contents(new string(move(data)));
|
||||
auto emplace_ret = this->name_to_file.emplace(
|
||||
piecewise_construct,
|
||||
forward_as_tuple(name),
|
||||
forward_as_tuple(name, contents, t));
|
||||
|
||||
if (!emplace_ret.second) {
|
||||
emplace_ret.first->second.contents = contents;
|
||||
emplace_ret.first->second.load_time = t;
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
|
||||
shared_ptr<const string> FileContentsCache::get(const char* name,
|
||||
std::function<std::string()> generate) {
|
||||
shared_ptr<const string> FileContentsCache::replace(
|
||||
const string& name, const void* data, size_t size, uint64_t t) {
|
||||
string s(reinterpret_cast<const char*>(data), size);
|
||||
return this->replace(name, move(s), t);
|
||||
}
|
||||
|
||||
FileContentsCache::GetResult FileContentsCache::get_or_load(const std::string& name) {
|
||||
return this->get(name, load_file);
|
||||
}
|
||||
|
||||
FileContentsCache::GetResult FileContentsCache::get_or_load(const char* name) {
|
||||
return this->get_or_load(string(name));
|
||||
}
|
||||
|
||||
shared_ptr<const string> FileContentsCache::get_or_throw(const std::string& name) {
|
||||
auto throw_fn = +[](const std::string&) -> string {
|
||||
throw out_of_range("file missing from cache");
|
||||
};
|
||||
return this->get(name, throw_fn).data;
|
||||
}
|
||||
|
||||
shared_ptr<const string> FileContentsCache::get_or_throw(const char* name) {
|
||||
return this->get_or_throw(string(name));
|
||||
}
|
||||
|
||||
FileContentsCache::GetResult FileContentsCache::get(const std::string& name,
|
||||
std::function<std::string(const std::string&)> generate) {
|
||||
uint64_t t = now();
|
||||
try {
|
||||
auto& entry = this->name_to_file.at(name);
|
||||
if (this->ttl_usecs && (t - entry.load_time < this->ttl_usecs)) {
|
||||
return {entry.contents, false};
|
||||
}
|
||||
} catch (const out_of_range& e) { }
|
||||
return {this->replace(name, generate(name)), true};
|
||||
}
|
||||
|
||||
FileContentsCache::GetResult FileContentsCache::get(const char* name,
|
||||
std::function<std::string(const std::string&)> generate) {
|
||||
return this->get(string(name), generate);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
@@ -26,21 +28,85 @@ private:
|
||||
};
|
||||
|
||||
public:
|
||||
FileContentsCache() = default;
|
||||
explicit FileContentsCache(uint64_t ttl_usecs);
|
||||
FileContentsCache(const FileContentsCache&) = delete;
|
||||
FileContentsCache(FileContentsCache&&) = delete;
|
||||
FileContentsCache& operator=(const FileContentsCache&) = delete;
|
||||
FileContentsCache& operator=(FileContentsCache&&) = delete;
|
||||
~FileContentsCache() = default;
|
||||
|
||||
std::shared_ptr<const std::string> get(const std::string& name);
|
||||
std::shared_ptr<const std::string> get(const char* name);
|
||||
template <typename NameT>
|
||||
bool delete_key(NameT key) {
|
||||
return this->name_to_file.erase(key);
|
||||
}
|
||||
|
||||
std::shared_ptr<const std::string> get(
|
||||
const std::string& name, std::function<std::string()> generate);
|
||||
std::shared_ptr<const std::string> get(
|
||||
const char* name, std::function<std::string()> generate);
|
||||
std::shared_ptr<const std::string> replace(
|
||||
const std::string& name, std::string&& data, uint64_t t = 0);
|
||||
std::shared_ptr<const std::string> replace(
|
||||
const std::string& name, const void* data, size_t size, uint64_t t = 0);
|
||||
|
||||
struct GetResult {
|
||||
std::shared_ptr<const std::string> data;
|
||||
bool generate_called;
|
||||
};
|
||||
|
||||
GetResult get_or_load(const std::string& name);
|
||||
GetResult get_or_load(const char* name);
|
||||
std::shared_ptr<const string> get_or_throw(const std::string& name);
|
||||
std::shared_ptr<const string> get_or_throw(const char* name);
|
||||
|
||||
GetResult get(
|
||||
const std::string& name, std::function<std::string(const std::string&)> generate);
|
||||
GetResult get(
|
||||
const char* name, std::function<std::string(const std::string&)> generate);
|
||||
|
||||
template <typename T>
|
||||
struct GetObjResult {
|
||||
const T& obj;
|
||||
std::shared_ptr<const std::string> data;
|
||||
bool generate_called;
|
||||
};
|
||||
|
||||
template <typename T, typename NameT>
|
||||
GetObjResult<T> get_obj_or_load(NameT name) {
|
||||
auto res = this->get_or_load(name);
|
||||
if (res.data->size() != sizeof(T)) {
|
||||
throw runtime_error("cached string size is incorrect");
|
||||
}
|
||||
return {*reinterpret_cast<const T*>(res.data->data()), res.data, res.generate_called};
|
||||
}
|
||||
template <typename T, typename NameT>
|
||||
GetObjResult<T> get_obj_or_throw(NameT name) {
|
||||
auto res = this->get_or_throw(name);
|
||||
if (res->size() != sizeof(T)) {
|
||||
throw runtime_error("cached string size is incorrect");
|
||||
}
|
||||
return {*reinterpret_cast<const T*>(res.data->data()), res.data, res.generate_called};
|
||||
}
|
||||
template <typename T, typename NameT>
|
||||
GetObjResult<T> get_obj(NameT name, std::function<T(const std::string&)> generate) {
|
||||
uint64_t t = now();
|
||||
try {
|
||||
auto& entry = this->name_to_file.at(name);
|
||||
if (entry.contents->size() != sizeof(T)) {
|
||||
throw runtime_error("cached string size is incorrect");
|
||||
}
|
||||
if (this->ttl_usecs && (t - entry.load_time < this->ttl_usecs)) {
|
||||
return {*reinterpret_cast<const T*>(entry.contents->data()), entry.contents, false};
|
||||
}
|
||||
} catch (const out_of_range& e) { }
|
||||
T value = generate(name);
|
||||
auto ret = this->replace_obj(name, value);
|
||||
ret.generate_called = true;
|
||||
return ret;
|
||||
}
|
||||
template <typename T, typename NameT>
|
||||
GetObjResult<T> replace_obj(NameT name, const T& value) {
|
||||
auto cached_value = this->replace(name, &value, sizeof(value));
|
||||
return {*reinterpret_cast<const T*>(cached_value->data()), cached_value, false};
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, File> name_to_file;
|
||||
uint64_t ttl_usecs;
|
||||
};
|
||||
|
||||
+15
-4
@@ -41,7 +41,8 @@ string License::str() const {
|
||||
|
||||
|
||||
|
||||
LicenseManager::LicenseManager(const string& filename) : filename(filename) {
|
||||
LicenseManager::LicenseManager(const string& filename)
|
||||
: filename(filename), autosave(true) {
|
||||
try {
|
||||
auto licenses = load_vector_file<License>(this->filename);
|
||||
for (const auto& read_license : licenses) {
|
||||
@@ -73,6 +74,10 @@ void LicenseManager::save() const {
|
||||
}
|
||||
}
|
||||
|
||||
void LicenseManager::set_autosave(bool autosave) {
|
||||
this->autosave = autosave;
|
||||
}
|
||||
|
||||
shared_ptr<const License> LicenseManager::verify_pc(uint32_t serial_number,
|
||||
const string& access_key) const {
|
||||
auto& license = this->serial_number_to_license.at(serial_number);
|
||||
@@ -132,7 +137,9 @@ size_t LicenseManager::count() const {
|
||||
|
||||
void LicenseManager::ban_until(uint32_t serial_number, uint64_t end_time) {
|
||||
this->serial_number_to_license.at(serial_number)->ban_end_time = end_time;
|
||||
this->save();
|
||||
if (this->autosave) {
|
||||
this->save();
|
||||
}
|
||||
}
|
||||
|
||||
void LicenseManager::add(shared_ptr<License> l) {
|
||||
@@ -141,7 +148,9 @@ void LicenseManager::add(shared_ptr<License> l) {
|
||||
if (!l->username.empty()) {
|
||||
this->bb_username_to_license.emplace(l->username, l);
|
||||
}
|
||||
this->save();
|
||||
if (this->autosave) {
|
||||
this->save();
|
||||
}
|
||||
}
|
||||
|
||||
void LicenseManager::remove(uint32_t serial_number) {
|
||||
@@ -150,7 +159,9 @@ void LicenseManager::remove(uint32_t serial_number) {
|
||||
if (!l->username.empty()) {
|
||||
this->bb_username_to_license.erase(l->username);
|
||||
}
|
||||
this->save();
|
||||
if (this->autosave) {
|
||||
this->save();
|
||||
}
|
||||
}
|
||||
|
||||
vector<License> LicenseManager::snapshot() const {
|
||||
|
||||
+5
-2
@@ -49,6 +49,9 @@ public:
|
||||
LicenseManager(const std::string& filename);
|
||||
~LicenseManager() = default;
|
||||
|
||||
void save() const;
|
||||
void set_autosave(bool autosave);
|
||||
|
||||
std::shared_ptr<const License> verify_pc(uint32_t serial_number,
|
||||
const std::string& access_key) const;
|
||||
std::shared_ptr<const License> verify_gc(uint32_t serial_number,
|
||||
@@ -75,9 +78,9 @@ public:
|
||||
const std::string& password, bool temporary);
|
||||
|
||||
protected:
|
||||
void save() const;
|
||||
|
||||
std::string filename;
|
||||
bool autosave;
|
||||
|
||||
std::unordered_map<std::string, std::shared_ptr<License>> bb_username_to_license;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<License>> serial_number_to_license;
|
||||
};
|
||||
|
||||
+7
-3
@@ -11,7 +11,6 @@
|
||||
#include <unordered_map>
|
||||
|
||||
#include "DNSServer.hh"
|
||||
#include "FileContentsCache.hh"
|
||||
#include "IPStackSimulator.hh"
|
||||
#include "Loggers.hh"
|
||||
#include "NetworkAddresses.hh"
|
||||
@@ -27,7 +26,6 @@ using namespace std;
|
||||
|
||||
|
||||
|
||||
FileContentsCache file_cache;
|
||||
bool use_terminal_colors = false;
|
||||
|
||||
|
||||
@@ -372,8 +370,14 @@ int main(int argc, char** argv) {
|
||||
config_log.info("Creating menus");
|
||||
state->create_menus(config_json);
|
||||
|
||||
if (replay_log_filename) {
|
||||
state->allow_saving = false;
|
||||
state->license_manager->set_autosave(false);
|
||||
config_log.info("Saving disabled because this is a replay session");
|
||||
}
|
||||
|
||||
shared_ptr<DNSServer> dns_server;
|
||||
if (state->dns_server_port) {
|
||||
if (state->dns_server_port && !replay_log_filename) {
|
||||
config_log.info("Starting DNS server");
|
||||
dns_server.reset(new DNSServer(base, state->local_address,
|
||||
state->external_address));
|
||||
|
||||
+2
-3
@@ -8,8 +8,6 @@
|
||||
|
||||
using namespace std;
|
||||
|
||||
extern FileContentsCache file_cache;
|
||||
|
||||
|
||||
|
||||
static void load_battle_param_file(const string& filename, BattleParams* entries) {
|
||||
@@ -437,7 +435,8 @@ static vector<PSOEnemy> parse_map(uint8_t episode, uint8_t difficulty,
|
||||
|
||||
vector<PSOEnemy> load_map(const std::string& filename, uint8_t episode,
|
||||
uint8_t difficulty, const BattleParams* battle_params, bool alt_enemies) {
|
||||
shared_ptr<const string> data = file_cache.get(filename);
|
||||
static FileContentsCache map_file_cache(300 * 1000 * 1000);
|
||||
shared_ptr<const string> data = map_file_cache.get_or_load(filename).data;
|
||||
const EnemyEntry* entries = reinterpret_cast<const EnemyEntry*>(data->data());
|
||||
size_t entry_count = data->size() / sizeof(EnemyEntry);
|
||||
return parse_map(episode, difficulty, battle_params, entries, entry_count,
|
||||
|
||||
+1
-1
@@ -186,7 +186,7 @@ std::string prepend_command_header(
|
||||
case GameVersion::BB: {
|
||||
PSOCommandHeaderBB header;
|
||||
if (encryption_enabled) {
|
||||
header.size = (sizeof(header) + data.size() + 7) & ~7;
|
||||
header.size = (sizeof(header) + data.size() + 3) & ~3;
|
||||
} else {
|
||||
header.size = (sizeof(header) + data.size());
|
||||
}
|
||||
|
||||
+32
-16
@@ -55,38 +55,54 @@ union PSOSubcommand {
|
||||
// This function is used in a lot of places to check received command sizes and
|
||||
// cast them to the appropriate type
|
||||
template <typename T>
|
||||
const T& check_size_t(
|
||||
const void* data,
|
||||
size_t size,
|
||||
size_t min_size = sizeof(T),
|
||||
size_t max_size = sizeof(T)) {
|
||||
if (size < min_size) {
|
||||
throw std::runtime_error(string_printf(
|
||||
"command too small (expected at least 0x%zX bytes, received 0x%zX bytes)",
|
||||
min_size, size));
|
||||
}
|
||||
if (size > max_size) {
|
||||
throw std::runtime_error(string_printf(
|
||||
"command too large (expected at most 0x%zX bytes, received 0x%zX bytes)",
|
||||
max_size, size));
|
||||
}
|
||||
return *reinterpret_cast<const T*>(data);
|
||||
}
|
||||
template <typename T>
|
||||
const T& check_size_t(
|
||||
const std::string& data,
|
||||
size_t min_size = sizeof(T),
|
||||
size_t max_size = sizeof(T)) {
|
||||
if (data.size() < min_size) {
|
||||
return check_size_t<T>(data.data(), data.size(), min_size, max_size);
|
||||
}
|
||||
template <typename T>
|
||||
T& check_size_t(
|
||||
void* data,
|
||||
size_t size,
|
||||
size_t min_size = sizeof(T),
|
||||
size_t max_size = sizeof(T)) {
|
||||
if (size < min_size) {
|
||||
throw std::runtime_error(string_printf(
|
||||
"command too small (expected at least 0x%zX bytes, received 0x%zX bytes)",
|
||||
min_size, data.size()));
|
||||
min_size, size));
|
||||
}
|
||||
if (data.size() > max_size) {
|
||||
if (size > max_size) {
|
||||
throw std::runtime_error(string_printf(
|
||||
"command too large (expected at most 0x%zX bytes, received 0x%zX bytes)",
|
||||
max_size, data.size()));
|
||||
max_size, size));
|
||||
}
|
||||
return *reinterpret_cast<const T*>(data.data());
|
||||
return *reinterpret_cast<T*>(data);
|
||||
}
|
||||
template <typename T>
|
||||
T& check_size_t(
|
||||
std::string& data,
|
||||
size_t min_size = sizeof(T),
|
||||
size_t max_size = sizeof(T)) {
|
||||
if (data.size() < min_size) {
|
||||
throw std::runtime_error(string_printf(
|
||||
"command too small (expected at least 0x%zX bytes, received 0x%zX bytes)",
|
||||
min_size, data.size()));
|
||||
}
|
||||
if (data.size() > max_size) {
|
||||
throw std::runtime_error(string_printf(
|
||||
"command too large (expected at most 0x%zX bytes, received 0x%zX bytes)",
|
||||
max_size, data.size()));
|
||||
}
|
||||
return *reinterpret_cast<T*>(data.data());
|
||||
return check_size_t<T>(data.data(), data.size(), min_size, max_size);
|
||||
}
|
||||
|
||||
void check_size_v(size_t size, size_t min_size, size_t max_size = 0);
|
||||
|
||||
+37
-14
@@ -7,10 +7,11 @@
|
||||
#include <stdexcept>
|
||||
#include <phosg/Filesystem.hh>
|
||||
|
||||
#include "FileContentsCache.hh"
|
||||
#include "Loggers.hh"
|
||||
#include "StaticGameData.hh"
|
||||
#include "Text.hh"
|
||||
#include "Version.hh"
|
||||
#include "StaticGameData.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -26,6 +27,10 @@ static const string ACCOUNT_FILE_SIGNATURE =
|
||||
|
||||
|
||||
|
||||
static FileContentsCache player_files_cache(300 * 1000 * 1000);
|
||||
|
||||
|
||||
|
||||
PlayerStats::PlayerStats() noexcept
|
||||
: atp(0), mst(0), evp(0), hp(0), dfp(0), ata(0), lck(0) { }
|
||||
|
||||
@@ -271,14 +276,17 @@ GuildCardBB::GuildCardBB() noexcept
|
||||
|
||||
|
||||
void PlayerBank::load(const string& filename) {
|
||||
*this = load_object_file<PlayerBank>(filename);
|
||||
*this = player_files_cache.get_obj_or_load<PlayerBank>(filename).obj;
|
||||
for (uint32_t x = 0; x < this->num_items; x++) {
|
||||
this->items[x].data.id = 0x0F010000 + x;
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerBank::save(const string& filename) const {
|
||||
save_file(filename, this, sizeof(*this));
|
||||
void PlayerBank::save(const string& filename, bool save_to_filesystem) const {
|
||||
player_files_cache.replace(filename, this, sizeof(*this));
|
||||
if (save_to_filesystem) {
|
||||
save_file(filename, this, sizeof(*this));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -381,35 +389,45 @@ void ClientGameData::load_account_data() {
|
||||
shared_ptr<SavedAccountDataBB> data;
|
||||
try {
|
||||
data.reset(new SavedAccountDataBB(
|
||||
load_object_file<SavedAccountDataBB>(filename)));
|
||||
player_files_cache.get_obj_or_load<SavedAccountDataBB>(filename).obj));
|
||||
if (data->signature != ACCOUNT_FILE_SIGNATURE) {
|
||||
throw runtime_error("account data header is incorrect");
|
||||
}
|
||||
player_data_log.info("Loaded account data file %s", filename.c_str());
|
||||
|
||||
} catch (const exception& e) {
|
||||
player_data_log.info("No account data for %s; using default",
|
||||
this->bb_username.c_str());
|
||||
player_data_log.info("Cannot load account data for %s (%s); using default",
|
||||
this->bb_username.c_str(), e.what());
|
||||
player_files_cache.delete_key(filename);
|
||||
data.reset(new SavedAccountDataBB(
|
||||
load_object_file<SavedAccountDataBB>("system/players/default.nsa")));
|
||||
player_files_cache.get_obj_or_load<SavedAccountDataBB>(
|
||||
"system/players/default.nsa").obj));
|
||||
if (data->signature != ACCOUNT_FILE_SIGNATURE) {
|
||||
throw runtime_error("default account data header is incorrect");
|
||||
}
|
||||
player_data_log.info("Loaded default account data file");
|
||||
}
|
||||
|
||||
this->account_data = data;
|
||||
player_data_log.info("Loaded account data file %s", filename.c_str());
|
||||
}
|
||||
|
||||
void ClientGameData::save_account_data() const {
|
||||
string filename = this->account_data_filename();
|
||||
save_file(filename, this->account_data.get(), sizeof(SavedAccountDataBB));
|
||||
player_data_log.info("Saved account data file %s", filename.c_str());
|
||||
player_files_cache.replace(filename, this->account_data.get(), sizeof(SavedAccountDataBB));
|
||||
if (this->should_save) {
|
||||
save_file(filename, this->account_data.get(), sizeof(SavedAccountDataBB));
|
||||
player_data_log.info("Saved account data file %s to filesystem", filename.c_str());
|
||||
} else {
|
||||
player_data_log.info("Saved account data file %s to cache only", filename.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void ClientGameData::load_player_data() {
|
||||
string filename = this->player_data_filename();
|
||||
shared_ptr<SavedPlayerDataBB> data(new SavedPlayerDataBB(
|
||||
load_object_file<SavedPlayerDataBB>(filename)));
|
||||
player_files_cache.get_obj_or_load<SavedPlayerDataBB>(filename).obj));
|
||||
if (data->signature != PLAYER_FILE_SIGNATURE) {
|
||||
player_files_cache.delete_key(filename);
|
||||
throw runtime_error("player data header is incorrect");
|
||||
}
|
||||
this->player_data = data;
|
||||
@@ -418,8 +436,13 @@ void ClientGameData::load_player_data() {
|
||||
|
||||
void ClientGameData::save_player_data() const {
|
||||
string filename = this->player_data_filename();
|
||||
save_file(filename, this->player_data.get(), sizeof(SavedPlayerDataBB));
|
||||
player_data_log.info("Saved player data file %s", filename.c_str());
|
||||
player_files_cache.replace(filename, this->player_data.get(), sizeof(SavedPlayerDataBB));
|
||||
if (this->should_save) {
|
||||
save_file(filename, this->player_data.get(), sizeof(SavedPlayerDataBB));
|
||||
player_data_log.info("Saved player data file %s to filesystem", filename.c_str());
|
||||
} else {
|
||||
player_data_log.info("Saved player data file %s to cache only", filename.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void ClientGameData::import_player(const PSOPlayerDataPC& pc) {
|
||||
|
||||
+3
-2
@@ -71,7 +71,7 @@ struct PlayerBank {
|
||||
PlayerBankItem items[200];
|
||||
|
||||
void load(const std::string& filename);
|
||||
void save(const std::string& filename) const;
|
||||
void save(const std::string& filename, bool save_to_filesystem) const;
|
||||
|
||||
bool switch_with_file(const std::string& save_filename,
|
||||
const std::string& load_filename);
|
||||
@@ -407,8 +407,9 @@ public:
|
||||
size_t bb_player_index;
|
||||
PlayerInventoryItem identify_result;
|
||||
std::vector<ItemData> shop_contents;
|
||||
bool should_save;
|
||||
|
||||
ClientGameData() : serial_number(0), bb_player_index(0) { }
|
||||
ClientGameData() : serial_number(0), bb_player_index(0), should_save(true) { }
|
||||
~ClientGameData();
|
||||
|
||||
std::shared_ptr<SavedAccountDataBB> account(bool should_load = true);
|
||||
|
||||
@@ -25,10 +25,6 @@ using namespace std;
|
||||
|
||||
|
||||
|
||||
extern FileContentsCache file_cache;
|
||||
|
||||
|
||||
|
||||
vector<MenuItem> quest_categories_menu({
|
||||
MenuItem(static_cast<uint32_t>(QuestCategory::RETRIEVAL), u"Retrieval", u"$E$C6Quests that involve\nretrieving an object", 0),
|
||||
MenuItem(static_cast<uint32_t>(QuestCategory::EXTERMINATION), u"Extermination", u"$E$C6Quests that involve\ndestroying all\nmonsters", 0),
|
||||
@@ -339,7 +335,7 @@ void process_login_bb(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
return;
|
||||
} else {
|
||||
shared_ptr<License> l = LicenseManager::create_license_bb(
|
||||
fnv1a32(cmd.username), cmd.username, cmd.password, true);
|
||||
fnv1a32(cmd.username) & 0x7FFFFFFF, cmd.username, cmd.password, true);
|
||||
s->license_manager->add(l);
|
||||
c->set_license(l);
|
||||
}
|
||||
@@ -1346,7 +1342,9 @@ void process_gba_file_request(shared_ptr<ServerState>, shared_ptr<Client> c,
|
||||
uint16_t, uint32_t, const string& data) { // D7
|
||||
string filename(data);
|
||||
strip_trailing_zeroes(filename);
|
||||
auto contents = file_cache.get("system/gba/" + filename);
|
||||
|
||||
static FileContentsCache gba_file_cache(300 * 1000 * 1000);
|
||||
auto contents = gba_file_cache.get_or_load("system/gba/" + filename).data;
|
||||
|
||||
send_quest_file(c, "", filename, *contents, QuestFileType::GBA_DEMO);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,117 @@ shared_ptr<ReplaySession::Event> ReplaySession::create_event(
|
||||
return event;
|
||||
}
|
||||
|
||||
void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
|
||||
auto version = this->clients.at(ev->client_id)->version;
|
||||
|
||||
void* cmd_data = ev->mask.data() + ((version == GameVersion::BB) ? 8 : 4);
|
||||
size_t cmd_size = ev->mask.size() - ((version == GameVersion::BB) ? 8 : 4);
|
||||
|
||||
switch (version) {
|
||||
case GameVersion::PATCH: {
|
||||
const auto& header = check_size_t<PSOCommandHeaderPC>(
|
||||
ev->data, sizeof(PSOCommandHeaderPC), 0xFFFF);
|
||||
if (header.command == 0x02) {
|
||||
auto& cmd_mask = check_size_t<S_ServerInit_Patch_02>(cmd_data, cmd_size);
|
||||
cmd_mask.server_key = 0;
|
||||
cmd_mask.client_key = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GameVersion::PC:
|
||||
case GameVersion::GC: {
|
||||
uint8_t command;
|
||||
if (version == GameVersion::PC) {
|
||||
command = check_size_t<PSOCommandHeaderPC>(
|
||||
ev->data, sizeof(PSOCommandHeaderPC), 0xFFFF).command;
|
||||
} else {
|
||||
command = check_size_t<PSOCommandHeaderDCGC>(
|
||||
ev->data, sizeof(PSOCommandHeaderDCGC), 0xFFFF).command;
|
||||
}
|
||||
switch (command) {
|
||||
case 0x02:
|
||||
case 0x17:
|
||||
case 0x91:
|
||||
case 0x9B: {
|
||||
auto& cmd_mask = check_size_t<S_ServerInit_DC_PC_GC_02_17_91_9B>(
|
||||
cmd_data, cmd_size, sizeof(S_ServerInit_DC_PC_GC_02_17_91_9B), 0xFFFF);
|
||||
cmd_mask.server_key = 0;
|
||||
cmd_mask.client_key = 0;
|
||||
break;
|
||||
}
|
||||
case 0x0019: {
|
||||
auto& cmd_mask = check_size_t<S_Reconnect_19>(cmd_data, cmd_size);
|
||||
cmd_mask.address = 0;
|
||||
break;
|
||||
}
|
||||
case 0x64: {
|
||||
if (version == GameVersion::PC) {
|
||||
auto& cmd_mask = check_size_t<S_JoinGame_PC_64>(cmd_data, cmd_size,
|
||||
offsetof(S_JoinGame_GC_64, players_ep3));
|
||||
cmd_mask.variations.clear(0);
|
||||
cmd_mask.rare_seed = 0;
|
||||
} else { // GC
|
||||
auto& cmd_mask = check_size_t<S_JoinGame_GC_64>(cmd_data, cmd_size,
|
||||
offsetof(S_JoinGame_GC_64, players_ep3));
|
||||
cmd_mask.variations.clear(0);
|
||||
cmd_mask.rare_seed = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0xB1: {
|
||||
for (size_t x = 8; x < ev->mask.size(); x++) {
|
||||
ev->mask[x] = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GameVersion::BB: {
|
||||
uint16_t command = check_size_t<PSOCommandHeaderBB>(
|
||||
ev->data, sizeof(PSOCommandHeaderBB), 0xFFFF).command;
|
||||
switch (command) {
|
||||
case 0x0003: {
|
||||
auto& cmd_mask = check_size_t<S_ServerInit_BB_03_9B>(
|
||||
cmd_data, cmd_size, sizeof(S_ServerInit_BB_03_9B), 0xFFFF);
|
||||
cmd_mask.server_key.clear(0);
|
||||
cmd_mask.client_key.clear(0);
|
||||
break;
|
||||
}
|
||||
case 0x0019: {
|
||||
auto& cmd_mask = check_size_t<S_Reconnect_19>(cmd_data, cmd_size);
|
||||
cmd_mask.address = 0;
|
||||
break;
|
||||
}
|
||||
case 0x0064: {
|
||||
auto& cmd_mask = check_size_t<S_JoinGame_BB_64>(cmd_data, cmd_size,
|
||||
offsetof(S_JoinGame_BB_64, players_ep3),
|
||||
offsetof(S_JoinGame_BB_64, players_ep3));
|
||||
cmd_mask.variations.clear(0);
|
||||
cmd_mask.rare_seed = 0;
|
||||
break;
|
||||
}
|
||||
case 0x00B1: {
|
||||
for (size_t x = 8; x < ev->mask.size(); x++) {
|
||||
ev->mask[x] = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x00E6: {
|
||||
auto& cmd_mask = check_size_t<S_ClientInit_BB_00E6>(cmd_data, cmd_size);
|
||||
cmd_mask.team_id = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GameVersion::DC:
|
||||
throw logic_error("DC auto-masking is not implemented");
|
||||
default:
|
||||
throw logic_error("invalid game version");
|
||||
}
|
||||
}
|
||||
|
||||
ReplaySession::ReplaySession(
|
||||
shared_ptr<struct event_base> base,
|
||||
FILE* input_log,
|
||||
@@ -79,6 +190,9 @@ ReplaySession::ReplaySession(
|
||||
parsing_command->mask += mask_bytes;
|
||||
continue;
|
||||
} else {
|
||||
if (parsing_command->type == Event::Type::RECEIVE) {
|
||||
this->apply_default_mask(parsing_command);
|
||||
}
|
||||
parsing_command = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -304,6 +418,13 @@ void ReplaySession::on_command_received(
|
||||
switch (c->version) {
|
||||
case GameVersion::DC:
|
||||
throw runtime_error("DC encryption is not supported during replays");
|
||||
case GameVersion::PATCH:
|
||||
if (command == 0x02) {
|
||||
auto& cmd = check_size_t<S_ServerInit_Patch_02>(data);
|
||||
c->channel.crypt_in.reset(new PSOPCEncryption(cmd.server_key));
|
||||
c->channel.crypt_out.reset(new PSOPCEncryption(cmd.client_key));
|
||||
}
|
||||
break;
|
||||
case GameVersion::PC:
|
||||
case GameVersion::GC:
|
||||
if (command == 0x02 || command == 0x17 || command == 0x91 || command == 0x9B) {
|
||||
|
||||
@@ -78,6 +78,8 @@ private:
|
||||
Event::Type type, std::shared_ptr<Client> c);
|
||||
void update_timeout_event();
|
||||
|
||||
void apply_default_mask(std::shared_ptr<Event> ev);
|
||||
|
||||
static void dispatch_on_timeout(evutil_socket_t fd, short events, void* ctx);
|
||||
static void dispatch_on_command_received(
|
||||
Channel& ch, uint16_t command, uint32_t flag, std::string& data);
|
||||
|
||||
+20
-7
@@ -344,6 +344,7 @@ static const vector<string> stream_file_entries = {
|
||||
"BattleParamEntry_ep4_on.dat",
|
||||
"PlyLevelTbl.prs",
|
||||
};
|
||||
static FileContentsCache bb_stream_files_cache(3600 * 1000 * 1000);
|
||||
|
||||
void send_stream_file_index_bb(shared_ptr<Client> c) {
|
||||
|
||||
@@ -357,11 +358,22 @@ void send_stream_file_index_bb(shared_ptr<Client> c) {
|
||||
vector<S_StreamFileIndexEntry_BB_01EB> entries;
|
||||
size_t offset = 0;
|
||||
for (const string& filename : stream_file_entries) {
|
||||
auto file_data = file_cache.get("system/blueburst/" + filename);
|
||||
string key = "system/blueburst/" + filename;
|
||||
auto cache_res = bb_stream_files_cache.get_or_load(key);
|
||||
auto& e = entries.emplace_back();
|
||||
e.size = file_data->size();
|
||||
// TODO: memoize the checksum somewhere; computing it can be slow
|
||||
e.checksum = crc32(file_data->data(), e.size);
|
||||
e.size = cache_res.data->size();
|
||||
// Computing the checksum can be slow, so we cache it along with the file
|
||||
// data. If the cache result was just populated, then it may be different,
|
||||
// so we always recompute the checksum in that case.
|
||||
if (cache_res.generate_called) {
|
||||
e.checksum = crc32(cache_res.data->data(), e.size);
|
||||
bb_stream_files_cache.replace_obj<uint32_t>(key + ".crc32", e.checksum);
|
||||
} else {
|
||||
auto compute_checksum = [&](const string&) -> uint32_t {
|
||||
return crc32(cache_res.data->data(), e.size);
|
||||
};
|
||||
e.checksum = bb_stream_files_cache.get_obj<uint32_t>(key + ".crc32", compute_checksum).obj;
|
||||
}
|
||||
e.offset = offset;
|
||||
e.filename = filename;
|
||||
offset += e.size;
|
||||
@@ -370,19 +382,20 @@ void send_stream_file_index_bb(shared_ptr<Client> c) {
|
||||
}
|
||||
|
||||
void send_stream_file_chunk_bb(shared_ptr<Client> c, uint32_t chunk_index) {
|
||||
auto contents = file_cache.get("<BB stream file>", +[]() -> string {
|
||||
auto cache_result = bb_stream_files_cache.get("<BB stream file>", +[](const string&) -> string {
|
||||
size_t bytes = 0;
|
||||
for (const auto& name : stream_file_entries) {
|
||||
bytes += file_cache.get("system/blueburst/" + name)->size();
|
||||
bytes += bb_stream_files_cache.get_or_load("system/blueburst/" + name).data->size();
|
||||
}
|
||||
|
||||
string ret;
|
||||
ret.reserve(bytes);
|
||||
for (const auto& name : stream_file_entries) {
|
||||
ret += *file_cache.get("system/blueburst/" + name);
|
||||
ret += *bb_stream_files_cache.get_or_load("system/blueburst/" + name).data;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
auto contents = cache_result.data;
|
||||
|
||||
S_StreamFileChunk_BB_02EB chunk_cmd;
|
||||
chunk_cmd.chunk_index = chunk_index;
|
||||
|
||||
@@ -80,6 +80,7 @@ void Server::on_listen_accept(struct evconnlistener* listener,
|
||||
BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
|
||||
shared_ptr<Client> c(new Client(
|
||||
bev, listening_socket->version, listening_socket->behavior));
|
||||
c->game_data.should_save = this->state->allow_saving;
|
||||
c->channel.on_command_received = Server::on_client_input;
|
||||
c->channel.on_error = Server::on_client_error;
|
||||
c->channel.context_obj = this;
|
||||
@@ -100,6 +101,7 @@ void Server::connect_client(
|
||||
struct bufferevent* bev, uint32_t address, uint16_t client_port,
|
||||
uint16_t server_port, GameVersion version, ServerBehavior initial_state) {
|
||||
shared_ptr<Client> c(new Client(bev, version, initial_state));
|
||||
c->game_data.should_save = this->state->allow_saving;
|
||||
c->channel.on_command_received = Server::on_client_input;
|
||||
c->channel.on_error = Server::on_client_error;
|
||||
c->channel.context_obj = this;
|
||||
|
||||
@@ -19,6 +19,7 @@ ServerState::ServerState()
|
||||
: dns_server_port(0),
|
||||
ip_stack_debug(false),
|
||||
allow_unregistered_users(false),
|
||||
allow_saving(true),
|
||||
item_tracking_enabled(true),
|
||||
run_shell_behavior(RunShellBehavior::DEFAULT), next_lobby_id(1),
|
||||
pre_lobby_event(0),
|
||||
|
||||
@@ -46,6 +46,7 @@ struct ServerState {
|
||||
std::vector<std::string> ip_stack_addresses;
|
||||
bool ip_stack_debug;
|
||||
bool allow_unregistered_users;
|
||||
bool allow_saving;
|
||||
bool item_tracking_enabled;
|
||||
RunShellBehavior run_shell_behavior;
|
||||
std::vector<std::shared_ptr<const PSOBBEncryption::KeyFile>> bb_private_keys;
|
||||
|
||||
Reference in New Issue
Block a user