Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f99b3b1c8 | |||
| da9765f1aa | |||
| b7897cddf2 | |||
| ce2300b116 | |||
| cb05dce764 | |||
| a762c0f8f8 |
+1
-1
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user