init mod
This commit is contained in:
commit
e8e0c5159d
46 files changed
+7923
No files matched your search
@@ -0,0 +1,32 @@
|
||||
namespace Mods.ModManager
|
||||
{
|
||||
/// <summary>
|
||||
/// User-triggered mod operation. The screen resolves a tile's primary action
|
||||
/// via <see cref="ModActions.GetPrimary"/> and executes the kind here; the
|
||||
/// detail view maps its buttons to the same kinds. Every kind is persisted to
|
||||
/// disk immediately but only takes effect after a restart.
|
||||
/// </summary>
|
||||
public enum ModActionKind
|
||||
{
|
||||
/// <summary>Download and install the selected catalog version.</summary>
|
||||
Install,
|
||||
|
||||
/// <summary>Install a newer catalog version over the local copy.</summary>
|
||||
Update,
|
||||
|
||||
/// <summary>Install an older catalog version over the local copy.</summary>
|
||||
Downgrade,
|
||||
|
||||
/// <summary>Reinstall the same version to replace locally modified files.</summary>
|
||||
Reinstall,
|
||||
|
||||
/// <summary>Delete the installed mod (user mods root only), then clean per-mod state.</summary>
|
||||
Delete,
|
||||
|
||||
/// <summary>Allow the mod to load on next start.</summary>
|
||||
Enable,
|
||||
|
||||
/// <summary>Prevent the mod from loading on next start.</summary>
|
||||
Disable,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace Mods.ModManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Business rules deciding which operation a mod tile offers. The tile shows
|
||||
/// a single contextual button; its meaning follows the state machine:
|
||||
/// catalog-only → Install, newer remote → Update, older remote → Downgrade,
|
||||
/// same version but edited locally → Reinstall, otherwise → Delete for
|
||||
/// deletable local mods and nothing for protected/shipped ones.
|
||||
/// </summary>
|
||||
public static class ModActions
|
||||
{
|
||||
/// <summary>Primary action for a tile/detail page, or null when there is nothing to do.</summary>
|
||||
public static ModActionKind? GetPrimary(ModCardData data)
|
||||
{
|
||||
if (data == null)
|
||||
return null;
|
||||
|
||||
if (!data.IsInstalled)
|
||||
return data.HasRemote ? ModActionKind.Install : null;
|
||||
|
||||
if (data.HasRemote)
|
||||
{
|
||||
switch (data.Status)
|
||||
{
|
||||
case ModItemStatus.UpdateAvailable:
|
||||
return ModActionKind.Update;
|
||||
case ModItemStatus.DowngradeAvailable:
|
||||
return ModActionKind.Downgrade;
|
||||
case ModItemStatus.ModifiedLocally:
|
||||
return ModActionKind.Reinstall;
|
||||
}
|
||||
}
|
||||
|
||||
return data.IsDeletable ? ModActionKind.Delete : null;
|
||||
}
|
||||
|
||||
/// <summary>True for actions that download and install files.</summary>
|
||||
public static bool IsInstall(ModActionKind kind)
|
||||
=> kind == ModActionKind.Install
|
||||
|| kind == ModActionKind.Update
|
||||
|| kind == ModActionKind.Downgrade
|
||||
|| kind == ModActionKind.Reinstall;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Utils;
|
||||
|
||||
namespace Mods.ModManager
|
||||
{
|
||||
/// <summary>
|
||||
/// One row of the mod manager grid: everything the tile and the detail page
|
||||
/// need to render a mod, regardless of whether it exists locally, in the
|
||||
/// online catalog, or both. The manager builds these in
|
||||
/// <see cref="ModManagerModel.Reload"/> by merging
|
||||
/// <c>ModRegistry.AllDiscoveredMods</c> with <c>CatalogService.CachedEntries</c>.
|
||||
/// <para/>
|
||||
/// <see cref="Status"/> is derived (not stored by the registry) from the local
|
||||
/// version, the selected catalog download and the local install hash — call
|
||||
/// <see cref="RecomputeStatus"/> after changing <see cref="SelectedDownload"/>,
|
||||
/// <see cref="IsDisabled"/> or the install state.
|
||||
/// </summary>
|
||||
public class ModCardData
|
||||
{
|
||||
/// <summary>Stable mod id shared by the local mod and the catalog entry.</summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>Localized display title (local manifest name or catalog locale name).</summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>Author string from whichever source is available.</summary>
|
||||
public string Author { get; set; }
|
||||
|
||||
/// <summary>Localized description.</summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>Installed version string, or null for catalog-only mods.</summary>
|
||||
public string LocalVersion { get; set; }
|
||||
|
||||
/// <summary>Manifest category (local) or catalog category (remote), flags combined.</summary>
|
||||
public CategoryType Category { get; set; } = CategoryType.Other;
|
||||
|
||||
/// <summary>Loaded local mod, or null when only the catalog knows this id.</summary>
|
||||
public ModInfo LocalInfo { get; set; }
|
||||
|
||||
/// <summary>Merged catalog entry, or null for local-only (development) mods.</summary>
|
||||
public CatalogExportEntry Catalog { get; set; }
|
||||
|
||||
/// <summary>True when a local copy exists (including an install done in this session).</summary>
|
||||
public bool IsInstalled { get; set; }
|
||||
|
||||
/// <summary>True when the local copy is listed in <c>disabled_mods.json</c>.</summary>
|
||||
public bool IsDisabled { get; set; }
|
||||
|
||||
/// <summary>True when <see cref="ModUninstaller"/> may delete the local copy.</summary>
|
||||
public bool IsDeletable { get; set; }
|
||||
|
||||
/// <summary>True for the mod manager itself, which cannot be disabled or deleted.</summary>
|
||||
public bool IsProtected { get; set; }
|
||||
|
||||
/// <summary>Derived tile state; see <see cref="RecomputeStatus"/>.</summary>
|
||||
public ModItemStatus Status { get; set; }
|
||||
|
||||
/// <summary>Catalog downloads, newest first (as merged by the catalog service).</summary>
|
||||
public List<CatalogEntryDownload> Downloads { get; set; } = new();
|
||||
|
||||
/// <summary>Version chosen in the tile/detail combo box; defaults to the newest download.</summary>
|
||||
public CatalogEntryDownload SelectedDownload { get; set; }
|
||||
|
||||
/// <summary>True when an online catalog entry exists for this mod.</summary>
|
||||
public bool HasRemote => Catalog != null;
|
||||
|
||||
/// <summary>
|
||||
/// True when several catalogs publish this id with the same version and
|
||||
/// title but different content — the tile and detail page warn about it.
|
||||
/// </summary>
|
||||
public bool IsConflict => Catalog?.VariantConflict == true;
|
||||
|
||||
/// <summary>
|
||||
/// How many <see cref="ModCollectionKind.Game"/> collections reference
|
||||
/// this mod (directly or through includes). Computed by the manager
|
||||
/// screen via <see cref="ModCollectionGallery.FindGamesUsingMod"/> on
|
||||
/// every grid refresh; zero means the mod belongs to no playable game.
|
||||
/// </summary>
|
||||
public int CollectionsCount { get; set; }
|
||||
|
||||
/// <summary>Catalog URLs that published this variant, or null for local-only mods.</summary>
|
||||
public IReadOnlyList<string> SourceUrls => Catalog?.SourceUrls;
|
||||
|
||||
/// <summary>First catalog URL; used to install exactly this variant and to match cards after reload.</summary>
|
||||
public string PrimarySource => ModCatalogSource.FirstUrl(SourceUrls);
|
||||
|
||||
/// <summary>
|
||||
/// Identity of a card across model reloads: the mod id plus the catalog
|
||||
/// variant it represents. Needed because conflicting variants share an id.
|
||||
/// </summary>
|
||||
public string MatchKey => $"{Id}|{PrimarySource ?? string.Empty}";
|
||||
|
||||
/// <summary>True when the tile should show the enable/disable switch.</summary>
|
||||
public bool CanToggleEnabled => IsInstalled && !IsProtected;
|
||||
|
||||
/// <summary>Version to display next to the title: installed version or newest catalog version.</summary>
|
||||
public string DisplayVersion => !string.IsNullOrEmpty(LocalVersion)
|
||||
? LocalVersion
|
||||
: SelectedDownload?.Version ?? Catalog?.Version;
|
||||
|
||||
/// <summary>
|
||||
/// Derives <see cref="Status"/> from the local and selected remote versions.
|
||||
/// Priority: not installed → disabled → version comparison →
|
||||
/// hash comparison. A missing catalog entry always yields
|
||||
/// <see cref="ModItemStatus.Installed"/> for a local copy.
|
||||
/// </summary>
|
||||
public void RecomputeStatus()
|
||||
{
|
||||
if (!IsInstalled)
|
||||
{
|
||||
Status = ModItemStatus.NotInstalled;
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsDisabled)
|
||||
{
|
||||
Status = ModItemStatus.Disabled;
|
||||
return;
|
||||
}
|
||||
|
||||
var remoteVersion = SelectedDownload?.Version ?? Catalog?.Version;
|
||||
if (string.IsNullOrEmpty(remoteVersion) || string.IsNullOrEmpty(LocalVersion))
|
||||
{
|
||||
Status = ModItemStatus.Installed;
|
||||
return;
|
||||
}
|
||||
|
||||
var comparison = VersionHelper.Compare(remoteVersion, LocalVersion);
|
||||
if (comparison > 0)
|
||||
{
|
||||
Status = ModItemStatus.UpdateAvailable;
|
||||
return;
|
||||
}
|
||||
|
||||
if (comparison < 0)
|
||||
{
|
||||
Status = ModItemStatus.DowngradeAvailable;
|
||||
return;
|
||||
}
|
||||
|
||||
var remoteHash = SelectedDownload?.InstallSha256;
|
||||
if (!string.IsNullOrEmpty(remoteHash)
|
||||
&& !string.IsNullOrEmpty(LocalInfo?.InstallSha256)
|
||||
&& !string.Equals(LocalInfo.InstallSha256, remoteHash, System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Status = ModItemStatus.ModifiedLocally;
|
||||
return;
|
||||
}
|
||||
|
||||
Status = ModItemStatus.Installed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Mods.ModManager
|
||||
{
|
||||
/// <summary>
|
||||
/// One row of the "+ мод" picker: the mods known to the local registry and
|
||||
/// the online catalog cache, merged by id. The picker excludes ids that are
|
||||
/// already resolved in the scheme, so this entity only ever describes mods
|
||||
/// the player can still add; <see cref="ModChoice.Id"/> becomes a new string
|
||||
/// in the target mechanic's <c>mods</c> array.
|
||||
/// </summary>
|
||||
public class ModChoice
|
||||
{
|
||||
/// <summary>Canonical mod id written into the collection document.</summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>Display title (localized catalog name or manifest name).</summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>Localized description shown under the title.</summary>
|
||||
public string Description { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Mods;
|
||||
|
||||
namespace Mods.ModManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Live filter state of the manager. Empty status/category sets mean "all",
|
||||
/// which matches Nexus-style sidebars where checkboxes only ever narrow the
|
||||
/// list. Search matches title, id and author case-insensitively. The state
|
||||
/// also owns the sort mode and the optional grouping toggle.
|
||||
/// </summary>
|
||||
public class ModFilterState
|
||||
{
|
||||
/// <summary>Free-text query matched against title, id and author.</summary>
|
||||
public string Search { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Selected statuses; empty means no status restriction.</summary>
|
||||
public HashSet<ModItemStatus> Statuses { get; } = new();
|
||||
|
||||
/// <summary>Selected categories; empty means no category restriction.</summary>
|
||||
public HashSet<CategoryType> Categories { get; } = new();
|
||||
|
||||
/// <summary>Grid ordering mode.</summary>
|
||||
public ModSortMode Sort { get; set; } = ModSortMode.Name;
|
||||
|
||||
/// <summary>When true the grid renders category headers between groups.</summary>
|
||||
public bool GroupByCategory { get; set; }
|
||||
|
||||
/// <summary>Full match: search + status + category.</summary>
|
||||
public bool Matches(ModCardData item)
|
||||
=> MatchesSearch(item) && MatchesStatuses(item) && MatchesCategories(item);
|
||||
|
||||
/// <summary>Match ignoring the category set — used to compute category counts.</summary>
|
||||
public bool MatchesExceptCategory(ModCardData item)
|
||||
=> MatchesSearch(item) && MatchesStatuses(item);
|
||||
|
||||
/// <summary>Match ignoring the status set — used to compute status counts.</summary>
|
||||
public bool MatchesExceptStatus(ModCardData item)
|
||||
=> MatchesSearch(item) && MatchesCategories(item);
|
||||
|
||||
/// <summary>Returns matching items in the configured order.</summary>
|
||||
public List<ModCardData> Apply(IEnumerable<ModCardData> source)
|
||||
{
|
||||
var result = new List<ModCardData>();
|
||||
foreach (var item in source)
|
||||
{
|
||||
if (Matches(item))
|
||||
result.Add(item);
|
||||
}
|
||||
|
||||
result.Sort(Compare);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Clears search, statuses and categories; keeps sort and grouping.</summary>
|
||||
public void Reset()
|
||||
{
|
||||
Search = string.Empty;
|
||||
Statuses.Clear();
|
||||
Categories.Clear();
|
||||
}
|
||||
|
||||
private int Compare(ModCardData a, ModCardData b)
|
||||
{
|
||||
switch (Sort)
|
||||
{
|
||||
case ModSortMode.Author:
|
||||
var byAuthor = string.Compare(a.Author, b.Author, StringComparison.OrdinalIgnoreCase);
|
||||
return byAuthor != 0 ? byAuthor : CompareTitle(a, b);
|
||||
|
||||
case ModSortMode.Status:
|
||||
var byStatus = ModStatusNames.SortRank(a.Status).CompareTo(ModStatusNames.SortRank(b.Status));
|
||||
return byStatus != 0 ? byStatus : CompareTitle(a, b);
|
||||
|
||||
default:
|
||||
return CompareTitle(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
private static int CompareTitle(ModCardData a, ModCardData b)
|
||||
=> string.Compare(a.Title, b.Title, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private bool MatchesSearch(ModCardData item)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Search))
|
||||
return true;
|
||||
|
||||
var query = Search.Trim();
|
||||
return Contains(item.Title, query) || Contains(item.Id, query) || Contains(item.Author, query);
|
||||
}
|
||||
|
||||
private bool MatchesStatuses(ModCardData item)
|
||||
=> Statuses.Count == 0 || Statuses.Contains(item.Status);
|
||||
|
||||
private bool MatchesCategories(ModCardData item)
|
||||
=> Categories.Count == 0 || (item.Category & AsFlags()) != 0;
|
||||
|
||||
private CategoryType AsFlags()
|
||||
{
|
||||
var flags = (CategoryType)0;
|
||||
foreach (var category in Categories)
|
||||
flags |= category;
|
||||
return flags;
|
||||
}
|
||||
|
||||
private static bool Contains(string value, string query)
|
||||
=> !string.IsNullOrEmpty(value) && value.Contains(query, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace Mods.ModManager
|
||||
{
|
||||
/// <summary>
|
||||
/// UI-level state of a mod tile. Combines the local install state with the
|
||||
/// online catalog comparison so a single value drives the tile badge and the
|
||||
/// contextual action button. <see cref="ModifiedLocally"/> means the local
|
||||
/// content hash differs from the catalog entry of the same version — the
|
||||
/// player edited the mod and overwriting would lose their changes.
|
||||
/// </summary>
|
||||
public enum ModItemStatus
|
||||
{
|
||||
/// <summary>No local copy exists; only the catalog entry is shown.</summary>
|
||||
NotInstalled,
|
||||
|
||||
/// <summary>Installed and up-to-date with the selected catalog version (or no catalog entry).</summary>
|
||||
Installed,
|
||||
|
||||
/// <summary>The catalog has a newer version than the installed one.</summary>
|
||||
UpdateAvailable,
|
||||
|
||||
/// <summary>The catalog's selected version is older than the installed one.</summary>
|
||||
DowngradeAvailable,
|
||||
|
||||
/// <summary>Same version as the catalog, but the local content hash differs.</summary>
|
||||
ModifiedLocally,
|
||||
|
||||
/// <summary>Installed but disabled by the player (<c>disabled_mods.json</c>).</summary>
|
||||
Disabled,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Mods;
|
||||
using Godot;
|
||||
|
||||
namespace Mods.ModManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Merges everything known about mods into the manager's grid model: local
|
||||
/// mods discovered by <see cref="ModRegistry"/> (with enable state and
|
||||
/// deletability) and online catalog entries from <see cref="CatalogService"/>.
|
||||
/// A mod present in both sources becomes one combined card; catalog-only mods
|
||||
/// appear as installable tiles.
|
||||
/// <para/>
|
||||
/// 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
|
||||
/// <see cref="MarkInstalled"/> / <see cref="MarkRemoved"/> adjust the view and
|
||||
/// the restart banner tells the player when the change becomes real.
|
||||
/// </summary>
|
||||
public class ModManagerModel
|
||||
{
|
||||
/// <summary>Id of the mod that renders this UI; it can never be disabled or removed.</summary>
|
||||
public const string SelfModId = "mod_manager";
|
||||
|
||||
private readonly List<ModCardData> _items = new();
|
||||
private readonly HashSet<string> _removed = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, string> _installed = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>All cards, unsorted and unfiltered; rebuild with <see cref="Reload"/>.</summary>
|
||||
public IReadOnlyList<ModCardData> Items => _items;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void Reload()
|
||||
{
|
||||
_items.Clear();
|
||||
|
||||
var disabled = DisabledModsStore.Load();
|
||||
var variantsById = BuildCatalogVariants();
|
||||
var localIds = new HashSet<string>(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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Remembers that a mod was deleted this session so it stays hidden until restart.</summary>
|
||||
public void MarkRemoved(string modId)
|
||||
{
|
||||
_removed.Add(modId);
|
||||
_installed.Remove(modId);
|
||||
}
|
||||
|
||||
/// <summary>Remembers a successful install so the card shows as installed before restart.</summary>
|
||||
public void MarkInstalled(string modId, string version)
|
||||
{
|
||||
_removed.Remove(modId);
|
||||
_installed[modId] = version ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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?.DependencyIds;
|
||||
if (dependencies != null)
|
||||
{
|
||||
foreach (var dependency in dependencies)
|
||||
disabled.Remove(dependency);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
disabled.Add(modId);
|
||||
CollectDependents(modId, disabled);
|
||||
}
|
||||
|
||||
DisabledModsStore.Save(disabled);
|
||||
}
|
||||
|
||||
/// <summary>Category buckets for the sidebar, respecting every filter except the category set.</summary>
|
||||
public Dictionary<CategoryType, int> CountCategories(ModFilterState filter)
|
||||
{
|
||||
var counts = new Dictionary<CategoryType, int>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Status buckets for the sidebar, respecting every filter except the status set.</summary>
|
||||
public Dictionary<ModItemStatus, int> CountStatuses(ModFilterState filter)
|
||||
{
|
||||
var counts = new Dictionary<ModItemStatus, int>();
|
||||
foreach (var item in _items)
|
||||
{
|
||||
if (!filter.MatchesExceptStatus(item))
|
||||
continue;
|
||||
counts[item.Status] = counts.GetValueOrDefault(item.Status) + 1;
|
||||
}
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private Dictionary<string, List<CatalogExportEntry>> BuildCatalogVariants()
|
||||
{
|
||||
var index = new Dictionary<string, List<CatalogExportEntry>>(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<CatalogExportEntry>();
|
||||
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<CatalogEntryDownload>();
|
||||
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<string> 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?.DependencyIds;
|
||||
if (dependencies != null && dependencies.Contains(modId))
|
||||
{
|
||||
disabled.Add(pair.Key);
|
||||
CollectDependents(pair.Key, disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Mods;
|
||||
|
||||
namespace Mods.ModManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Read-only cross-mod lookups used by the detail page: direct dependencies,
|
||||
/// reverse dependencies (which mods depend on this one) and declared
|
||||
/// conflicts. Prefers local manifest data and falls back to the catalog entry
|
||||
/// for mods that are not installed.
|
||||
/// </summary>
|
||||
public static class ModQueries
|
||||
{
|
||||
/// <summary>Dependency strings (<c>core>=1</c> format) of a mod, whatever source knows it.</summary>
|
||||
public static List<string> GetDependencies(ModCardData data)
|
||||
{
|
||||
var manifestDeps = data?.LocalInfo?.Manifest?.DependencyIds;
|
||||
if (manifestDeps != null && manifestDeps.Count > 0)
|
||||
return manifestDeps;
|
||||
|
||||
return data?.Catalog?.Depends ?? new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>Ids of installed mods whose manifests depend on <paramref name="modId"/>.</summary>
|
||||
public static List<string> GetDependents(string modId)
|
||||
{
|
||||
var result = new List<string>();
|
||||
if (string.IsNullOrEmpty(modId))
|
||||
return result;
|
||||
|
||||
var allMods = ModRegistry.Instance.AllDiscoveredMods;
|
||||
if (allMods == null)
|
||||
return result;
|
||||
|
||||
foreach (var pair in allMods)
|
||||
{
|
||||
if (pair.Key == modId || pair.Value is not ModInfo info)
|
||||
continue;
|
||||
|
||||
var deps = info.Manifest?.DependencyIds;
|
||||
if (deps != null && deps.Contains(modId))
|
||||
result.Add(pair.Key);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Declared mutually exclusive mod ids (manifest first, catalog fallback).</summary>
|
||||
public static List<string> GetConflicts(ModCardData data)
|
||||
{
|
||||
var manifestConflicts = data?.LocalInfo?.Manifest?.ConflictsWith;
|
||||
if (manifestConflicts != null && manifestConflicts.Count > 0)
|
||||
return manifestConflicts;
|
||||
|
||||
return data?.Catalog?.ConflictsWith ?? new List<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Mods.ModManager
|
||||
{
|
||||
/// <summary>Ordering mode of the mod grid; grouping by category is a separate toggle.</summary>
|
||||
public enum ModSortMode
|
||||
{
|
||||
/// <summary>Alphabetical by display title.</summary>
|
||||
Name,
|
||||
|
||||
/// <summary>Alphabetical by author, then title.</summary>
|
||||
Author,
|
||||
|
||||
/// <summary>By install/update state (installed, updates, catalog-only).</summary>
|
||||
Status,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user