namespace MrGameEng.Mods; /// A file contributed by a mod, addressed by its content-relative path. /// Path relative to the content folder, forward slashes. /// Absolute path of the winning file on disk. /// The mod that contributed the file. public readonly record struct ModFile(string RelativePath, string FullPath, Mod Mod); /// /// Merged view of one content folder (e.g. Textures) across the active mods in load /// order: a later mod shipping a file under the same relative path overrides the earlier /// one. Paths use forward slashes and match case-insensitively (Windows-friendly content). /// public sealed class ModContentTree { private readonly Dictionary _files; private ModContentTree(Dictionary files, IReadOnlyList ordered) { _files = files; Files = ordered; } /// Winning files, sorted by relative path — deterministic for identical mod sets. public IReadOnlyList Files { get; } /// Returns the winning file for . public bool TryGet(string relativePath, out ModFile file) => _files.TryGetValue(Normalize(relativePath), out file); /// /// Builds the merged tree of over /// (in load order). With only matching files are included /// (e.g. ".png"); without them, every file. /// public static ModContentTree Build(IReadOnlyList mods, string contentFolder, params string[] extensions) { var files = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var mod in mods) { var root = mod.ContentPath(contentFolder); if (!Directory.Exists(root)) { continue; } foreach (var fullPath in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)) { if (extensions.Length > 0 && !extensions.Contains(Path.GetExtension(fullPath), StringComparer.OrdinalIgnoreCase)) { continue; } var relative = Normalize(Path.GetRelativePath(root, fullPath)); files[relative] = new ModFile(relative, fullPath, mod); // поздний мод побеждает } } var ordered = files.Values.OrderBy(f => f.RelativePath, StringComparer.Ordinal).ToList(); return new ModContentTree(files, ordered); } private static string Normalize(string path) => path.Replace('\\', '/'); }