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
5 changes: 1 addition & 4 deletions src/DLSSUpdater/Core/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,6 @@ public static bool IsKnownProxy(string? name) =>
public bool InstallRenoDx { get; set; }
public bool InstallUplift { get; set; }
public bool InstallLecram { get; set; }
/// <summary>Lets ShortFuse's and Lecram's add-ons be on together (NR then runs twice); off keeps one NR add-on per game (<see cref="NrAddons"/>).</summary>
public bool AllowSeveralNrAddons { get; set; }
/// <summary>The OptiScaler build's repo. New installs start on F5; a saved choice is always kept.</summary>
public string OptiSource { get; set; } = OptiSources.F5.Repo;
public string? PreviousDlssNrId { get; set; }
Expand Down Expand Up @@ -229,9 +227,8 @@ public static AppSettings Load()
if (sourceMigrated) s.OptiSource = ComponentStore.OptiRepo;
s.Presets.RemoveAll(p => p.Name.Equals(ConfigProfile.RecommendedName, StringComparison.OrdinalIgnoreCase));
var renoDxMigrated = RenoDxIni.Migrate(s);
var addonsMigrated = NrAddons.Migrate(s);
var lecramMigrated = s.MigrateLecramNewest();
if (s._legacyTokenRead || sourceMigrated || renoDxMigrated || addonsMigrated || lecramMigrated) s.Save();
if (s._legacyTokenRead || sourceMigrated || renoDxMigrated || lecramMigrated) s.Save();
return s;
}
}
Expand Down
1 change: 1 addition & 0 deletions src/DLSSUpdater/Core/Demo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ public static async Task InstallAsync(ComponentStore store, GameInfo game, strin
m.Uplift = o.Uplift;
m.Dfc = o.Dfc;
m.AntiCheatConfirmed = o.AntiCheatConfirmed;
m.ConfirmedConflict = o.ConfirmedConflict;
m.OptiTag = o.Opti ? o.OptiTag ?? store.Opti?.Tag : null;
m.MfgTag = o.Mfg ? o.MfgTag ?? store.Mfg?.Tag : null;
m.UpliftTag = o.Uplift ? o.UpliftTag ?? store.Uplift?.Tag : null;
Expand Down
2 changes: 2 additions & 0 deletions src/DLSSUpdater/Core/InstallManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ public sealed class InstallManifest
public Dictionary<string, string> OptiIni { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public Dictionary<string, string> ReShadeIni { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public bool AntiCheatConfirmed { get; set; }
/// <summary>The clashes (<see cref="Installer.ConflictKey"/>) the user accepted for what is installed; kept until the selection changes them.</summary>
public string? ConfirmedConflict { get; set; }

/// <summary>Files we placed: relative to the game root, or <see cref="InstallTag"/>\… relative to the install folder (see <see cref="Entry"/>).</summary>
public List<string> Files { get; set; } = [];
Expand Down
121 changes: 74 additions & 47 deletions src/DLSSUpdater/Core/Installer.cs

Large diffs are not rendered by default.

73 changes: 56 additions & 17 deletions src/DLSSUpdater/Core/MethodAdvisor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public enum Availability
public sealed record MethodOption(Method Method, string Title, Availability Availability, string Reason,
IReadOnlyList<string> Components, IReadOnlyList<string> Needs, InstallMode? Mode, bool Unverified)
{
/// <summary>Set when the method adds files to a game that is risky to add them to (anti-cheat, Store install): installing asks for a confirmation and auto never picks it.</summary>
public string? Warning { get; init; }
/// <summary>True when the game page can select this method's components.</summary>
public bool Selectable => (Availability is Availability.Available or Availability.NeedsSetup) && Components.Count > 0;
}
Expand All @@ -63,8 +65,10 @@ public sealed record MethodOption(Method Method, string Title, Availability Avai
/// </summary>
public sealed record MethodAdvice(IReadOnlyList<MethodOption> Methods, IReadOnlyList<int> Rows, string? Blocked)
{
/// <summary>The best method the game page can select.</summary>
public MethodOption? Top => Methods.FirstOrDefault(m => m.Selectable);
/// <summary>Shown above the methods when adding files to this game is risky; each such method also carries it as its <see cref="MethodOption.Warning"/>.</summary>
public string? Caution { get; init; }
/// <summary>The best method the game page can select: never one that carries a warning.</summary>
public MethodOption? Top => Methods.FirstOrDefault(m => m.Selectable && m.Warning is null);
public IEnumerable<MethodOption> Possible => Methods.Where(m => m.Availability is Availability.Available or Availability.NeedsSetup);
public IEnumerable<MethodOption> Unavailable => Methods.Where(m => m.Availability is Availability.NotPossible or Availability.Coming);
}
Expand All @@ -76,6 +80,8 @@ public sealed record AdvisorInput(GameInfo Game, GpuInfo Gpu)
public bool? NativeDlss { get; init; }
/// <summary>A Microsoft Store / Xbox install whose folder is protected (<see cref="MethodAdvisor.IsProtectedStoreInstall"/>).</summary>
public bool ProtectedInstall { get; init; }
/// <summary>A Store / Xbox install whose folder the app can write to: allowed after a confirmation (<see cref="MethodAdvisor.IsStoreInstall"/>).</summary>
public bool StoreInstall { get; init; }
public CatalogOptiGame? OptiCompat { get; init; }
public bool HudfixIncompatible { get; init; }
/// <summary>The game's entry on RenoDX's list, when the catalog has one.</summary>
Expand Down Expand Up @@ -116,35 +122,68 @@ public static class MethodAdvisor
};

/// <summary>
/// A Store install the app can't write to: under WindowsApps, or a MicrosoftGame.config game whose exe could not be read.
/// (docs/research/methods-2026-10.md section 6.3: the folder is unwritable and the exe unreadable.)
/// A Store / Xbox layout: under WindowsApps, or a MicrosoftGame.config game whose exe could not be read
/// (docs/research/methods-2026-10.md section 6.3).
/// </summary>
public static bool IsProtectedStoreInstall(GameInfo game, Func<string, bool>? fileExists = null)
public static bool IsStoreInstall(GameInfo game, Func<string, bool>? fileExists = null)
{
fileExists ??= File.Exists;
if (game.Root.Contains(@"\WindowsApps\", StringComparison.OrdinalIgnoreCase)) return true;
return game.Tech.Bits == 0 && game.Exes.Count > 0 && fileExists(Path.Combine(game.Root, "MicrosoftGame.config"));
}

/// <summary>A Store install whose folder the app can't write to (a probe file fails): nothing can be added, whatever the user confirms.</summary>
public static bool IsProtectedStoreInstall(GameInfo game, Func<string, bool>? fileExists = null, Func<string, bool>? canWrite = null) =>
IsStoreInstall(game, fileExists) && !(canWrite ?? FolderWritable)(game.Root);

/// <summary>Creates and deletes a probe file in <paramref name="dir"/>.</summary>
public static bool FolderWritable(string dir)
{
try
{
var probe = Path.Combine(dir, ".dlssu-probe-" + Guid.NewGuid().ToString("N")[..8]);
File.WriteAllBytes(probe, []);
File.Delete(probe);
return true;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException) { return false; }
}

public const string StoreCaution = "Microsoft Store install: Store updates can undo changes in this folder and the game may refuse to start modified. The folder is writable here, so you can install anyway.";

public static string AntiCheatCaution(string ac) =>
$"{ac} anti-cheat found: injecting files into this game can get your account banned. Online play is the risk; offline and single-player use is fine. A driver frame limit is still safe.";

public static MethodAdvice Advise(AdvisorInput input)
{
var game = input.Game;
// Row 20: nothing may be added to the game.
if (game.AntiCheat is { } ac)
return Blocked(20, $"{ac} anti-cheat found: injecting files into the game can get you banned, so no method that adds files is offered. A driver frame limit is still safe.");
// Row 20: the folder is locked, so nothing can be added whatever the user says.
if (input.ProtectedInstall)
return Blocked(20, "Microsoft Store install: the game folder is protected, so the app can't add files to it. The driver frame limit still works.");

// Row 19: a GPU without tensor cores or from another vendor.
MethodAdvice advice;
if (!input.Gpu.MayRunNvidiaMethods)
return NonNvidia(input);
advice = NonNvidia(input);
else
{
var ctx = new Ctx(input);
var rows = new List<int>();
var methods = new List<MethodOption>();
Shape(ctx, rows, methods);
AddExtras(ctx, rows, methods);
advice = new MethodAdvice(Rank(methods), rows, null);
}

var ctx = new Ctx(input);
var rows = new List<int>();
var methods = new List<MethodOption>();
Shape(ctx, rows, methods);
AddExtras(ctx, rows, methods);
return new MethodAdvice(Rank(methods), rows, null);
// Row 20, the risky kind: adding files stays possible, after a confirmation; auto never picks it.
var caution = string.Join(" ", new[] { game.AntiCheat is { } ac ? AntiCheatCaution(ac) : null, input.StoreInstall ? StoreCaution : null }.Where(x => x is not null));
if (caution.Length == 0) return advice;
return advice with
{
Methods = [.. advice.Methods.Select(m => m.Selectable ? m with { Warning = caution } : m)],
Rows = [.. advice.Rows.Append(20).Distinct()],
Caution = caution,
};
}

private static MethodAdvice Blocked(int row, string reason) => new([], [row], reason);
Expand Down Expand Up @@ -381,8 +420,8 @@ private static void AddExtras(Ctx c, List<int> rows, List<MethodOption> list)
Method.OptiScalerNr => NrAddon.OptiScaler,
_ => null,
};
if (adds is { } a && NrAddons.Refusal(a, [NrAddon.Bridge], false) is { } why)
list[i] = list[i] with { Availability = Availability.NotPossible, Reason = why + " Remove the bridge files first." };
if (adds is { } a && NrAddons.Conflict(a, [NrAddon.Bridge]) is { } why)
list[i] = list[i] with { Warning = why + " Installing it asks you to confirm, or remove the bridge files first." };
}
}
else if (c.In.InstalledRoutes.Count > 0)
Expand Down
86 changes: 27 additions & 59 deletions src/DLSSUpdater/Core/NrAddons.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,14 @@ namespace DLSSUpdater.Core;
public enum NrAddon { RenoDx, Lecram, Uplift, Dfc, Bridge, OptiScaler }

/// <summary>
/// Which NR add-ons may be on together (docs/addons). Only ShortFuse's and Lecram's can share a game: Lecram then
/// observes instead of standing down and NR runs twice. GITC Uplift refuses to run next to the RenoDX add-ons, and
/// Deep Fried Chicken replaces the others, so those two always stay on their own.
/// Which NR routes clash (docs/addons). No pair is blocked: a clash is a reason shown with "Install anyway", so every add-on can
/// be tried next to the others for comparison. Defaults in Settings and the method picks never choose a clashing set.
/// </summary>
public static class NrAddons
{
/// <summary>Who stays when only one may: the route the mod picker treats as the one in use.</summary>
/// <summary>The route the mod picker treats as the one in use when several are on.</summary>
public static readonly NrAddon[] Priority = [NrAddon.Uplift, NrAddon.Dfc, NrAddon.Lecram, NrAddon.RenoDx];

public static bool CanRunTogether(NrAddon a, NrAddon b, bool allowSeveral) =>
allowSeveral && (a, b) is (NrAddon.RenoDx, NrAddon.Lecram) or (NrAddon.Lecram, NrAddon.RenoDx);

/// <summary>The enabled add-ons to switch off after the user switched <paramref name="turnedOn"/> on.</summary>
public static IReadOnlyList<NrAddon> ToSwitchOff(NrAddon turnedOn, IEnumerable<NrAddon> enabled, bool allowSeveral) =>
enabled.Where(a => a != turnedOn && !CanRunTogether(turnedOn, a, allowSeveral)).Distinct().ToList();

/// <summary>The add-ons that survive the rule, kept by <see cref="Priority"/>.</summary>
public static IReadOnlySet<NrAddon> Normalize(IEnumerable<NrAddon> enabled, bool allowSeveral)
{
var on = enabled.ToHashSet();
var kept = new List<NrAddon>();
foreach (var a in Priority)
if (on.Contains(a) && kept.All(k => CanRunTogether(k, a, allowSeveral))) kept.Add(a);
return kept.ToHashSet();
}

public static string Name(NrAddon a) => a switch
{
NrAddon.RenoDx => "the ShortFuse RenoDX DLSS add-on",
Expand All @@ -45,29 +27,41 @@ public static IReadOnlySet<NrAddon> Normalize(IEnumerable<NrAddon> enabled, bool
};

/// <summary>
/// Why <paramref name="adding"/> may not join the routes already in the game (<paramref name="present"/>), as one line; null when
/// it may. One neural consumer per game (docs/research/methods-2026-10.md section 2 and section 8 row 18): they all detour the
/// game's NGX calls, and Deep Fried Chicken does nothing at all when another one is loaded beside it.
/// Why <paramref name="adding"/> clashes with the routes already in the game (<paramref name="present"/>), as one line; null
/// when it doesn't. One neural consumer per game is the rule (docs/research/methods-2026-10.md section 2 and section 8 row 18):
/// they all detour the game's NGX calls, and Deep Fried Chicken does nothing at all when another one is loaded beside it.
/// </summary>
public static string? Refusal(NrAddon adding, IEnumerable<NrAddon> present, bool allowSeveral)
public static string? Conflict(NrAddon adding, IEnumerable<NrAddon> present)
{
foreach (var other in present.Where(p => p != adding).Distinct())
{
if (CanRunTogether(adding, other, allowSeveral)) continue;
if ((adding, other) is (NrAddon.RenoDx, NrAddon.Lecram) or (NrAddon.Lecram, NrAddon.RenoDx))
return "ShortFuse's and Lecram's add-ons both on: NR runs twice, which costs more FPS, and not every pair of builds works together.";
var why = adding == NrAddon.Dfc || other == NrAddon.Dfc ? "Deep Fried Chicken does nothing when another neural route is loaded beside it"
: adding == NrAddon.OptiScaler || other == NrAddon.OptiScaler ? "OptiScaler takes over the NGX calls the other route needs"
: adding == NrAddon.Bridge || other == NrAddon.Bridge ? "the bridge is for games with their own DLSS and is never combined with another neural route"
: "only one neural route can hook the game's DLSS calls";
return $"{Capitalize(Name(adding))} and {Name(other)} can't be in the same game: {why}.";
return $"{Capitalize(Name(adding))} and {Name(other)} don't work well in the same game: {why}.";
}
return null;
}

/// <summary>Every clash among <paramref name="routes"/>, one line per pair.</summary>
public static IReadOnlyList<string> Conflicts(IEnumerable<NrAddon> routes)
{
var list = routes.Distinct().ToList();
var found = new List<string>();
for (var i = 0; i < list.Count; i++)
if (Conflict(list[i], list.Skip(i + 1)) is { } why && !found.Contains(why)) found.Add(why);
return found;
}

/// <summary>
/// What stops DLSS5-Feeder in a game with <paramref name="present"/> routes; null when it fits. Feeder only makes the DLSS
/// What clashes with DLSS5-Feeder in a game with <paramref name="present"/> routes; null when it fits. Feeder only makes the DLSS
/// calls a game without DLSS never makes, so it needs exactly one neural consumer to act on them, and the app only pairs it
/// with Deep Fried Chicken (the consumer Feeder's README recommends, and the one the app can install in a game without DLSS).
/// </summary>
public static string? FeederRefusal(IEnumerable<NrAddon> present)
public static string? FeederConflict(IEnumerable<NrAddon> present)
{
var routes = present.Distinct().ToList();
if (routes.Where(r => r != NrAddon.Dfc).Select(r => (NrAddon?)r).FirstOrDefault() is { } other)
Expand Down Expand Up @@ -103,37 +97,11 @@ public static IReadOnlyList<NrAddon> PresentIn(string dir, InstallManifest? reco
return found;
}

/// <summary>The one-line note shown while ShortFuse and Lecram are both on; null otherwise.</summary>
public static string? Warning(bool renoDx, bool lecram) =>
renoDx && lecram ? "ShortFuse and Lecram both on: NR runs twice, which costs more FPS, and not every pair of builds works together." : null;
/// <summary>Switches a default add-on in Settings. Other defaults stay as they are; a clash is a warning the user confirms.</summary>
public static void SetDefault(AppSettings s, NrAddon addon, bool on) => Apply(s, addon, on);

/// <summary>
/// Settings saved before the rule may have several add-ons on. A ShortFuse + Lecram setup keeps working with
/// <see cref="AppSettings.AllowSeveralNrAddons"/> turned on; with GITC Uplift among them Uplift stays, as installs already did.
/// </summary>
public static bool Migrate(AppSettings s)
{
var before = (s.InstallRenoDx, s.InstallLecram, s.InstallUplift, s.AllowSeveralNrAddons);
if (s.InstallRenoDx && s.InstallLecram && !s.InstallUplift) s.AllowSeveralNrAddons = true;
SetAllowSeveral(s, s.AllowSeveralNrAddons);
return before != (s.InstallRenoDx, s.InstallLecram, s.InstallUplift, s.AllowSeveralNrAddons);
}

/// <summary>Switches a default add-on in Settings; switching one on switches off those it can't run with.</summary>
public static void SetDefault(AppSettings s, NrAddon addon, bool on)
{
if (on)
foreach (var other in ToSwitchOff(addon, Defaults(s), s.AllowSeveralNrAddons)) Apply(s, other, false);
Apply(s, addon, on);
}

/// <summary>Turning the override off leaves one default add-on, kept by <see cref="Priority"/>.</summary>
public static void SetAllowSeveral(AppSettings s, bool allow)
{
s.AllowSeveralNrAddons = allow;
var kept = Normalize(Defaults(s), allow);
foreach (var a in Defaults(s).ToList()) Apply(s, a, kept.Contains(a));
}
/// <summary>What clashes among the defaults in Settings, one line per pair; empty when they fit.</summary>
public static IReadOnlyList<string> DefaultConflicts(AppSettings s) => Conflicts(Defaults(s));

private static void Apply(AppSettings s, NrAddon addon, bool on)
{
Expand Down
Loading