Files
ReSkate/Engine/Vfs/Test/mod_conflicts_tests.cpp
T
AyeZeeBB fffd34ca80 Mods: conflicts resolved in MY MODS, official mods live in Mods, and big maps merge faster
MY MODS: a table with an animated drag for the load order, a conflicts bar with Check conflicts, and a Resolve popup whose choices are kept. The check runs at startup and after changes; conflicts no longer show as problems, block Play, or hold mod files during an update.
Conflicts: only game EBX that cannot be combined, or Lua scripts with different versions; colourways and same-name additions are not conflicts.
Official mods: installed in Mods/<Namespace-Name> and turned on or off through mods.json, so they show in MY MODS; the old .reskate-official folder is moved over.
Merge: a scan cache (Mods/.reskate-scan.txt) keeps each unchanged mod's repair, mesh and texture scans, so big maps are not re-read every merge.
Launcher: an announcement from the backend shows under STATUS, with HIDE.
2026-10-18 16:33:53 -05:00

78 lines
3.8 KiB
C++

// Checks a data root's enabled mods for conflicts the way the launcher's mod list does
// (mods::ConflictChecker): once, again with nothing changed, and again with the load order
// reversed, saying how long each took and which mods each would leave out. The second check
// has to find what the first did, and both later ones have to be quick: nothing is read again.
// First argument: a folder holding the game's Data/ and a Mods/ folder. Skips when none is given.
#include "Engine/Vfs/mod_conflicts.h"
#include "Engine/Vfs/mod_list.h"
#include <Windows.h>
#include <algorithm>
#include <chrono>
#include <filesystem>
#include <iostream>
#include <thread>
namespace mods = dingosdk::mods;
namespace {
void say(const char* what, const std::vector<std::string>& order, const std::vector<mods::MergeReport::Conflict>& conflicts,
std::chrono::steady_clock::duration took) {
std::cout << what << ": " << conflicts.size() << " conflicting pair(s) in "
<< std::chrono::duration_cast<std::chrono::milliseconds>(took).count() << " ms\n";
for (const auto& [lost, loss] : mods::conflict_losses(order, conflicts)) {
std::string areas;
for (const auto& area : loss.rows.front()->areas) areas += (areas.empty() ? "" : ", ") + area;
std::cout << " left out: " << lost << " (loses to " << loss.winner << ": " << areas << ")\n";
}
}
} // namespace
int main(int argc, char** argv) {
if (argc < 2 || !std::filesystem::exists(std::filesystem::path(argv[1]) / "Data")) {
std::cout << "No data root given; skipped.\n";
return 0;
}
const std::filesystem::path root(argv[1]);
const auto list = mods::scan_mods(root);
std::vector<const mods::Mod*> enabled;
for (const auto& entry : list.entries)
if (entry.enabled && entry.mod.provides_layout && entry.mod.outdated.empty()) enabled.push_back(&entry.mod);
const auto threads = std::max(1U, std::thread::hardware_concurrency()) - 1;
mods::ConflictChecker checker(root);
const auto timed = [&](const std::vector<const mods::Mod*>& order, const char* what) {
std::vector<std::string> names;
for (const auto* mod : order) names.push_back(mod->name);
const auto start = std::chrono::steady_clock::now();
auto conflicts = checker.check(order, threads);
say(what, names, conflicts, std::chrono::steady_clock::now() - start);
return conflicts;
};
std::cout << enabled.size() << " enabled mod(s)\n";
const auto first = timed(enabled, "first check");
const auto again = timed(enabled, "again, nothing changed");
auto reversed = enabled;
std::reverse(reversed.begin(), reversed.end());
(void)timed(reversed, "load order reversed");
// Nothing is held open between checks: every mod's archives can be taken alone, as an update
// replacing them needs.
std::size_t held{}, tried{};
for (const auto* mod : enabled) {
std::error_code error;
for (std::filesystem::recursive_directory_iterator at(mod->directory, error), end; !error && at != end; at.increment(error)) {
if (at->path().extension() != L".cas") continue;
++tried;
const auto file = CreateFileW(at->path().c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (file == INVALID_HANDLE_VALUE) ++held;
else CloseHandle(file);
}
}
std::cout << tried << " mod archive(s) tried after the checks, " << held << " still held\n";
const bool same = held == 0 && first.size() == again.size() &&
std::equal(first.begin(), first.end(), again.begin(), [](const auto& a, const auto& b) {
return a.kept == b.kept && a.lost == b.lost && a.assets == b.assets; });
std::cout << (same ? "mod conflicts: ok\n" : "mod conflicts: the second check differs from the first, or a file is held\n");
return same ? 0 : 1;
}