Files
ReSkate/BuiltinPacker/main.cpp
T
AyeZeeBB 43b998f285 Built-in content: Board Break and Road Rash, replacing their Thunderstore mods, packed with zstd
Fourth Studio's Board Break pack (ten decks and every sound) and Road Rash wounds are built into
ReSkate.dll beside the FPV drone, unpacked under Mods/.reskate-builtin and always on.

- The Thunderstore copies (Fourth_Studio-Board_Break_Pack, Fourth_Studio-Road_Rash and the
  deprecated Fourth_Studio-Board_Break, or the same put in by hand) are moved to the Recycle Bin
  as the launcher starts and dropped from mods.json; the merge leaves them out should one still
  be there, and the Thunderstore browser no longer lists, updates or installs them, a dependency
  on one included.
- Board Break's deck list, sounds and deck settings look in the built-in packs too.
- Names with spaces inside them (Board Break's "Body Impact 1.wav") unpack.
- The build makes each zip in assets/builtin into one zstd stream (ReSkateBuiltinPacker,
  Engine/Vfs/builtin_pack_format.h), which the DLL carries: repeated files across a pack cost next
  to nothing. Board Break 15.0 -> 5.4 MB, the drone 6.0 -> 5.4 MB, Road Rash 36.1 -> 34.2 MB;
  ReSkate.dll 86 -> 70 MB. Plain zips still unpack.
- Tests: the mods taken for built-in ones and their retirement, and a pack unpacking to exactly
  its files, refusing one that leaves its folder or is cut short (RESKATE_BUILTIN_PACKS also
  unpacks the packs a build made).
2026-10-16 18:38:46 -05:00

97 lines
4.9 KiB
C++

// Builds a built-in content pack (Engine/Vfs/builtin_packs.h) from its zip in assets/builtin:
// ReSkateBuiltinPacker <pack.zip> <pack.rskpack>
// The zip's files go into one plain archive (builtin_pack_format.h) with files of the same name
// next to each other (a Board Break deck pack carries much the same files once for every deck),
// which is then compressed whole with zstd and long-distance matching. Zip compresses each file
// on its own and finds none of that; this way each repeat costs next to nothing.
#include "Engine/Vfs/builtin_pack_format.h"
#include <miniz.h>
#include <zstd.h>
#include <algorithm>
#include <cstdio>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
namespace {
struct File {
std::string name;
std::vector<unsigned char> bytes;
};
std::string base_name(const std::string& path) {
const auto slash = path.find_last_of('/');
return slash == std::string::npos ? path : path.substr(slash + 1);
}
template<class T> void put(std::vector<unsigned char>& out, T value) {
for (std::size_t i = 0; i < sizeof(T); ++i) out.push_back(static_cast<unsigned char>(value >> (8 * i)));
}
int fail(const std::string& message) {
std::cerr << "ReSkateBuiltinPacker: " << message << '\n';
return 1;
}
}
int main(int argc, char** argv) {
if (argc != 3) return fail("usage: ReSkateBuiltinPacker <pack.zip> <pack.rskpack>");
std::ifstream input(argv[1], std::ios::binary);
const std::vector<unsigned char> zip_bytes{std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
if (zip_bytes.empty()) return fail(std::string("cannot read ") + argv[1]);
mz_zip_archive zip{};
if (!mz_zip_reader_init_mem(&zip, zip_bytes.data(), zip_bytes.size(), 0)) return fail("not a zip");
std::vector<File> files;
std::uint64_t total{};
for (mz_uint index = 0; index < mz_zip_reader_get_num_files(&zip); ++index) {
mz_zip_archive_file_stat info{};
if (!mz_zip_reader_file_stat(&zip, index, &info)) return fail("an unreadable entry");
if (info.m_is_directory) continue;
File file;
file.name = info.m_filename;
if (!dingosdk::mods::builtin_pack::safe_name(file.name)) return fail("an entry the game may not have: " + file.name);
file.bytes.resize(static_cast<std::size_t>(info.m_uncomp_size));
if (!file.bytes.empty() && !mz_zip_reader_extract_to_mem(&zip, index, file.bytes.data(), file.bytes.size(), 0))
return fail("cannot unpack " + file.name);
total += file.bytes.size();
files.push_back(std::move(file));
}
mz_zip_reader_end(&zip);
if (total > dingosdk::mods::builtin_pack::max_bytes) return fail("the pack is larger than a built-in pack may be");
// Same names together (a deck pack's decks/<deck>/<file> for every deck), then by path.
std::stable_sort(files.begin(), files.end(), [](const File& a, const File& b) {
const auto ka = base_name(a.name), kb = base_name(b.name);
return ka != kb ? ka < kb : a.name < b.name;
});
std::vector<unsigned char> plain;
plain.insert(plain.end(), dingosdk::mods::builtin_pack::magic.begin(), dingosdk::mods::builtin_pack::magic.end());
put<std::uint32_t>(plain, static_cast<std::uint32_t>(files.size()));
for (const auto& file : files) {
put<std::uint16_t>(plain, static_cast<std::uint16_t>(file.name.size()));
plain.insert(plain.end(), file.name.begin(), file.name.end());
put<std::uint64_t>(plain, file.bytes.size());
}
for (const auto& file : files) plain.insert(plain.end(), file.bytes.begin(), file.bytes.end());
ZSTD_CCtx* context = ZSTD_createCCtx();
if (!context) return fail("out of memory");
// A window to cover the whole pack (within what any decoder takes by default, 2^27), so a
// file repeated anywhere in it is found.
int window = 10;
while (window < dingosdk::mods::builtin_pack::max_window_log && (std::size_t{1} << window) < plain.size()) ++window;
ZSTD_CCtx_setParameter(context, ZSTD_c_compressionLevel, 19);
ZSTD_CCtx_setParameter(context, ZSTD_c_enableLongDistanceMatching, 1);
ZSTD_CCtx_setParameter(context, ZSTD_c_windowLog, window);
ZSTD_CCtx_setParameter(context, ZSTD_c_checksumFlag, 1);
std::vector<unsigned char> packed(ZSTD_compressBound(plain.size()));
const auto size = ZSTD_compress2(context, packed.data(), packed.size(), plain.data(), plain.size());
ZSTD_freeCCtx(context);
if (ZSTD_isError(size)) return fail(std::string("zstd: ") + ZSTD_getErrorName(size));
packed.resize(size);
std::ofstream output(argv[2], std::ios::binary | std::ios::trunc);
output.write(reinterpret_cast<const char*>(packed.data()), static_cast<std::streamsize>(packed.size()));
if (!output) return fail(std::string("cannot write ") + argv[2]);
std::cout << argv[1] << ": " << files.size() << " files, " << total << " bytes -> " << packed.size() << " bytes ("
<< zip_bytes.size() << " as a zip)\n";
return 0;
}