Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/DLSSUpdater/Core/CatalogModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,16 @@ public sealed class CatalogBinary
public string DownloadUrl => string.IsNullOrWhiteSpace(MirrorUrl) ? Url : MirrorUrl;

/// <summary>Only entries with a hash can be installed ("no digest = no install").</summary>
public bool IsInstallable => Sha256 is { Length: 64 } && !string.IsNullOrWhiteSpace(DownloadUrl);
public bool IsInstallable => HasValidSha256 && HasPlainName && !string.IsNullOrWhiteSpace(DownloadUrl);

/// <summary>64 hex characters: the hash names cache folders, so nothing else may get in.</summary>
public bool HasValidSha256 => Sha256 is { Length: 64 } h && h.All(char.IsAsciiHexDigit);

/// <summary>The catalog is trusted for the bytes (it gives the hash), not for a path: a name with folders or invalid characters is refused.</summary>
public bool HasPlainName => Name.Length > 0 && Path.GetFileName(Name) == Name && Name is not ("." or "..") && Name.IndexOfAny(Path.GetInvalidFileNameChars()) < 0;

/// <summary>The name to build a path with; only the last segment of whatever the catalog says.</summary>
public string FileName => Path.GetFileName(Name);
}

public sealed class CatalogHistory
Expand Down
60 changes: 57 additions & 3 deletions src/DLSSUpdater/Core/ComponentInstall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public Task<ComponentUndo> InstallComponentAsync(GameInfo game, string targetDir
{
var m = InstallManifest.Read(targetDir) ?? new InstallManifest();
m.Contain(targetDir, game.Root);
var ctx = new Ctx(FileUtil.Normalize(game.Root), targetDir, m);
var ctx = new Ctx(FileUtil.Normalize(game.Root), targetDir, m, null, game);
try
{
var undo = ApplyPlan(ctx, plan);
Expand Down Expand Up @@ -51,7 +51,7 @@ public Task<ComponentUndo> RemoveComponentAsync(GameInfo game, string targetDir,
var m = InstallManifest.Read(targetDir) ?? throw new InvalidOperationException("No DLSS5 Updater install found here.");
m.Contain(targetDir, game.Root);
var c = m.Component(id) ?? throw new InvalidOperationException($"{ComponentRegistry.Find(id)?.Name ?? id} is not installed here.");
var ctx = new Ctx(FileUtil.Normalize(game.Root), targetDir, m);
var ctx = new Ctx(FileUtil.Normalize(game.Root), targetDir, m, null, game);
try
{
var undo = UndoComponent(ctx, c);
Expand Down Expand Up @@ -153,6 +153,22 @@ private static string ConfigEntryOf(Ctx ctx, string full)
return entry;
}

/// <summary>
/// A record can name a config file in AppData or Documents, which is where Unreal keeps its Engine.ini. A hand-edited or crafted
/// record must not make us edit another app's config there, so such a file is only changed when it lies in the Unreal config
/// folder worked out for this game. Files in the game or install folder are always ours.
/// </summary>
private static bool MayChangeConfig(Ctx ctx, string entry, string full)
{
if (!InstallManifest.KnownFolderTags.Any(t => entry.StartsWith(t, StringComparison.Ordinal))) return true;
if (ctx.Game is not { } game) return false;
var local = InstallManifest.KnownFolder("|localappdata|") ?? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var docs = InstallManifest.KnownFolder("|documents|") ?? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
return EngineIniLocator.Resolve(game, null, local, docs) is { } target
&& string.Equals(Path.GetDirectoryName(Path.GetFullPath(target.Path)), Path.GetDirectoryName(full), StringComparison.OrdinalIgnoreCase)
&& full.EndsWith(".ini", StringComparison.OrdinalIgnoreCase);
}

private static string ConfigFull(Ctx ctx, string entry)
{
var full = InstallManifest.Resolve(entry, ctx.Target, ctx.Root);
Expand Down Expand Up @@ -302,12 +318,24 @@ private static void UndoConfigEdits(Ctx ctx, InstalledComponent c, IReadOnlyList
foreach (var group in edits.GroupBy(e => e.File, StringComparer.OrdinalIgnoreCase))
{
var full = ConfigFull(ctx, group.Key);
if (!MayChangeConfig(ctx, group.Key, full))
{
Log.Warn($" {group.Key}: not the Unreal config folder of this game, left as it is");
foreach (var e in group) c.ConfigEdits.Remove(e);
Persist(ctx);
continue;
}
if (File.Exists(full))
{
var text = File.ReadAllText(full);
var ini = IniFile.Parse(text, keepFinalNewlineAsIs: true);
foreach (var e in group.Reverse())
{
if (ReShadeLayout.IsSearchPathKey(e.Section, e.Key))
{
UndoSearchPaths(ctx, c, ini, e);
continue;
}
// IniFile reads values trimmed.
if (!string.Equals(ini.Get(e.Section, e.Key), e.Value.Trim(), StringComparison.Ordinal))
{
Expand All @@ -327,6 +355,30 @@ private static void UndoConfigEdits(Ctx ctx, InstalledComponent c, IReadOnlyList
}
}

/// <summary>
/// Search path lists are shared: the add-ons and the shader packs both need the same shaders folder on them. Undo takes out only
/// the entries this component's edit added and no other installed component still needs, instead of putting the old list back.
/// </summary>
private static void UndoSearchPaths(Ctx ctx, InstalledComponent c, IniFile ini, ConfigEdit e)
{
var added = ReShadeLayout.AddedEntries(e.Value, e.Previous);
var mine = new List<string>();
foreach (var entry in added)
{
var needer = ctx.M.Components.FirstOrDefault(o => o != c
&& o.Extras.GetValueOrDefault(ReShadeLayout.OwnedEntryKey(e.Key)) is { } n && ReShadeLayout.AddedEntries(entry, n).Count == 0);
if (needer is null) mine.Add(entry);
// The entry stays, and the other component takes over undoing it, so it goes when that one is removed.
else if (needer.EditOf(e.File, e.Section, e.Key) is null)
needer.ConfigEdits.Add(new ConfigEdit { File = e.File, Section = e.Section, Key = e.Key, Previous = e.Previous, Value = e.Value, FileCreated = e.FileCreated, SectionCreated = e.SectionCreated });
}
var current = ini.Get(e.Section, e.Key);
if (current is null) return;
var left = ReShadeLayout.RemoveEntries(current, mine);
if (left is null && e.Previous is null) ini.Remove(e.Section, e.Key);
else ini.Set(e.Section, e.Key, left ?? e.Previous ?? "");
}

private static void SetReadOnly(Ctx ctx, InstalledComponent c, string entry)
{
var full = ConfigFull(ctx, entry);
Expand Down Expand Up @@ -354,7 +406,9 @@ private static void SetReadOnly(Ctx ctx, InstalledComponent c, string entry)
private static void UndoAttributes(Ctx ctx, InstalledComponent c, AttributeChange a, UndoReport report)
{
var full = ConfigFull(ctx, a.Path);
if (File.Exists(full))
if (!MayChangeConfig(ctx, a.Path, full))
Log.Warn($" {a.Path}: not the Unreal config folder of this game, left as it is");
else if (File.Exists(full))
{
var all = File.GetAttributes(full);
if ((all & AttributeChange.Managed) != a.After)
Expand Down
20 changes: 16 additions & 4 deletions src/DLSSUpdater/Core/DxvkService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,27 @@ public async Task<DxvkResult> ApplyAsync(DxvkRequest r, string? defaultPick, Act
return new DxvkResult(build, undone, notes);
}

/// <summary>Takes DXVK out, puts back the dlls it replaced and the driver profile values it changed.</summary>
public async Task<ComponentUndo> RemoveAsync(GameInfo game, string targetDir, NvidiaGame driver, Action saveSettings, CancellationToken ct)
/// <summary>
/// Takes DXVK out, puts back the dlls it replaced and the driver profile values it changed. When DXVK had moved ReShade to the Vulkan
/// layer, ReShade is installed again under the proxy name of the game's API (<paramref name="reShadeBack"/> = the options of its install),
/// so a Direct3D game that has a ReShade.ini still has the dll that loads it.
/// </summary>
public async Task<ComponentUndo> RemoveAsync(GameInfo game, string targetDir, NvidiaGame driver, InstallOptions? reShadeBack, Action saveSettings, CancellationToken ct)
{
targetDir = FileUtil.Normalize(targetDir);
var undo = await installer.RemoveComponentAsync(game, targetDir, ComponentRegistry.Dxvk, ct);
if (nvidia.Available) nvidia.RestoreLiliumPresentMethod(driver);
saveSettings();
if (InstallManifest.Read(targetDir) is { ReShadeVulkan: true } && !game.Effective.PrimaryApi.HasFlag(GraphicsApis.Vulkan))
Log.Info($"{game.Name}: ReShade still loads through its Vulkan layer. Install ReShade again to give it the {ReShadeNaming.ForApi(game.Effective.PrimaryApi)} name back.");
if (InstallManifest.Read(targetDir) is { ReShade: true, ReShadeVulkan: true } && ReShadeNaming.ForApi(game.Effective.PrimaryApi) is { } proxy)
{
if (reShadeBack is null)
Log.Info($"{game.Name}: ReShade still loads through its Vulkan layer. Install ReShade again to give it the {proxy} name back.");
else
{
await installer.InstallAsync(game, targetDir, reShadeBack with { ReShadeVulkan = false, Proxy = proxy }, null, ct);
Log.Info($"{game.Name}: ReShade is installed as {proxy} again.");
}
}
return undo;
}

Expand Down
10 changes: 5 additions & 5 deletions src/DLSSUpdater/Core/Installer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public sealed partial class Installer(ComponentStore store)
private static readonly string[] PackageDirs = ["OptiScaler", "Licenses"];
private static readonly string[] LogFiles = ["OptiScaler.log", "ReShade.log", "ReShade64.log"];

private sealed record Ctx(string Root, string Target, InstallManifest M, IReadOnlySet<string>? Keep = null)
private sealed record Ctx(string Root, string Target, InstallManifest M, IReadOnlySet<string>? Keep = null, GameInfo? Game = null)
{
public string Rel(string full) => InstallManifest.Entry(full, Target, Root);
/// <summary>Entries live in the game or in the install folder, which the user may have picked outside the game.</summary>
Expand Down Expand Up @@ -232,7 +232,7 @@ private async Task InstallCoreAsync(GameInfo game, string targetDir, InstallOpti
// An unreadable or unsafe record stops the install instead of starting over and burying the originals.
var m = InstallManifest.Read(targetDir) ?? new InstallManifest();
m.Contain(targetDir, game.Root);
var ctx = new Ctx(FileUtil.Normalize(game.Root), targetDir, m, KeptAddonFiles(o));
var ctx = new Ctx(FileUtil.Normalize(game.Root), targetDir, m, KeptAddonFiles(o), game);
if (o.Uplift)
foreach (var full in ComponentStore.UpliftFiles.Select(f => Path.Combine(targetDir, f)).Append(UpliftConfig.NrPath(targetDir)))
if (FileUtil.LinkBelow(FileUtil.IsUnder(full, ctx.Root) ? ctx.Root : ctx.Target, full) is { } link)
Expand Down Expand Up @@ -1298,7 +1298,7 @@ public Task SwitchDlssNrAsync(GameInfo game, string targetDir, string? id, Cance
var m = InstallManifest.Read(targetDir) ?? throw new InvalidOperationException("Install NR in this game first.");
m.Contain(targetDir, game.Root);
if (!m.DlssNr) throw new InvalidOperationException("Install NR in this game first.");
var ctx = new Ctx(FileUtil.Normalize(game.Root), targetDir, m);
var ctx = new Ctx(FileUtil.Normalize(game.Root), targetDir, m, null, game);
var source = store.VerifiedDlssNrPath(id ?? store.SelectedDlssNr());
var target = m.Uplift ? UpliftConfig.NrPath(targetDir) : Path.Combine(targetDir, ComponentStore.DlssNrFile);
var previous = File.Exists(target) ? store.RetainDlssNr(target) : null;
Expand All @@ -1325,7 +1325,7 @@ public Task UninstallAsync(GameInfo game, string targetDir, CancellationToken ct
EnsureNotRunning(game.Root);
var m = InstallManifest.Read(targetDir) ?? throw new InvalidOperationException("No DLSS5 Updater install found here.");
m.Contain(targetDir, game.Root);
var ctx = new Ctx(FileUtil.Normalize(game.Root), targetDir, m);
var ctx = new Ctx(FileUtil.Normalize(game.Root), targetDir, m, null, game);
try
{
// Every step is recorded as soon as it is done, so a retry after a failure part-way (a locked file)
Expand Down Expand Up @@ -1383,7 +1383,7 @@ public Task RestoreDlssAsync(GameInfo game, string targetDir, CancellationToken
EnsureNotRunning(game.Root);
var m = InstallManifest.Read(targetDir) ?? throw new InvalidOperationException("No DLSS5 Updater install found here.");
m.Contain(targetDir, game.Root);
var ctx = new Ctx(FileUtil.Normalize(game.Root), targetDir, m);
var ctx = new Ctx(FileUtil.Normalize(game.Root), targetDir, m, null, game);
try
{
foreach (var b in m.Backups.Where(b => b.Kind is "dlss" or "streamline").ToList())
Expand Down
2 changes: 1 addition & 1 deletion src/DLSSUpdater/Core/LumaInstall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ public async Task<LumaInstallResult> InstallAsync(GameInfo game, string targetDi
return ZipStage.Extract(ComponentStore.LumaPathOf(build), pkg, LumaFiles.Include);
}, ct);

await host.EnsureAsync(game, targetDir, reshade ?? new HdrReShade(), new("Luma", "Luma needs ReShade 6 or newer with add-on support."), progress, ct);
await host.EnsureAsync(game, targetDir, reshade ?? new HdrReShade(), new("Luma", "Luma needs ReShade 6.6 or newer (its ReadMe says so). Update ReShade for this game first.", new Version(6, 6)), progress, ct);

var existing = InstallManifest.Read(targetDir)?.Component(ComponentRegistry.Luma);
var plan = LumaPlanner.Build(game, targetDir, choice, build, pkg, names, hdr, engineIniHint, existing);
Expand Down
11 changes: 9 additions & 2 deletions src/DLSSUpdater/Core/ReShadeAddonHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
namespace DLSSUpdater.Core;

/// <summary>The words an add-on component uses when ReShade isn't ready for it: what it is called in a sentence, and the version complaint.</summary>
public sealed record AddonPurpose(string Name, string NeedsReShade6);
/// <param name="NeedsReShade6">The message when the installed ReShade is older than <paramref name="Minimum"/>.</param>
/// <param name="Minimum">The oldest ReShade the add-on works with; null = 6.0, the first with full add-on support.</param>
public sealed record AddonPurpose(string Name, string NeedsReShade6, Version? Minimum = null)
{
public static readonly Version AddonSupport = new(6, 0);

public bool Allows(Version? reshade) => reshade is null || reshade >= (Minimum ?? AddonSupport);
}

/// <summary>
/// Makes sure a game has ReShade with add-on support before an add-on component goes in (the RenoDX HDR mod, the frame limiters):
Expand Down Expand Up @@ -45,7 +52,7 @@ private void CheckVersion(string targetDir, InstallManifest? m, AddonPurpose pur
var version = m.ReShadeVulkan
? store.ReShadeBuildFor(m.ReShadeId)?.ParsedVersion
: FileUtil.ReadVersion(Path.Combine(targetDir, m is { Mode: InstallMode.ReShadeOnly, Proxy: { } p } ? p : ComponentStore.ReShadeFile));
if (version is { Major: < 6 })
if (!purpose.Allows(version))
throw new InvalidOperationException(purpose.NeedsReShade6);
}
}
5 changes: 4 additions & 1 deletion src/DLSSUpdater/Core/ReShadeExtras.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ public async Task<IReadOnlyList<ExtrasResult>> UpdateAsync(GameInfo game, string
else
{
Directory.CreateDirectory(stage);
File.Copy(part, Path.Combine(stage, file.Name), true);
File.Copy(part, Path.Combine(stage, file.FileName), true);
}
Directory.Move(stage, dir);
return (dir, null);
Expand Down Expand Up @@ -259,6 +259,7 @@ public async Task<ExtrasResult> ApplyAddonsAsync(GameInfo game, string target, I
else edits.AddRange(ReShadeLayout.SearchPathEdits(target, folder, effects: true, textures: false));
}
var extras = WantedAddons(game);
if (plan.HasEffects && !UserSets(game.Id, "EffectSearchPaths")) extras[ReShadeLayout.OwnedEntryKey("EffectSearchPaths")] = ReShadeLayout.SearchEntry(folder, "Shaders");
foreach (var id in plan.Placed.Keys) extras["placed:" + id] = plan.Placed[id];
var undo = await _installer.InstallComponentAsync(game, target,
new ComponentPlan(ComponentRegistry.ReShadeAddons) { Version = Count(plan.Placed.Count, "add-on"), Files = plan.Files, ConfigEdits = edits, Extras = extras }, ct);
Expand Down Expand Up @@ -315,6 +316,8 @@ bool Wanted(string name) => name.StartsWith(layout.ShadersDir, StringComparison.
}
var extras = WantedPacks(game);
extras["folder"] = folder.Name;
if (!UserSets(game.Id, "EffectSearchPaths")) extras[ReShadeLayout.OwnedEntryKey("EffectSearchPaths")] = ReShadeLayout.SearchEntry(folder, "Shaders");
if (hasTextures && !UserSets(game.Id, "TextureSearchPaths")) extras[ReShadeLayout.OwnedEntryKey("TextureSearchPaths")] = ReShadeLayout.SearchEntry(folder, "Textures");
foreach (var id in plan.Placed.Keys) extras["placed:" + id] = plan.Placed[id];
var undo = await _installer.InstallComponentAsync(game, target,
new ComponentPlan(ComponentRegistry.ReShadeShaders) { Version = Count(plan.Placed.Count, "pack"), Files = files, ConfigEdits = edits, Extras = extras }, ct);
Expand Down
24 changes: 24 additions & 0 deletions src/DLSSUpdater/Core/ReShadeLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,30 @@ public static string MergeSearchPath(string? existing, string entry)
return items.Any(i => SameFolder(i, entry)) ? string.Join(",", items) : string.Join(",", [.. items, entry]);
}

/// <summary>The Extras key under which a component records the search path entry of <paramref name="iniKey"/> it needs, even when another component already added it.</summary>
public static string OwnedEntryKey(string iniKey) => "searchpath:" + iniKey;

public static bool IsSearchPathKey(string section, string key) =>
section.Equals("GENERAL", StringComparison.OrdinalIgnoreCase) && key is "EffectSearchPaths" or "TextureSearchPaths";

/// <summary>The entries <paramref name="value"/> holds that <paramref name="previous"/> did not: what one component's edit added.</summary>
public static IReadOnlyList<string> AddedEntries(string value, string? previous)
{
var before = Split(previous);
return Split(value).Where(i => !before.Any(b => SameFolder(b, i))).ToList();
}

/// <summary><paramref name="current"/> without the entries naming the same folders as <paramref name="remove"/>; null when nothing is left.</summary>
public static string? RemoveEntries(string? current, IEnumerable<string> remove)
{
var gone = remove.ToList();
var left = Split(current).Where(i => !gone.Any(g => SameFolder(g, i))).ToList();
return left.Count == 0 ? null : string.Join(",", left);
}

private static List<string> Split(string? list) =>
(list ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();

private static string Canon(string path) =>
path.Replace('/', '\\').Trim().TrimEnd('*').TrimEnd('\\').TrimStart('.').TrimStart('\\').ToLowerInvariant();

Expand Down
2 changes: 1 addition & 1 deletion src/DLSSUpdater/Core/RenoDxSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ private AddonBuild ImportRenoDx(string source, bool downloaded)
try
{
Directory.CreateDirectory(work);
var zip = Path.Combine(work, newest.File.Name);
var zip = Path.Combine(work, newest.File.FileName);
var result = await downloader.DownloadAsync(newest.File, zip, progress, ct);
if (!result.Ok)
{
Expand Down
Loading