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
17 changes: 17 additions & 0 deletions src/LuaToolsGui/Models/FixModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,20 @@ public class DenuvoDownloadResponse
{
[JsonPropertyName("url")] public string Url { get; set; } = "";
}

// ── Fix revert manifest (written to .luatools-fix/ inside the game folder) ──

public class DenuvoFixManifest
{
[JsonPropertyName("appId")] public long AppId { get; set; }
[JsonPropertyName("fixId")] public string FixId { get; set; } = "";
[JsonPropertyName("appliedAt")] public string AppliedAt { get; set; } = "";
[JsonPropertyName("files")] public List<DenuvoFixManifestEntry> Files { get; set; } = [];
}

public class DenuvoFixManifestEntry
{
[JsonPropertyName("relativePath")] public string RelativePath { get; set; } = "";
[JsonPropertyName("action")] public string Action { get; set; } = ""; // "modified" or "added"
[JsonPropertyName("backupPath")] public string? BackupPath { get; set; } // relative to .luatools-fix/
}
14 changes: 14 additions & 0 deletions src/LuaToolsGui/Resources/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions src/LuaToolsGui/Resources/Strings.es.resx
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,20 @@ Esto elimina los archivos .lua de Steam\config\stplug-in.</value></data>
<data name="Fixes_Toast_CouldntApply" xml:space="preserve"><value>No se pudo aplicar el parche</value></data>
<data name="Fixes_Toast_Refreshed_Title" xml:space="preserve"><value>Actualizado</value></data>
<data name="Fixes_Toast_Refreshed_Body" xml:space="preserve"><value>{0} juegos con fixes.</value></data>
<data name="Fixes_Revert" xml:space="preserve"><value>Revertir</value></data>
<data name="Fixes_Revert_Cancel" xml:space="preserve"><value>Cancelar</value></data>
<data name="Fixes_Revert_Confirm_Title" xml:space="preserve"><value>¿Revertir el parche de {0}?</value></data>
<data name="Fixes_Revert_Confirm_Body" xml:space="preserve"><value>¿Revertir "{0}"? Los archivos modificados se restaurarán desde la copia de seguridad y los archivos añadidos por el parche se eliminarán.</value></data>
<data name="Fixes_Revert_Done" xml:space="preserve"><value>Parche revertido</value></data>
<data name="Fixes_Revert_Done_Body" xml:space="preserve"><value>Se restauraron {0} y se eliminaron {1} archivo(s) añadido(s).</value></data>
<data name="Fixes_Revert_Partial" xml:space="preserve"><value>Parche revertido parcialmente</value></data>
<data name="Fixes_Revert_Partial_Body" xml:space="preserve"><value>No se pudieron restaurar {0} archivo(s). Cierra el juego e inténtalo de nuevo.</value></data>
<data name="Fixes_Revert_Failed" xml:space="preserve"><value>No se pudo revertir el parche</value></data>
<data name="Fixes_Revert_NoManifest" xml:space="preserve"><value>No hay registro de que este parche se haya aplicado. Puede que ya se haya revertido.</value></data>
<data name="Fixes_Applied_Hint" xml:space="preserve"><value>Este parche ya está aplicado. Revierte para instalar otro.</value></data>
<data name="Fixes_MyGames" xml:space="preserve"><value>Mis juegos</value></data>
<data name="Fixes_MyGames_Count" xml:space="preserve"><value>{0} de tus juegos tienen parches. Desplázate para ver el resto.</value></data>
<data name="Fixes_MyGames_NotInstalled" xml:space="preserve"><value>Aún no hay luas agregadas: añade un juego (o instala uno) para verlo aquí.</value></data>
<data name="Add_Title" xml:space="preserve"><value>¡A por los Luas™!</value></data>
<data name="Add_Subtitle" xml:space="preserve"><value>Introduce un App ID, pega un enlace de Steam o busca por nombre de juego</value></data>
<data name="Add_SearchPlaceholder" xml:space="preserve"><value>Introduce un App ID o busca juegos...</value></data>
Expand Down
14 changes: 14 additions & 0 deletions src/LuaToolsGui/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,20 @@ This deletes the .lua files from Steam\config\stplug-in.</value></data>
<data name="Fixes_Toast_CouldntApply" xml:space="preserve"><value>Couldn't apply fix</value></data>
<data name="Fixes_Toast_Refreshed_Title" xml:space="preserve"><value>Refreshed</value></data>
<data name="Fixes_Toast_Refreshed_Body" xml:space="preserve"><value>{0} games with fixes.</value></data>
<data name="Fixes_Revert" xml:space="preserve"><value>Revert</value></data>
<data name="Fixes_Revert_Cancel" xml:space="preserve"><value>Cancel</value></data>
<data name="Fixes_Revert_Confirm_Title" xml:space="preserve"><value>Revert fix for {0}?</value></data>
<data name="Fixes_Revert_Confirm_Body" xml:space="preserve"><value>Revert "{0}"? Modified files will be restored from backup and files added by the fix will be deleted.</value></data>
<data name="Fixes_Revert_Done" xml:space="preserve"><value>Fix reverted</value></data>
<data name="Fixes_Revert_Done_Body" xml:space="preserve"><value>{0} restored, {1} added files removed.</value></data>
<data name="Fixes_Revert_Partial" xml:space="preserve"><value>Fix partially reverted</value></data>
<data name="Fixes_Revert_Partial_Body" xml:space="preserve"><value>{0} file(s) couldn't be restored. Close the game and try again.</value></data>
<data name="Fixes_Revert_Failed" xml:space="preserve"><value>Couldn't revert fix</value></data>
<data name="Fixes_Revert_NoManifest" xml:space="preserve"><value>No record of this fix being applied. It may already have been reverted.</value></data>
<data name="Fixes_Applied_Hint" xml:space="preserve"><value>This fix is already applied. Revert it if you want to install another.</value></data>
<data name="Fixes_MyGames" xml:space="preserve"><value>My games</value></data>
<data name="Fixes_MyGames_Count" xml:space="preserve"><value>{0} of your games have fixes. Scroll the grid to browse the rest.</value></data>
<data name="Fixes_MyGames_NotInstalled" xml:space="preserve"><value>No luas added yet — add a game (or install one) to see it here.</value></data>

