From 9c03edda34baccd042d48de72209d2c7ddd774f1 Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Sun, 4 Oct 2026 00:37:15 -0500 Subject: [PATCH] Show each mod's size in the mod manager Show each mod's size in the mod manager A mod's size was only visible in the package overview and the install prompt, so there was no telling from either list what a mod costs to download or how much room the installed ones take. - MY MODS: each row carries the size of the mod's folder on disk after the version ("v1.0.1 / 410 MB / by anton / 1 map"), and so does the line under the title in a mod's overview. The folders are measured when the Mods folder is scanned, so the figure follows an install, an update or Refresh. The game's merged patch hard links a mod's archives rather than copying them, so this is also what removing the mod gives back. - GET MODS: each row carries the download size Thunderstore reports for the latest version, in the same place ("v1.0.0 / 101 MB / 0 downloads / 2026-10-04"), as the package overview already did. - size_text moves out of gui_mods_browse.cpp's file-local namespace so both pages format a size the same way. The two figures are not the same thing: one is the unpacked folder, the other the zip, so a mod reads a little larger once installed. --- Launcher/gui_internal.h | 5 +++++ Launcher/gui_mods.cpp | 27 ++++++++++++++++++++++++--- Launcher/gui_mods_browse.cpp | 17 +++++++++-------- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/Launcher/gui_internal.h b/Launcher/gui_internal.h index fbddd81..16ab218 100644 --- a/Launcher/gui_internal.h +++ b/Launcher/gui_internal.h @@ -379,6 +379,9 @@ struct ModsPanel { bool scanned{}; fs::path root; mods::ModList list; + // What each installed mod's folder holds on disk, by folder name; measured + // when the folder is scanned. + std::map> sizes; int selected{-1}; // the mod whose overview is open bool overview{}; // the overview popup is showing int tab{}; // 0 MY MODS, 1 GET MODS @@ -414,6 +417,8 @@ void start_install(ModsPanel& panel, const fs::path& source, bool replace); // ---------------------------------------------------------------- Thunderstore (gui_mods_browse.cpp) +// "1.27 GB", "263 MB", "12 KB": a mod's size, on both pages. +std::string size_text(std::uint64_t bytes); // Starts a listing fetch when none ran yet, or when `force`. void refresh_listing(ModsPanel& panel, double time, bool force = false); // Takes over a fetch the worker finished; call once a frame. diff --git a/Launcher/gui_mods.cpp b/Launcher/gui_mods.cpp index c112dd4..efe28c8 100644 --- a/Launcher/gui_mods.cpp +++ b/Launcher/gui_mods.cpp @@ -80,9 +80,28 @@ fs::path pick(HWND owner, bool folder) { return result; } -std::string summary(const mods::Mod& mod) { +// Everything in the mod's folder, as Explorer would count it. The game's merged +// patch hard links a mod's archives rather than copying them, so this is also +// what removing the mod gives back. +std::uint64_t folder_size(const fs::path& directory) { + std::uint64_t total{}; + std::error_code error; + for (fs::recursive_directory_iterator it(directory, fs::directory_options::skip_permission_denied, error), end; + it != end && !error; it.increment(error)) { + std::error_code ignored; + if (it->is_regular_file(ignored) && !ignored) { + const auto size = it->file_size(ignored); + if (!ignored) total += size; + } + } + return total; +} + +std::string summary(const ModsPanel& panel, const mods::Mod& mod) { std::vector parts; if (!mod.version.empty()) parts.push_back("v" + mod.version); + if (const auto size = panel.sizes.find(mod.name); size != panel.sizes.end() && size->second) + parts.push_back(size_text(size->second)); if (!mod.author.empty()) parts.push_back("by " + mod.author); if (!mod.levels.empty()) parts.push_back(mod.levels.size() == 1 ? "1 map" : std::to_string(mod.levels.size()) + " maps"); else if (mod.provides_layout) parts.push_back("game data"); @@ -169,7 +188,7 @@ void installed_row(const Fonts& fonts, ModsPanel& panel, const thunderstore::Ins const auto title = std::to_string(index + 1) + ". " + mod.title; draw->AddText(fonts.bold, fonts.bold->FontSize, ImVec2(text_x, start.y + S(16)), entry.enabled ? color::text : color::muted, title.c_str()); - if (const auto detail = summary(mod); !detail.empty()) + if (const auto detail = summary(panel, mod); !detail.empty()) draw->AddText(fonts.body, fonts.body->FontSize, ImVec2(text_x, start.y + S(40)), color::muted, detail.c_str()); // The widgets that sit on the row, over its Selectable. @@ -316,7 +335,7 @@ void mod_overview(const Fonts& fonts, ModsPanel& panel, const thunderstore::Inst ImGui::TextUnformatted(mod.title.c_str()); ImGui::PopTextWrapPos(); ImGui::PopFont(); - ImGui::TextDisabled("%s", summary(mod).c_str()); + ImGui::TextDisabled("%s", summary(panel, mod).c_str()); ImGui::Spacing(); if (update) { ImGui::BeginDisabled(installing); @@ -463,6 +482,8 @@ void refresh_mods(Launcher& launcher, ModsPanel& panel) { void scan(ModsPanel& panel, const launcher_app::Session& session) { panel.root = launcher_mods::mods_root(session.paths.directory); panel.list = mods::scan_mods(panel.root.parent_path()); + panel.sizes.clear(); + for (const auto& entry : panel.list.entries) panel.sizes[entry.mod.name] = folder_size(entry.mod.directory); panel.scanned = true; if (panel.selected >= static_cast(panel.list.entries.size())) panel.selected = -1; } diff --git a/Launcher/gui_mods_browse.cpp b/Launcher/gui_mods_browse.cpp index 7354085..827ed9d 100644 --- a/Launcher/gui_mods_browse.cpp +++ b/Launcher/gui_mods_browse.cpp @@ -137,12 +137,6 @@ std::string count_text(std::uint64_t value) { return std::to_string(value); } -std::string size_text(std::uint64_t bytes) { - if (bytes >= 1024ull * 1024 * 1024) return std::format("{:.2f} GB", static_cast(bytes) / (1024.0 * 1024 * 1024)); - if (bytes >= 1024ull * 1024) return std::format("{:.0f} MB", static_cast(bytes) / (1024.0 * 1024)); - return std::format("{} KB", std::max(1, bytes / 1024)); -} - std::string date_text(const std::string& iso) { return iso.substr(0, std::min(iso.size(), 10)); } void set_activity(ModsPanel& panel, std::string text) { @@ -252,6 +246,12 @@ void mod_icon(ModsPanel& panel, const thunderstore::Package* package, ImVec2 pos draw->AddRectFilled(position, end, rgba(255, 255, 255, 0.06f), S(4)); } +std::string size_text(std::uint64_t bytes) { + if (bytes >= 1024ull * 1024 * 1024) return std::format("{:.2f} GB", static_cast(bytes) / (1024.0 * 1024 * 1024)); + if (bytes >= 1024ull * 1024) return std::format("{:.0f} MB", static_cast(bytes) / (1024.0 * 1024)); + return std::format("{} KB", std::max(1, bytes / 1024)); +} + void refresh_listing(ModsPanel& panel, double time, bool force) { auto& store = panel.store; // Once per launch, unless asked; a failed fetch retries after a minute. @@ -569,8 +569,9 @@ void browse_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, float draw->AddText(fonts.body, fonts.body->FontSize, ImVec2(text_x, start.y + S(32)), color::text, line.c_str(), nullptr, 0, &clip); draw->AddText(fonts.caption, fonts.caption->FontSize, ImVec2(text_x, start.y + S(55)), color::muted, - std::format("v{} / {} downloads / {}", version.number, count_text(package.downloads), - date_text(package.date_updated)).c_str()); + std::format("v{}{} / {} downloads / {}", version.number, + version.file_size ? " / " + size_text(version.file_size) : std::string(), + count_text(package.downloads), date_text(package.date_updated)).c_str()); ImGui::PopID(); }); ImGui::EndDisabled();