mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-27 17:27:08 +01:00
Mod Manager: a pause menu tab after Mod Options to browse, install, update and switch mods in game
A third ReSkate tab in the game's pause menu (native_menu.cpp slot 2, the Store's tab art), drawn by the overlay as the other two are (mod_manager_page.inl, included into hub_page.cpp): - BROWSE: search, categories in a panel of their own (shown only, hidden or either, kept in the profile), sort, a featured carousel and rows (most popular, recently updated, new, the biggest categories), a grid for results, and a mod's own page with its README, what it brings with it, and INSTALL, switch on or off and uninstall. Works from a controller: the carousel's card is one stop that left and right turn, and B goes back from a mod's page (the page's Back is held meanwhile). - MY MODS: the Mods folder with the states the ReSkate menu shows; UPDATES: Update all. - The foot of the page shows the install's progress and the apply's, and Apply (View on a pad). Leaving the menu with changes not applied asks first (a two-answer card in message_card.cpp, answered by the runtime as the message is); LATER closes the menu. Behind it, Extension/Assets/mod_store: Thunderstore's listing (cached on disk), the installed mods, and a queue that downloads and installs packages with their dependencies, updates and uninstalls them. An update or uninstall of a mod the game holds open waits in Mods/.reskate-pending and the launcher makes it before the next start (launcher_mods::apply_pending). Package icons load into their own SRV slots (store_icons.cpp), sharing the launcher's icon cache. Tests: mod_store_view (search, categories, the front page's rows, install plans), the pending updates and uninstalls in mod_manager_tests, and the third tab in native_menu_view_tests.
This commit is contained in:
1 parent
54716f2798
commit
725c18ef3d
25 files changed
+3164
-52
No files matched your search
@@ -485,4 +485,112 @@ void remove(const fs::path& mods, const std::string& name) {
|
||||
recycle(target);
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr wchar_t removals_file[] = L"remove.txt";
|
||||
fs::path pending_root(const fs::path& mods) { return mods / from_utf8(pending_folder); }
|
||||
std::vector<std::string> read_removals(const fs::path& mods) {
|
||||
std::vector<std::string> names;
|
||||
std::ifstream in(pending_root(mods) / removals_file);
|
||||
for (std::string line; std::getline(in, line);) {
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) line.pop_back();
|
||||
if (mods::valid_mod_name(line) && std::find(names.begin(), names.end(), line) == names.end()) names.push_back(line);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
// Replaces remove.txt (or removes it when there is nothing in it).
|
||||
void write_removals(const fs::path& mods, const std::vector<std::string>& names) {
|
||||
const auto file = pending_root(mods) / removals_file;
|
||||
std::error_code error;
|
||||
if (names.empty()) {
|
||||
fs::remove(file, error);
|
||||
return;
|
||||
}
|
||||
fs::create_directories(file.parent_path());
|
||||
const auto temporary = fs::path(file).concat(L".tmp");
|
||||
{
|
||||
std::ofstream out(temporary, std::ios::binary | std::ios::trunc);
|
||||
for (const auto& name : names) out << name << '\n';
|
||||
if (!out) fail("Could not note the mod to uninstall (is the disk full?).");
|
||||
}
|
||||
if (!MoveFileExW(temporary.c_str(), file.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
|
||||
fail("Could not note the mod to uninstall.");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string install_later(const fs::path& mods, const fs::path& source, const Progress& progress,
|
||||
const std::atomic<bool>& cancel, const InstallOptions& options) {
|
||||
const auto name = install(pending_root(mods), source, true, progress, cancel, options);
|
||||
auto removals = read_removals(mods);
|
||||
if (std::erase(removals, name)) write_removals(mods, removals);
|
||||
return name;
|
||||
}
|
||||
|
||||
void remove_later(const fs::path& mods, const std::string& name) {
|
||||
if (!mods::valid_mod_name(name)) fail("\"" + name + "\" is not a mod folder.");
|
||||
std::error_code error;
|
||||
fs::remove_all(pending_root(mods) / from_utf8(name), error);
|
||||
auto removals = read_removals(mods);
|
||||
if (std::find(removals.begin(), removals.end(), name) == removals.end()) removals.push_back(name);
|
||||
write_removals(mods, removals);
|
||||
}
|
||||
|
||||
Pending pending(const fs::path& mods) noexcept {
|
||||
Pending result;
|
||||
try {
|
||||
std::error_code error;
|
||||
for (fs::directory_iterator it(pending_root(mods), error), end; !error && it != end; it.increment(error)) {
|
||||
const auto name = utf8(it->path().filename().wstring());
|
||||
// (install()'s own staging folder is a dot folder too.)
|
||||
if (it->is_directory(error) && mods::valid_mod_name(name) && !name.ends_with(".replaced")) result.updates.push_back(name);
|
||||
}
|
||||
result.removals = read_removals(mods);
|
||||
} catch (...) {}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> apply_pending(const fs::path& mods) noexcept {
|
||||
std::vector<std::string> lines;
|
||||
try {
|
||||
const auto waiting = pending(mods);
|
||||
if (waiting.updates.empty() && waiting.removals.empty()) return lines;
|
||||
const auto root = pending_root(mods);
|
||||
for (const auto& name : waiting.updates) {
|
||||
const auto target = mods / from_utf8(name), copy = root / from_utf8(name);
|
||||
const auto aside = root / (from_utf8(name).wstring() + L".replaced");
|
||||
std::error_code error;
|
||||
fs::remove_all(aside, error);
|
||||
if (fs::exists(target, error) && !MoveFileExW(target.c_str(), aside.c_str(), MOVEFILE_WRITE_THROUGH)) {
|
||||
lines.push_back("The update of " + name + " waits: its folder is in use (Windows error " + std::to_string(GetLastError()) + ").");
|
||||
continue;
|
||||
}
|
||||
if (!MoveFileExW(copy.c_str(), target.c_str(), MOVEFILE_WRITE_THROUGH)) {
|
||||
const auto code = GetLastError();
|
||||
MoveFileExW(aside.c_str(), target.c_str(), MOVEFILE_WRITE_THROUGH);
|
||||
lines.push_back("The update of " + name + " waits: it could not be moved in (Windows error " + std::to_string(code) + ").");
|
||||
continue;
|
||||
}
|
||||
fs::remove_all(aside, error);
|
||||
lines.push_back("Updated " + name + " (installed in game, waiting for this start).");
|
||||
}
|
||||
std::vector<std::string> kept;
|
||||
for (const auto& name : waiting.removals) {
|
||||
std::error_code error;
|
||||
if (!fs::is_directory(mods / from_utf8(name), error)) continue;
|
||||
try {
|
||||
recycle(mods / from_utf8(name));
|
||||
lines.push_back("Uninstalled " + name + " (asked for in game).");
|
||||
} catch (const std::exception& failure) {
|
||||
kept.push_back(name);
|
||||
lines.push_back("Uninstalling " + name + " waits: " + failure.what());
|
||||
}
|
||||
}
|
||||
try { write_removals(mods, kept); } catch (const std::exception& failure) { lines.push_back(failure.what()); }
|
||||
std::error_code error;
|
||||
if (fs::is_empty(root, error)) fs::remove(root, error);
|
||||
} catch (const std::exception& failure) {
|
||||
lines.push_back(std::string("Mod changes waiting from the game could not be made: ") + failure.what());
|
||||
} catch (...) {}
|
||||
return lines;
|
||||
}
|
||||
|
||||
} // namespace dingosdk::launcher_mods
|
||||
Reference in new issue
Block a user