using System; using System.Collections.Generic; using Cthangover.Core.Mods; using Godot; namespace Mods.ModManager { /// /// Merges everything known about mods into the manager's grid model: local /// mods discovered by (with enable state and /// deletability) and online catalog entries from . /// A mod present in both sources becomes one combined card; catalog-only mods /// appear as installable tiles. /// /// The model also owns the session overrides that keep the UI honest after /// file operations: a mod installed or deleted from the manager is not yet /// reflected by the registry (which is loaded once per launch), so /// / adjust the view and /// the restart banner tells the player when the change becomes real. /// public class ModManagerModel { /// Id of the mod that renders this UI; it can never be disabled or removed. public const string SelfModId = "mod_manager"; private readonly List _items = new(); private readonly HashSet _removed = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _installed = new(StringComparer.OrdinalIgnoreCase); /// All cards, unsorted and unfiltered; rebuild with . public IReadOnlyList Items => _items; /// /// Rebuilds the card list from the current registry, disabled store and /// catalog cache. Safe to call on every action completion and after a /// catalog refresh. /// public void Reload() { _items.Clear(); var disabled = DisabledModsStore.Load(); var variantsById = BuildCatalogVariants(); var localIds = new HashSet(StringComparer.OrdinalIgnoreCase); var ordered = ModRegistry.Instance.GetOrderedModIds(filterDisabled: false); foreach (var id in ordered) { if (_removed.Contains(id)) continue; if (!ModRegistry.Instance.AllDiscoveredMods.TryGetValue(id, out var info) || info is not ModInfo modInfo) continue; localIds.Add(id); var isDisabled = disabled.Contains(id); if (variantsById.TryGetValue(id, out var variants)) { foreach (var variant in variants) _items.Add(BuildLocalCard(id, modInfo, isDisabled, variant)); } else { _items.Add(BuildLocalCard(id, modInfo, isDisabled, null)); } } foreach (var pair in variantsById) { if (localIds.Contains(pair.Key) || _removed.Contains(pair.Key)) continue; foreach (var variant in pair.Value) _items.Add(BuildRemoteCard(variant)); } } /// Remembers that a mod was deleted this session so it stays hidden until restart. public void MarkRemoved(string modId) { _removed.Add(modId); _installed.Remove(modId); } /// Remembers a successful install so the card shows as installed before restart. public void MarkInstalled(string modId, string version) { _removed.Remove(modId); _installed[modId] = version ?? string.Empty; } /// /// Applies an enable/disable toggle with the same cascade as the legacy /// menu: disabling a mod also disables its dependents, enabling one also /// re-enables the dependencies it needs. /// public void SetEnabled(string modId, bool enabled) { var disabled = DisabledModsStore.Load(); if (enabled) { disabled.Remove(modId); var info = ModRegistry.Instance.GetMod(modId) as ModInfo; var dependencies = info?.Manifest?.RequiredDependencyIds; if (dependencies != null) { foreach (var dependency in dependencies) disabled.Remove(dependency); } } else { disabled.Add(modId); CollectDependents(modId, disabled); } DisabledModsStore.Save(disabled); } /// Category buckets for the sidebar, respecting every filter except the category set. public Dictionary CountCategories(ModFilterState filter) { var counts = new Dictionary(); foreach (var item in _items) { if (!filter.MatchesExceptCategory(item)) continue; foreach (var category in ModCategoryNames.Ordered) { if ((item.Category & category) == 0) continue; counts[category] = counts.GetValueOrDefault(category) + 1; } } return counts; } /// Status buckets for the sidebar, respecting every filter except the status set. public Dictionary CountStatuses(ModFilterState filter) { var counts = new Dictionary(); foreach (var item in _items) { if (!filter.MatchesExceptStatus(item)) continue; counts[item.Status] = counts.GetValueOrDefault(item.Status) + 1; } return counts; } /// /// Indexes catalog entries by mod id. Normally one entry per id, but a /// variant conflict (same id/version/title, different content across /// catalogs) produces several entries that the UI shows as separate mods. /// private Dictionary> BuildCatalogVariants() { var index = new Dictionary>(StringComparer.OrdinalIgnoreCase); var entries = CatalogService.CachedEntries; if (entries == null) return index; foreach (var entry in entries) { if (string.IsNullOrEmpty(entry?.Id)) continue; if (!index.TryGetValue(entry.Id, out var list)) { list = new List(); index[entry.Id] = list; } list.Add(entry); } return index; } private ModCardData BuildLocalCard(string id, ModInfo modInfo, bool disabled, CatalogExportEntry catalog) { var data = new ModCardData { Id = id, Title = modInfo.DisplayTitle, Author = modInfo.Author, Description = modInfo.LocalizedDescription, LocalVersion = modInfo.Version, Category = modInfo.Manifest?.Category ?? CategoryType.Other, LocalInfo = modInfo, Catalog = catalog, IsInstalled = true, IsDisabled = disabled, IsDeletable = ModUninstaller.CanUninstall(modInfo), IsProtected = string.Equals(id, SelfModId, StringComparison.OrdinalIgnoreCase), }; if (_installed.TryGetValue(id, out var installedVersion)) data.LocalVersion = installedVersion; ApplyDownloads(data, catalog); data.RecomputeStatus(); return data; } private ModCardData BuildRemoteCard(CatalogExportEntry catalog) { var locale = GetCatalogLocale(); var data = new ModCardData { Id = catalog.Id, Title = catalog.GetLocalizedName(locale), Author = catalog.Author, Description = catalog.GetLocalizedDescription(locale), Category = catalog.Category, Catalog = catalog, IsInstalled = false, }; if (_installed.TryGetValue(catalog.Id, out var installedVersion)) { data.IsInstalled = true; data.LocalVersion = installedVersion; } ApplyDownloads(data, catalog); data.RecomputeStatus(); return data; } private static void ApplyDownloads(ModCardData data, CatalogExportEntry catalog) { data.Downloads = catalog?.Downloads ?? new List(); data.SelectedDownload = data.Downloads.Count > 0 ? data.Downloads[0] : null; } private static string GetCatalogLocale() { var locale = TranslationServer.GetLocale(); return string.IsNullOrEmpty(locale) ? "en" : locale; } private static void CollectDependents(string modId, HashSet disabled) { var allMods = ModRegistry.Instance.AllDiscoveredMods; foreach (var pair in allMods) { if (disabled.Contains(pair.Key) || pair.Value is not ModInfo info) continue; var dependencies = info.Manifest?.RequiredDependencyIds; if (dependencies != null && dependencies.Contains(modId)) { disabled.Add(pair.Key); CollectDependents(pair.Key, disabled); } } } } }