Compare commits

..

6 Commits

Author SHA1 Message Date
Martin Michelsen 6f99b3b1c8 run patch server on main thread on windows 2024-03-25 22:28:15 -07:00
Martin Michelsen da9765f1aa fix cleanup in compression test 2024-03-25 22:28:02 -07:00
Martin Michelsen b7897cddf2 show uncaught exception messages on windows 2024-03-24 22:00:22 -07:00
Martin Michelsen ce2300b116 add pessimal compression 2024-03-24 21:59:28 -07:00
Martin Michelsen cb05dce764 handle quest loading client bug 2024-03-24 15:43:35 -07:00
Martin Michelsen a762c0f8f8 make prev battle record const 2024-03-24 10:24:36 -07:00
10 changed files with 143 additions and 20 deletions
+1 -1
View File
@@ -217,7 +217,7 @@ public:
uint16_t card_battle_table_seat_number;
uint16_t card_battle_table_seat_state;
std::weak_ptr<Episode3::Tournament::Team> ep3_tournament_team;
std::shared_ptr<Episode3::BattleRecord> ep3_prev_battle_record;
std::shared_ptr<const Episode3::BattleRecord> ep3_prev_battle_record;
std::shared_ptr<const Menu> last_menu_sent;
uint32_t last_game_info_requested;
struct JoinCommand {
+8 -1
View File
@@ -709,7 +709,14 @@ struct C_MenuSelection_PC_BB_10_Flag03 : C_MenuSelection_10_Flag03<TextEncoding:
// memory card), use A7 instead.
// All chunks except the last must have 0x400 data bytes. When downloading an
// online quest, the .bin and .dat chunks may be interleaved (although newserv
// currently sends them sequentially).
// currently sends them sequentially). There is a client bug in BB (and
// probably all other versions) where if the quest file's size is a multiple
// of 0x400, the last chunk will have size 0x400, and the client will never
// consider the download complete since it only checks if the last chunk has
// size < 0x400; it does not check if all expected bytes have been received.
// To work around this, newserv appends an extra zero byte if the quest file's
// size is a multiple of 0x400; this byte will be ignored since the PRS
// decompression algorithm contains a stop command, so it will never read it.
// header.flag = file chunk index (start offset / 0x400)
struct S_WriteFile_13_A7 {
+39
View File
@@ -438,6 +438,45 @@ string prs_compress_optimal(const string& data, ProgressCallback progress_fn) {
return prs_compress_optimal(data.data(), data.size(), 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.
WindowIndex<0x1FFF, 1> window(in_data, size);
LZSSInterleavedWriter w;
for (size_t z = 0; z < size; z++) {
auto match = window.get_best_match();
if (match.second >= 1) {
// Write extended copy
int16_t offset = match.first - window.offset;
w.write_control(false);
w.flush_if_ready();
w.write_control(true);
uint16_t a = (offset << 3);
w.write_data(a & 0xFF);
w.write_data(a >> 8);
w.write_data(0);
} else {
// Write literal
w.write_control(true);
w.write_data(in_data[z]);
}
w.flush_if_ready();
window.advance();
}
// Write stop command
w.write_control(false);
w.flush_if_ready();
w.write_control(true);
w.write_data(0);
w.write_data(0);
return std::move(w.close());
}
PRSCompressor::PRSCompressor(
ssize_t compression_level, ProgressCallback progress_fn)
: compression_level(compression_level),
+4
View File
@@ -177,6 +177,10 @@ std::string prs_compress_indexed(
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.
std::string prs_compress_pessimal(const void* vdata, size_t size);
// Decompresses PRS-compressed data.
struct PRSDecompressResult {
std::string data;
+28 -1
View File
@@ -266,6 +266,7 @@ static void a_compress_decompress_fn(Arguments& args) {
bool is_decompress = starts_with(action, "decompress-");
bool is_big_endian = args.get<bool>("big-endian");
bool is_optimal = args.get<bool>("optimal");
bool is_pessimal = args.get<bool>("pessimal");
int8_t compression_level = args.get<int8_t>("compression-level", 0);
size_t bytes = args.get<size_t>("bytes", 0);
string seed = args.get<string>("seed");
@@ -298,6 +299,8 @@ static void a_compress_decompress_fn(Arguments& args) {
if (!is_decompress && (is_prs || is_pr2 || is_prc)) {
if (is_optimal) {
data = prs_compress_optimal(data.data(), data.size(), optimal_progress_fn);
} else if (is_pessimal) {
data = prs_compress_pessimal(data.data(), data.size());
} else {
data = prs_compress(data, compression_level, progress_fn);
}
@@ -2358,7 +2361,7 @@ Action a_run_server_replay_log(
}
if (evthread_use_pthreads()) {
throw runtime_error("failed to setup libevent threads");
throw runtime_error("failed to set up libevent threads");
}
if (!isdir("system/players")) {
@@ -2647,6 +2650,30 @@ int main(int argc, char** argv) {
log_error("Unknown or invalid action; try --help");
return 1;
}
#ifdef PHOSG_WINDOWS
// Cygwin just gives a stackdump when an exception falls out of main(), so
// unlike Linux and macOS, we have to manually catch exceptions here just to
// see what the exception message was.
try {
a->run(args);
} catch (const cannot_open_file& e) {
log_error("Top-level exception (cannot_open_file): %s", e.what());
throw;
} catch (const invalid_argument& e) {
log_error("Top-level exception (invalid_argument): %s", e.what());
throw;
} catch (const out_of_range& e) {
log_error("Top-level exception (out_of_range): %s", e.what());
throw;
} catch (const runtime_error& e) {
log_error("Top-level exception (runtime_error): %s", e.what());
throw;
} catch (const exception& e) {
log_error("Top-level exception: %s", e.what());
throw;
}
#else
a->run(args);
#endif
return 0;
}
+20 -6
View File
@@ -395,17 +395,31 @@ void PatchServer::on_client_error(Channel& ch, short events) {
}
PatchServer::PatchServer(shared_ptr<const Config> config)
: base(event_base_new(), event_base_free),
config(config),
destroy_clients_ev(event_new(this->base.get(), -1, EV_TIMEOUT, &PatchServer::dispatch_destroy_clients, this), event_free),
th(&PatchServer::thread_fn, this) {}
: config(config) {
if (config->shared_base) {
this->base = config->shared_base;
this->base_is_shared = true;
} else {
this->base.reset(event_base_new(), event_base_free);
this->base_is_shared = false;
}
this->destroy_clients_ev.reset(
event_new(this->base.get(), -1, EV_TIMEOUT, &PatchServer::dispatch_destroy_clients, this), event_free);
if (!this->base_is_shared) {
this->th = thread(&PatchServer::thread_fn, this);
}
}
void PatchServer::schedule_stop() {
event_base_loopexit(this->base.get(), nullptr);
if (!this->base_is_shared) {
event_base_loopexit(this->base.get(), nullptr);
}
}
void PatchServer::wait_for_stop() {
this->th.join();
if (!this->base_is_shared) {
this->th.join();
}
}
void PatchServer::listen(const std::string& addr_str, const string& socket_path, Version version) {
+2
View File
@@ -22,6 +22,7 @@ public:
std::string message;
std::shared_ptr<const LicenseIndex> license_index;
std::shared_ptr<const PatchFileIndex> patch_file_index;
std::shared_ptr<struct event_base> shared_base;
};
PatchServer() = delete;
@@ -86,6 +87,7 @@ private:
};
std::shared_ptr<struct event_base> base;
bool base_is_shared;
std::shared_ptr<const Config> config;
std::unordered_set<std::shared_ptr<Client>> clients_to_destroy;
+16 -10
View File
@@ -490,7 +490,7 @@ QuestIndex::QuestIndex(
continue;
}
auto add_file = [&](map<string, FileData>& files, const string& basename, const string& filename, string&& value) {
auto add_file = [&](map<string, FileData>& files, const string& basename, const string& filename, string&& value, bool check_chunk_size) {
if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) {
throw runtime_error("file " + basename + " exists in multiple categories");
}
@@ -498,6 +498,12 @@ QuestIndex::QuestIndex(
if (!files.emplace(basename, FileData{filename, data_ptr}).second) {
throw runtime_error("file " + basename + " already exists");
}
// There is a bug in the client that prevents quests from loading properly
// if any file's size is a multiple of 0x400. See the comments on the 13
// command in CommandFormats.hh for more details.
if (check_chunk_size && !(data_ptr->size() & 0x3FF)) {
data_ptr->push_back(0x00);
}
};
string cat_path = directory + "/" + cat->directory_name;
@@ -544,26 +550,26 @@ QuestIndex::QuestIndex(
}
if (extension == "json") {
add_file(json_files, file_basename, orig_filename, std::move(file_data));
add_file(json_files, file_basename, orig_filename, std::move(file_data), false);
} else if (extension == "bin" || extension == "mnm") {
add_file(bin_files, file_basename, orig_filename, std::move(file_data));
add_file(bin_files, file_basename, orig_filename, std::move(file_data), true);
} else if (extension == "bind" || extension == "mnmd") {
add_file(bin_files, file_basename, orig_filename, prs_compress_optimal(file_data));
add_file(bin_files, file_basename, orig_filename, prs_compress_optimal(file_data), true);
} else if (extension == "dat") {
add_file(dat_files, file_basename, orig_filename, std::move(file_data));
add_file(dat_files, file_basename, orig_filename, std::move(file_data), true);
} else if (extension == "datd") {
add_file(dat_files, file_basename, orig_filename, prs_compress_optimal(file_data));
add_file(dat_files, file_basename, orig_filename, prs_compress_optimal(file_data), true);
} else if (extension == "pvr") {
add_file(pvr_files, file_basename, orig_filename, std::move(file_data));
add_file(pvr_files, file_basename, orig_filename, std::move(file_data), true);
} else if (extension == "qst") {
auto files = decode_qst_data(file_data);
for (auto& it : files) {
if (ends_with(it.first, ".bin")) {
add_file(bin_files, file_basename, orig_filename, std::move(it.second));
add_file(bin_files, file_basename, orig_filename, std::move(it.second), true);
} else if (ends_with(it.first, ".dat")) {
add_file(dat_files, file_basename, orig_filename, std::move(it.second));
add_file(dat_files, file_basename, orig_filename, std::move(it.second), true);
} else if (ends_with(it.first, ".pvr")) {
add_file(pvr_files, file_basename, orig_filename, std::move(it.second));
add_file(pvr_files, file_basename, orig_filename, std::move(it.second), true);
} else {
throw runtime_error("qst file contains unsupported file type: " + it.first);
}
+8
View File
@@ -1787,6 +1787,14 @@ void ServerState::load_all() {
shared_ptr<PatchServer::Config> ServerState::generate_patch_server_config(bool is_bb) const {
auto ret = make_shared<PatchServer::Config>();
#ifdef PHOSG_WINDOWS
// libevent doesn't play nice with Cygwin, so we run the patch server on the
// main thread there. The problem seems to be that the locking structures are
// never set up, presumably since we call event_use_pthreads() since
// event_use_windows_threads() doesn't exist. (Does literally no one else use
// libevent with Cygwin??)
ret->shared_base = this->base;
#endif
ret->allow_unregistered_users = this->allow_unregistered_users;
ret->hide_data_from_logs = this->hide_download_commands;
ret->idle_timeout_usecs = this->patch_client_idle_timeout_usecs;
+17 -1
View File
@@ -20,6 +20,10 @@ echo "... compress with level=0"
$EXECUTABLE compress-$SCHEME --compression-level=0 $BASENAME.mnrd $BASENAME.mnrd.$SCHEME.l0
echo "... compress with level=1"
$EXECUTABLE compress-$SCHEME --compression-level=1 $BASENAME.mnrd $BASENAME.mnrd.$SCHEME.l1
echo "... compress optimally"
$EXECUTABLE compress-$SCHEME --optimal $BASENAME.mnrd $BASENAME.mnrd.$SCHEME.lo
echo "... compress pessimally"
$EXECUTABLE compress-$SCHEME --pessimal $BASENAME.mnrd $BASENAME.mnrd.$SCHEME.lp
echo "... decompress from level=-1 (no compression)"
$EXECUTABLE decompress-$SCHEME $BASENAME.mnrd.$SCHEME.lN $BASENAME.mnrd.$SCHEME.lN.dec
@@ -27,6 +31,10 @@ echo "... decompress from level=0"
$EXECUTABLE decompress-$SCHEME $BASENAME.mnrd.$SCHEME.l0 $BASENAME.mnrd.$SCHEME.l0.dec
echo "... decompress from level=1"
$EXECUTABLE decompress-$SCHEME $BASENAME.mnrd.$SCHEME.l1 $BASENAME.mnrd.$SCHEME.l1.dec
echo "... decompress from optimal"
$EXECUTABLE decompress-$SCHEME $BASENAME.mnrd.$SCHEME.lo $BASENAME.mnrd.$SCHEME.lo.dec
echo "... decompress from pessimal"
$EXECUTABLE decompress-$SCHEME $BASENAME.mnrd.$SCHEME.lp $BASENAME.mnrd.$SCHEME.lp.dec
echo "... check result from level=-1 (no compression)"
diff $BASENAME.mnrd $BASENAME.mnrd.$SCHEME.lN.dec
@@ -34,12 +42,20 @@ echo "... check result from level=0"
diff $BASENAME.mnrd $BASENAME.mnrd.$SCHEME.l0.dec
echo "... check result from level=1"
diff $BASENAME.mnrd $BASENAME.mnrd.$SCHEME.l1.dec
echo "... check result from optimal"
diff $BASENAME.mnrd $BASENAME.mnrd.$SCHEME.lo.dec
echo "... check result from pessimal"
diff $BASENAME.mnrd $BASENAME.mnrd.$SCHEME.lp.dec
echo "... clean up"
rm $BASENAME.mnrd \
$BASENAME.mnrd.$SCHEME.lN \
$BASENAME.mnrd.$SCHEME.l0 \
$BASENAME.mnrd.$SCHEME.l1 \
$BASENAME.mnrd.$SCHEME.lo \
$BASENAME.mnrd.$SCHEME.lp \
$BASENAME.mnrd.$SCHEME.lN.dec \
$BASENAME.mnrd.$SCHEME.l0.dec \
$BASENAME.mnrd.$SCHEME.l1.dec
$BASENAME.mnrd.$SCHEME.l1.dec \
$BASENAME.mnrd.$SCHEME.lo.dec \
$BASENAME.mnrd.$SCHEME.lp.dec