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_number;
uint16_t card_battle_table_seat_state; uint16_t card_battle_table_seat_state;
std::weak_ptr<Episode3::Tournament::Team> ep3_tournament_team; 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; std::shared_ptr<const Menu> last_menu_sent;
uint32_t last_game_info_requested; uint32_t last_game_info_requested;
struct JoinCommand { 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. // memory card), use A7 instead.
// All chunks except the last must have 0x400 data bytes. When downloading an // 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 // 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) // header.flag = file chunk index (start offset / 0x400)
struct S_WriteFile_13_A7 { 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); 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( PRSCompressor::PRSCompressor(
ssize_t compression_level, ProgressCallback progress_fn) ssize_t compression_level, ProgressCallback progress_fn)
: compression_level(compression_level), : 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 void* vdata, size_t size, ProgressCallback progress_fn = nullptr);
std::string prs_compress_optimal(const std::string& data, 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. // Decompresses PRS-compressed data.
struct PRSDecompressResult { struct PRSDecompressResult {
std::string data; std::string data;
+27
View File
@@ -266,6 +266,7 @@ static void a_compress_decompress_fn(Arguments& args) {
bool is_decompress = starts_with(action, "decompress-"); bool is_decompress = starts_with(action, "decompress-");
bool is_big_endian = args.get<bool>("big-endian"); bool is_big_endian = args.get<bool>("big-endian");
bool is_optimal = args.get<bool>("optimal"); 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); int8_t compression_level = args.get<int8_t>("compression-level", 0);
size_t bytes = args.get<size_t>("bytes", 0); size_t bytes = args.get<size_t>("bytes", 0);
string seed = args.get<string>("seed"); 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_decompress && (is_prs || is_pr2 || is_prc)) {
if (is_optimal) { if (is_optimal) {
data = prs_compress_optimal(data.data(), data.size(), optimal_progress_fn); data = prs_compress_optimal(data.data(), data.size(), optimal_progress_fn);
} else if (is_pessimal) {
data = prs_compress_pessimal(data.data(), data.size());
} else { } else {
data = prs_compress(data, compression_level, progress_fn); data = prs_compress(data, compression_level, progress_fn);
} }
@@ -2647,6 +2650,30 @@ int main(int argc, char** argv) {
log_error("Unknown or invalid action; try --help"); log_error("Unknown or invalid action; try --help");
return 1; 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); 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; return 0;
} }
+18 -4
View File
@@ -395,18 +395,32 @@ void PatchServer::on_client_error(Channel& ch, short events) {
} }
PatchServer::PatchServer(shared_ptr<const Config> config) PatchServer::PatchServer(shared_ptr<const Config> config)
: base(event_base_new(), event_base_free), : config(config) {
config(config), if (config->shared_base) {
destroy_clients_ev(event_new(this->base.get(), -1, EV_TIMEOUT, &PatchServer::dispatch_destroy_clients, this), event_free), this->base = config->shared_base;
th(&PatchServer::thread_fn, this) {} 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() { void PatchServer::schedule_stop() {
if (!this->base_is_shared) {
event_base_loopexit(this->base.get(), nullptr); event_base_loopexit(this->base.get(), nullptr);
} }
}
void PatchServer::wait_for_stop() { void PatchServer::wait_for_stop() {
if (!this->base_is_shared) {
this->th.join(); this->th.join();
} }
}
void PatchServer::listen(const std::string& addr_str, const string& socket_path, Version version) { void PatchServer::listen(const std::string& addr_str, const string& socket_path, Version version) {
int fd = ::listen(socket_path, 0, SOMAXCONN); int fd = ::listen(socket_path, 0, SOMAXCONN);
+2
View File
@@ -22,6 +22,7 @@ public:
std::string message; std::string message;
std::shared_ptr<const LicenseIndex> license_index; std::shared_ptr<const LicenseIndex> license_index;
std::shared_ptr<const PatchFileIndex> patch_file_index; std::shared_ptr<const PatchFileIndex> patch_file_index;
std::shared_ptr<struct event_base> shared_base;
}; };
PatchServer() = delete; PatchServer() = delete;
@@ -86,6 +87,7 @@ private:
}; };
std::shared_ptr<struct event_base> base; std::shared_ptr<struct event_base> base;
bool base_is_shared;
std::shared_ptr<const Config> config; std::shared_ptr<const Config> config;
std::unordered_set<std::shared_ptr<Client>> clients_to_destroy; std::unordered_set<std::shared_ptr<Client>> clients_to_destroy;
+16 -10
View File
@@ -490,7 +490,7 @@ QuestIndex::QuestIndex(
continue; 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) { if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) {
throw runtime_error("file " + basename + " exists in multiple categories"); throw runtime_error("file " + basename + " exists in multiple categories");
} }
@@ -498,6 +498,12 @@ QuestIndex::QuestIndex(
if (!files.emplace(basename, FileData{filename, data_ptr}).second) { if (!files.emplace(basename, FileData{filename, data_ptr}).second) {
throw runtime_error("file " + basename + " already exists"); 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; string cat_path = directory + "/" + cat->directory_name;
@@ -544,26 +550,26 @@ QuestIndex::QuestIndex(
} }
if (extension == "json") { 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") { } 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") { } 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") { } 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") { } 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") { } 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") { } else if (extension == "qst") {
auto files = decode_qst_data(file_data); auto files = decode_qst_data(file_data);
for (auto& it : files) { for (auto& it : files) {
if (ends_with(it.first, ".bin")) { 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")) { } 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")) { } 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 { } else {
throw runtime_error("qst file contains unsupported file type: " + it.first); 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 { shared_ptr<PatchServer::Config> ServerState::generate_patch_server_config(bool is_bb) const {
auto ret = make_shared<PatchServer::Config>(); 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->allow_unregistered_users = this->allow_unregistered_users;
ret->hide_data_from_logs = this->hide_download_commands; ret->hide_data_from_logs = this->hide_download_commands;
ret->idle_timeout_usecs = this->patch_client_idle_timeout_usecs; 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 $EXECUTABLE compress-$SCHEME --compression-level=0 $BASENAME.mnrd $BASENAME.mnrd.$SCHEME.l0
echo "... compress with level=1" echo "... compress with level=1"
$EXECUTABLE compress-$SCHEME --compression-level=1 $BASENAME.mnrd $BASENAME.mnrd.$SCHEME.l1 $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)" echo "... decompress from level=-1 (no compression)"
$EXECUTABLE decompress-$SCHEME $BASENAME.mnrd.$SCHEME.lN $BASENAME.mnrd.$SCHEME.lN.dec $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 $EXECUTABLE decompress-$SCHEME $BASENAME.mnrd.$SCHEME.l0 $BASENAME.mnrd.$SCHEME.l0.dec
echo "... decompress from level=1" echo "... decompress from level=1"
$EXECUTABLE decompress-$SCHEME $BASENAME.mnrd.$SCHEME.l1 $BASENAME.mnrd.$SCHEME.l1.dec $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)" echo "... check result from level=-1 (no compression)"
diff $BASENAME.mnrd $BASENAME.mnrd.$SCHEME.lN.dec 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 diff $BASENAME.mnrd $BASENAME.mnrd.$SCHEME.l0.dec
echo "... check result from level=1" echo "... check result from level=1"
diff $BASENAME.mnrd $BASENAME.mnrd.$SCHEME.l1.dec 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" echo "... clean up"
rm $BASENAME.mnrd \ rm $BASENAME.mnrd \
$BASENAME.mnrd.$SCHEME.lN \ $BASENAME.mnrd.$SCHEME.lN \
$BASENAME.mnrd.$SCHEME.l0 \ $BASENAME.mnrd.$SCHEME.l0 \
$BASENAME.mnrd.$SCHEME.l1 \ $BASENAME.mnrd.$SCHEME.l1 \
$BASENAME.mnrd.$SCHEME.lo \
$BASENAME.mnrd.$SCHEME.lp \
$BASENAME.mnrd.$SCHEME.lN.dec \ $BASENAME.mnrd.$SCHEME.lN.dec \
$BASENAME.mnrd.$SCHEME.l0.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