<!-- ── Add / Download page ── -->
<data name="Add_Title" xml:space="preserve"><value>Let's get Luing™!</value></data>
Expand Down
162 changes: 155 additions & 7 deletions src/LuaToolsGui/Services/Downloads/ManifestJobFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public DownloadJob CreateDenuvoJob(
},
(file, _, _) => Task.FromResult(isManifestSlot
? InstallDenuvoManifest(file, appId, gameName)
: ApplyDenuvoFix(file, appId, gameName)),
: ApplyDenuvoFix(file, appId, fixId, gameName)),
ConfirmAsync: null,
OnFinished: onFinished);
}
Expand Down Expand Up @@ -561,8 +561,9 @@ private JobResult InstallDenuvoManifest(DownloadedFile file, long appId, string
}
}

/// <summary>Denuvo fix slot: extract into the game folder. Only possible if the game is installed.</summary>
private JobResult ApplyDenuvoFix(DownloadedFile file, long appId, string gameName)
/// <summary>Denuvo fix slot: extract into the game folder. Only possible if the game is installed.
/// Existing files are backed up as .bak inside .luatools-fix/ so the fix can be reverted.</summary>
private JobResult ApplyDenuvoFix(DownloadedFile file, long appId, string fixId, string gameName)
{
try
{
Expand All @@ -574,22 +575,70 @@ private JobResult ApplyDenuvoFix(DownloadedFile file, long appId, string gameNam
return new JobResult(false, err);
}

// Extract into the game folder, overwriting. Best-effort per entry so one locked file
// doesn't abandon the rest of the fix.
string fixKey = SafeFixKey(fixId);
string fixDir = Path.Combine(installDir, FixManifestDir);
string backupDir = Path.Combine(fixDir, fixKey);
string manifestPath = Path.Combine(fixDir, $"{fixKey}.json");
Directory.CreateDirectory(backupDir);

var manifest = new DenuvoFixManifest
{
AppId = appId,
FixId = fixId,
AppliedAt = DateTimeOffset.UtcNow.ToString("o"),
};

using var archive = ZipFile.OpenRead(file.FilePath);
int failed = 0;
foreach (var entry in archive.Entries)
{
if (string.IsNullOrEmpty(entry.Name)) continue; // directory entry
if (string.IsNullOrEmpty(entry.Name)) continue;
string dest = Path.Combine(installDir, entry.FullName);
string relPath = entry.FullName.Replace('\\', '/');

try
{
if (File.Exists(dest))
{
// Back up the original — never clobber a known-good .bak
string bakRel = $"{fixKey}/{Path.GetFileName(dest)}.bak";
string bakAbs = Path.Combine(installDir, FixManifestDir, bakRel);
if (!File.Exists(bakAbs))
{
Directory.CreateDirectory(Path.GetDirectoryName(bakAbs)!);
File.Copy(dest, bakAbs, overwrite: false);
}
manifest.Files.Add(new DenuvoFixManifestEntry
{
RelativePath = relPath,
Action = "modified",
BackupPath = bakRel,
});
}
else
{
manifest.Files.Add(new DenuvoFixManifestEntry
{
RelativePath = relPath,
Action = "added",
});
}

Directory.CreateDirectory(Path.GetDirectoryName(dest)!);
entry.ExtractToFile(dest, overwrite: true);
}
catch { failed++; }
}

// Write manifest even on partial failure — the backed-up files still need to be recoverable.
try
{
string json = System.Text.Json.JsonSerializer.Serialize(
manifest, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(manifestPath, json);
}
catch { /* best effort */ }

if (failed > 0)
{
string err = string.Format(Resources.Strings.Fixes_Toast_PartiallyApplied_Body, failed);
Expand All @@ -608,7 +657,106 @@ private JobResult ApplyDenuvoFix(DownloadedFile file, long appId, string gameNam
}
finally
{
DeleteStaged(file.FilePath); // archive is disposed by now
DeleteStaged(file.FilePath);
}
}

// ── Fix revert ──────────────────────────────────────────────────

private static readonly System.Text.Json.JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true };

internal const string FixManifestDir = ".luatools-fix";

/// <summary>
/// A fix id can carry `/`, `:`, and other characters that are illegal in a Windows path segment
/// (e.g. "online-fix:5e01e852-..."). Sanitise it for use as a folder/file name while keeping the
/// raw id in the manifest itself.
/// </summary>
internal static string SafeFixKey(string fixId) =>
string.Concat(fixId.Select(ch => Path.GetInvalidFileNameChars().Contains(ch) ? '_' : ch));

/// <summary>Resolve the manifest path for a given fix inside a game's install folder.</summary>
internal static string GetFixManifestPath(string installDir, string fixId) =>
Path.Combine(installDir, FixManifestDir, $"{SafeFixKey(fixId)}.json");

/// <summary>Read a fix manifest from disk, or null if it doesn't exist.</summary>
internal static DenuvoFixManifest? ReadFixManifest(string manifestPath)
{
try
{
if (!File.Exists(manifestPath)) return null;
string json = File.ReadAllText(manifestPath);
return System.Text.Json.JsonSerializer.Deserialize<DenuvoFixManifest>(json, JsonOpts);
}
catch { return null; }
}

/// <summary>
/// Revert a previously applied Denuvo fix: restore .bak files for modified entries, delete added
/// files, then clean up the backup directory and manifest.
/// </summary>
public JobResult RevertDenuvoFix(long appId, string fixId, string gameName)
{
try
{
string? installDir = library.GetInstallDir(appId);
if (installDir is null)
return new JobResult(false, string.Format(Resources.Strings.Fixes_Toast_GameNotFound_Body, gameName));

string manifestPath = GetFixManifestPath(installDir, fixId);
var manifest = ReadFixManifest(manifestPath);
if (manifest is null)
return new JobResult(false, Resources.Strings.Fixes_Revert_NoManifest);

int restored = 0, deleted = 0, errors = 0;

foreach (var entry in manifest.Files)
{
string dest = Path.Combine(installDir, entry.RelativePath);
try
{
if (entry.Action == "modified" && entry.BackupPath is { } bakRel)
{
string bakAbs = Path.Combine(installDir, FixManifestDir, bakRel);
if (File.Exists(bakAbs))
{
File.Copy(bakAbs, dest, overwrite: true);
File.Delete(bakAbs);
restored++;
}
}
else if (entry.Action == "added")
{
if (File.Exists(dest))
{
File.Delete(dest);
deleted++;
}
}
}
catch { errors++; }
}

// Clean up backup dir for this fix
string backupDir = Path.Combine(installDir, FixManifestDir, SafeFixKey(fixId));
try { if (Directory.Exists(backupDir)) Directory.Delete(backupDir, recursive: true); } catch { }
try { if (File.Exists(manifestPath)) File.Delete(manifestPath); } catch { }

if (errors > 0)
{
string err = string.Format(Resources.Strings.Fixes_Revert_Partial_Body, errors);
toast.Show(Resources.Strings.Fixes_Revert_Partial, err, error: true);
return new JobResult(false, err);
}

string message = string.Format(Resources.Strings.Fixes_Revert_Done_Body, restored, deleted);
toast.Show(Resources.Strings.Fixes_Revert_Done, message);
return new JobResult(true, message, installDir);
}
catch (Exception ex)
{
toast.Show(Resources.Strings.Fixes_Toast_CouldntApply, ex.Message, error: true);
return new JobResult(false, ex.Message);
}
}

Expand Down
Loading
Loading