mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-26 20:56:42 +01:00
Launcher: GET MODS installs plugin mods and mod packs instead of refusing them
A Thunderstore install was refused unless the package held layout.toc, reskate-levels.json or a parks folder, so a package with only a plugin, or a pack whose manifest only lists dependencies, could not be installed from GET MODS. reskate-plugin.json now counts as content (and finds the mod inside a package like the other markers), and a manifest with dependencies is a mod pack, as InstallOptions::require_content always said.
This commit is contained in:
1 parent
b978853bab
commit
7fa407ac36
3 files changed
+73
-20
No files matched your search
@@ -403,6 +403,45 @@ int main() {
|
||||
!fs::exists(store / L"Team-Empty"),
|
||||
"Thunderstore installs refuse a package with nothing to load");
|
||||
check(install(store, sources / L"empty.zip") == "empty", "A plain install still takes a description-only mod");
|
||||
make_zip(sources / L"empty-list.zip", {{"manifest.json", R"({"name":"Nothing","dependencies":[]})"}});
|
||||
options.folder = "Team-Empty_List";
|
||||
check(error_of([&] { launcher_mods::install(store, sources / L"empty-list.zip", true, {}, cancel, options); })
|
||||
.find("plugins and mod packs") != std::string::npos &&
|
||||
!fs::exists(store / L"Team-Empty_List"),
|
||||
"An empty dependencies list is not a mod pack, and the refusal names plugins and packs");
|
||||
|
||||
// A plugin-only package: reskate-plugin.json is something to load, at the top or one folder down.
|
||||
make_zip(sources / L"plugin.zip", {{"manifest.json", manifest}, {"icon.png", "png"}, {"README.md", "readme"},
|
||||
{"reskate-plugin.json", R"({"api":1})"}, {"main.luau", "-- plugin"}});
|
||||
options.folder = "Team-Plugin";
|
||||
check(launcher_mods::install(store, sources / L"plugin.zip", true, {}, cancel, options) == "Team-Plugin" &&
|
||||
read(store / L"Team-Plugin" / L"main.luau") == "-- plugin" &&
|
||||
read(store / L"Team-Plugin" / L"icon.png") == "png",
|
||||
"Thunderstore installs take a plugin-only package");
|
||||
make_zip(sources / L"nested-plugin.zip", {{"manifest.json", manifest}, {"icon.png", "png"}, {"README.md", "readme"},
|
||||
{"Plugin/reskate-plugin.json", R"({"api":1})"}, {"Plugin/main.luau", "-- nested"}});
|
||||
options.folder = "Team-Nested_Plugin";
|
||||
check(launcher_mods::install(store, sources / L"nested-plugin.zip", true, {}, cancel, options) == "Team-Nested_Plugin" &&
|
||||
read(store / L"Team-Nested_Plugin" / L"reskate-plugin.json") == R"({"api":1})" &&
|
||||
read(store / L"Team-Nested_Plugin" / L"main.luau") == "-- nested" &&
|
||||
read(store / L"Team-Nested_Plugin" / L"icon.png") == "png" &&
|
||||
read(store / L"Team-Nested_Plugin" / L"README.md") == "readme" &&
|
||||
!fs::exists(store / L"Team-Nested_Plugin" / L"Plugin"),
|
||||
"A plugin one folder down is the mod, and the package files follow it in");
|
||||
const auto plugin_manifest = read(store / L"Team-Nested_Plugin" / L"manifest.json");
|
||||
check(plugin_manifest.find("Desert_Springs") != std::string::npos && plugin_manifest.find(R"("Zee")") != std::string::npos,
|
||||
"The carried manifest.json gets the team as author");
|
||||
|
||||
// A mod pack: nothing of its own to load, only dependencies the launcher installs first.
|
||||
make_zip(sources / L"modpack.zip", {
|
||||
{"manifest.json", R"({"name":"Pack","version_number":"1.0.0","dependencies":["Zee-Desert_Springs-1.2.3"]})"},
|
||||
{"icon.png", "png"}, {"README.md", "readme"}});
|
||||
options.folder = "Team-Pack";
|
||||
check(launcher_mods::install(store, sources / L"modpack.zip", true, {}, cancel, options) == "Team-Pack" &&
|
||||
read(store / L"Team-Pack" / L"icon.png") == "png",
|
||||
"Thunderstore installs take a mod pack that only lists dependencies");
|
||||
const auto pack = installed("Team-Pack");
|
||||
check(pack && pack->author == "Zee" && pack->version == "1.0.0", "A mod pack is listed with its team as author");
|
||||
}
|
||||
|
||||
// A built-in pack in ReSkate's own format unpacks to exactly its files (a name with spaces
|
||||
|
||||
+32
-18
@@ -23,13 +23,15 @@ namespace fs = std::filesystem;
|
||||
namespace dingosdk::launcher_mods {
|
||||
namespace {
|
||||
|
||||
// What the game loads from a mod, and the files that only describe one.
|
||||
constexpr std::array<std::string_view, 2> content_markers{"layout.toc", "reskate-levels.json"};
|
||||
// What the game loads from a mod (a plugin's reskate-plugin.json sits at its
|
||||
// mod's top), and the files that only describe one.
|
||||
constexpr std::array<std::string_view, 3> content_markers{"layout.toc", "reskate-levels.json", "reskate-plugin.json"};
|
||||
constexpr std::array<std::string_view, 2> info_markers{"manifest.json", "reskate-mod.json"};
|
||||
// Thunderstore requires these at the top of a package (lower case here); they
|
||||
// follow the mod in when it is packed one folder deeper.
|
||||
constexpr std::array<std::string_view, 4> package_files{"manifest.json", "icon.png", "readme.md", "changelog.md"};
|
||||
constexpr char no_mod[] = " does not contain a ReSkate mod (no layout.toc, reskate-levels.json or manifest.json).";
|
||||
constexpr char no_mod[] =
|
||||
" does not contain a ReSkate mod (no layout.toc, reskate-levels.json, reskate-plugin.json or manifest.json).";
|
||||
// Staging lives inside Mods so the final move is a rename on one volume; the
|
||||
// leading dot keeps the runtime from ever treating it as a mod.
|
||||
constexpr wchar_t staging_folder[] = L".reskate-install";
|
||||
@@ -84,8 +86,9 @@ struct Root {
|
||||
bool content{}; // holds something the game loads
|
||||
};
|
||||
|
||||
// The shallowest folder holding game content (layout.toc, reskate-levels.json
|
||||
// or parks/<map>.park.json), else the shallowest holding only a description.
|
||||
// The shallowest folder holding game content (layout.toc, reskate-levels.json,
|
||||
// reskate-plugin.json or parks/<map>.park.json), else the shallowest holding
|
||||
// only a description.
|
||||
// Content wins so a package's own manifest.json at the top cannot hide a mod
|
||||
// packed one folder down.
|
||||
std::optional<Root> mod_root(const std::vector<std::string>& files) {
|
||||
@@ -146,15 +149,11 @@ std::string mod_name(const Root& root, const std::string& source_name, const Ins
|
||||
return folder_name(root.folder.empty() ? without_version(source_name) : last_folder(root.folder));
|
||||
}
|
||||
|
||||
// Last steps in staging: a source that gives the game nothing to load is
|
||||
// refused when asked, and the author goes into manifest.json.
|
||||
void finish(const fs::path& staging, const Root& root, const std::string& source_name, const InstallOptions& options) {
|
||||
if (options.require_content && !root.content)
|
||||
fail(source_name + " has nothing for ReSkate to load: no layout.toc, reskate-levels.json or parks folder. "
|
||||
"Only mods built with ReSkate Studio, and park mods, can be installed.");
|
||||
const auto manifest = staging / mods::manifest_file;
|
||||
// The mod's manifest.json as an object, if it has a readable one. An unreadable
|
||||
// manifest is the mod's own business; the game reports it.
|
||||
std::optional<Json> read_manifest(const fs::path& manifest) {
|
||||
std::error_code error;
|
||||
if (options.author.empty() || !fs::is_regular_file(manifest, error)) return;
|
||||
if (!fs::is_regular_file(manifest, error)) return std::nullopt;
|
||||
try {
|
||||
std::string text;
|
||||
{
|
||||
@@ -163,13 +162,28 @@ void finish(const fs::path& staging, const Root& root, const std::string& source
|
||||
}
|
||||
if (text.starts_with("\xef\xbb\xbf")) text.erase(0, 3);
|
||||
auto json = Json::parse(text, JsonLimits{1024 * 1024, 16, 65536});
|
||||
if (!json.is_object()) return;
|
||||
if (json.contains("author") && json.at("author").is_string() && !json.at("author").string().empty()) return;
|
||||
json.items()["author"] = Json(options.author);
|
||||
std::ofstream(manifest, std::ios::binary | std::ios::trunc) << json.dump(4) << '\n';
|
||||
if (json.is_object()) return json;
|
||||
} catch (...) {
|
||||
// An unreadable manifest is the mod's own business; the game reports it.
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Last steps in staging: a source that gives the game nothing to load is
|
||||
// refused when asked, unless its manifest.json lists dependencies (a mod pack:
|
||||
// what it brings is those), and the author goes into manifest.json.
|
||||
void finish(const fs::path& staging, const Root& root, const std::string& source_name, const InstallOptions& options) {
|
||||
const auto manifest = staging / mods::manifest_file;
|
||||
auto json = read_manifest(manifest);
|
||||
const bool pack = json && json->contains("dependencies") && json->at("dependencies").is_array() &&
|
||||
!json->at("dependencies").empty();
|
||||
if (options.require_content && !root.content && !pack)
|
||||
fail(source_name + " has nothing for ReSkate to load: no layout.toc, reskate-levels.json, reskate-plugin.json "
|
||||
"or parks folder, and no dependencies. Only mods built with ReSkate Studio, park mods, plugins and mod "
|
||||
"packs can be installed.");
|
||||
if (options.author.empty() || !json) return;
|
||||
if (json->contains("author") && json->at("author").is_string() && !json->at("author").string().empty()) return;
|
||||
json->items()["author"] = Json(options.author);
|
||||
std::ofstream(manifest, std::ios::binary | std::ios::trunc) << json->dump(4) << '\n';
|
||||
}
|
||||
|
||||
// Entry names from an archive are untrusted: no absolute paths, drive
|
||||
|
||||
@@ -40,8 +40,8 @@ struct InstallOptions {
|
||||
};
|
||||
|
||||
// Installs `source` (a .zip, or a folder) as Mods/<name> and returns <name>.
|
||||
// The mod is the shallowest folder holding layout.toc, reskate-levels.json or
|
||||
// parks/<map>.park.json, else the shallowest holding manifest.json or
|
||||
// The mod is the shallowest folder holding layout.toc, reskate-levels.json,
|
||||
// reskate-plugin.json or parks/<map>.park.json, else the shallowest holding manifest.json or
|
||||
// reskate-mod.json. Thunderstore's manifest.json, icon.png, README.md and
|
||||
// CHANGELOG.md at the top of a package are copied in when the mod sits one
|
||||
// folder deeper. A top-level mod takes the archive's name without a trailing
|
||||
|
||||
Reference in new issue
Block a user