Recall Installed DLC Locker for Games / Labels for DLC Unlockers / Additional Extra Protection Changes

- Added method to remember the games you've installed so they don't need to be reselected.
- Added labels for to display if CreamAPI / SmokeAPI DLC Unlockers are installed
- Logic to ensure the Extra Protection checkbox displays at the appropriate time
- Added logic to read Extra Protection state from cream_api.ini when CreamAPI is detected
This commit is contained in:
Frog
2026-06-01 02:46:47 -07:00
parent 66cf72faeb
commit 94bec38bd0
7 changed files with 391 additions and 9 deletions
+52 -1
View File
@@ -257,7 +257,58 @@ internal sealed class CustomTreeView : TreeView
graphics.FillRectangle(brush, bounds);
}
if (!Program.UseSmokeAPI)
// Unlocker badge
if (selection.InstalledUnlocker != InstalledUnlocker.None)
{
string badgeText = selection.InstalledUnlocker.ToString();
size = TextRenderer.MeasureText(graphics, badgeText, font, Size.Empty, TextFormatFlags.NoPadding);
const int badgePadding = 3;
Rectangle badgeBounds = new(bounds.X + bounds.Width + 2, bounds.Y + 1, size.Width + badgePadding * 2, bounds.Height - 2);
selectionBounds = new(selectionBounds.Location, selectionBounds.Size + new Size(badgeBounds.Width + 2, 0));
// Get theme-appropriate colors for each unlocker from ThemeManager
Color badgeBack, badgeBorder;
switch (selection.InstalledUnlocker)
{
case InstalledUnlocker.SmokeAPI:
badgeBack = highlighted
? ThemeManager.SmokeAPIBadgeBackgroundHighlightColor
: ThemeManager.SmokeAPIBadgeBackgroundColor;
badgeBorder = ThemeManager.SmokeAPIBadgeBorderColor;
break;
case InstalledUnlocker.CreamAPI:
badgeBack = highlighted
? ThemeManager.CreamAPIBadgeBackgroundHighlightColor
: ThemeManager.CreamAPIBadgeBackgroundColor;
badgeBorder = ThemeManager.CreamAPIBadgeBorderColor;
break;
default:
badgeBack = highlighted
? ThemeManager.DefaultBadgeBackgroundHighlightColor
: ThemeManager.DefaultBadgeBackgroundColor;
badgeBorder = ThemeManager.DefaultBadgeBorderColor;
break;
}
using (SolidBrush badgeBrush = new(badgeBack))
graphics.FillRectangle(badgeBrush, badgeBounds);
using (Pen badgePen = new(badgeBorder))
graphics.DrawRectangle(badgePen, badgeBounds);
TextRenderer.DrawText(graphics, badgeText, font,
new Point(badgeBounds.X + badgePadding, badgeBounds.Y + 1),
Color.White, TextFormatFlags.NoPadding);
bounds = bounds with { X = badgeBounds.X, Width = badgeBounds.Width + 2 };
}
// Show Extra Protection checkbox for CreamAPI:
// - When CreamAPI is installed, OR
// - When no unlocker is installed yet AND user hasn't enabled SmokeAPI mode, OR
// - When SmokeAPI is installed BUT user has disabled SmokeAPI mode (about to replace with CreamAPI)
bool showExtraProtection = selection.InstalledUnlocker == InstalledUnlocker.CreamAPI ||
(selection.InstalledUnlocker == InstalledUnlocker.None && !Program.UseSmokeAPI) ||
(selection.InstalledUnlocker == InstalledUnlocker.SmokeAPI && !Program.UseSmokeAPI);
if (showExtraProtection)
{
CheckBoxState extraProtState = selection.UseExtraProtection
? (Enabled ? CheckBoxState.CheckedNormal : CheckBoxState.CheckedDisabled)
+35 -1
View File
@@ -9,7 +9,6 @@ using CreamInstaller.Resources;
using CreamInstaller.Utility;
using static CreamInstaller.Platforms.Paradox.ParadoxLauncher;
using static CreamInstaller.Resources.Resources;
namespace CreamInstaller.Forms;
internal sealed partial class InstallForm : CustomForm
@@ -351,6 +350,41 @@ internal sealed partial class InstallForm : CustomForm
++completeOperationsCount;
}
// Persist install/uninstall results
foreach (Selection selection in Selection.AllEnabled)
{
if (uninstalling)
{
selection.InstalledUnlocker = InstalledUnlocker.None;
ProgramData.RemoveInstalledGame(selection.Platform, selection.Id);
}
else
{
InstalledUnlocker unlocker = selection.DetectInstalledUnlocker();
selection.InstalledUnlocker = unlocker;
if (unlocker != InstalledUnlocker.None)
ProgramData.UpsertInstalledGame(new InstalledGameRecord
{
Platform = selection.Platform,
Id = selection.Id,
Name = selection.Name,
RootDirectory = selection.RootDirectory,
Unlocker = unlocker,
UseProxy = selection.UseProxy,
Proxy = selection.Proxy,
UseExtraProtection = selection.UseExtraProtection,
Dlc = selection.DLC.Select(dlc => new InstalledDlcRecord
{
DlcType = dlc.Type.ToString(),
Id = dlc.Id,
Name = dlc.Name
}).ToList()
});
}
}
SelectForm.Current?.Invoke(() => SelectForm.Current?.InvalidateGameList());
Program.Cleanup();
int activeCount = activeSelections.Count;
if (activeCount > 0)
+94 -1
View File
@@ -689,6 +689,7 @@ internal sealed partial class SelectForm : CustomForm
}
OnLoadSelections(null, null);
await LoadSavedInstalledGames();
HideProgressBar();
selectionTreeView.Enabled = !Selection.All.IsEmpty;
allCheckBox.Enabled = selectionTreeView.Enabled;
@@ -974,6 +975,79 @@ internal sealed partial class SelectForm : CustomForm
contextMenuStrip.Refresh();
});
private async Task LoadSavedInstalledGames()
{
List<InstalledGameRecord> saved = ProgramData.ReadInstalledGames();
if (saved.Count == 0)
return;
List<InstalledGameRecord> toRemove = [];
foreach (InstalledGameRecord record in saved)
{
// Already in the list from this scan — just ensure unlocker is set
Selection existing = Selection.FromId(record.Platform, record.Id);
if (existing is not null)
{
if (existing.InstalledUnlocker == InstalledUnlocker.None)
existing.InstalledUnlocker = record.Unlocker;
continue;
}
// Root directory no longer exists — mark for removal
if (!record.RootDirectory.DirectoryExists())
{
toRemove.Add(record);
continue;
}
// Reconstruct a minimal Selection from the saved record
HashSet<string> dllDirectories =
await record.RootDirectory.GetDllDirectoriesFromGameDirectory(record.Platform);
if (dllDirectories is null || dllDirectories.Count == 0)
{
toRemove.Add(record);
continue;
}
List<(string directory, BinaryType binaryType)> executableDirectories =
await record.RootDirectory.GetExecutableDirectories(true);
Selection selection = Selection.FromId(record.Platform, record.Id) ?? Selection.GetOrCreate(record.Platform, record.Id, record.Name,
record.RootDirectory, dllDirectories, executableDirectories);
selection.InstalledUnlocker = selection.DetectInstalledUnlocker();
if (selection.InstalledUnlocker == InstalledUnlocker.None)
selection.InstalledUnlocker = record.Unlocker;
selection.UseProxy = record.UseProxy;
selection.Proxy = record.Proxy;
selection.UseExtraProtection = record.UseExtraProtection;
Invoke(delegate
{
if (selection.TreeNode.TreeView is null)
_ = selectionTreeView.Nodes.Add(selection.TreeNode);
// Restore DLC children from saved record
if (record.Dlc != null && record.Dlc.Count > 0)
{
foreach (InstalledDlcRecord dlcRecord in record.Dlc)
{
if (!Enum.TryParse(dlcRecord.DlcType, out DLCType dlcType))
continue;
SelectionDLC dlc = SelectionDLC.GetOrCreate(dlcType, record.Id, dlcRecord.Id, dlcRecord.Name);
dlc.Selection = selection;
}
}
});
}
// Clean up records for games that are gone
if (toRemove.Count > 0)
{
List<InstalledGameRecord> updated = saved.Except(toRemove).ToList();
ProgramData.WriteInstalledGames(updated);
}
}
private void OnLoad(object sender, EventArgs _)
{
retry:
@@ -1177,6 +1251,21 @@ internal sealed partial class SelectForm : CustomForm
ProgramData.WriteExtraProtectionChoices(extraProtectionChoices);
loadButton.Enabled = CanLoadSelections();
// Detect installed unlockers from disk for all selections
foreach (Selection selection in Selection.All.Keys)
selection.InstalledUnlocker = selection.DetectInstalledUnlocker();
// Merge with persisted installed game records for any saved games not yet having a detected unlocker
List<InstalledGameRecord> installedRecords = ProgramData.ReadInstalledGames();
foreach (InstalledGameRecord record in installedRecords)
{
Selection selection = Selection.FromId(record.Platform, record.Id);
if (selection is null)
continue;
if (selection.InstalledUnlocker == InstalledUnlocker.None && record.Unlocker != InstalledUnlocker.None)
selection.InstalledUnlocker = record.Unlocker;
}
OnProxyChanged();
}
@@ -1206,6 +1295,8 @@ internal sealed partial class SelectForm : CustomForm
OnProxyChanged();
}
internal void InvalidateGameList() => selectionTreeView.Invalidate();
internal void OnProxyChanged()
{
selectionTreeView.Invalidate();
@@ -1257,7 +1348,9 @@ internal sealed partial class SelectForm : CustomForm
private void OnUseSmokeAPICheckBoxChanged(object sender, EventArgs e)
{
Program.UseSmokeAPI = useSmokeAPICheckBox.Checked;
OnLoad(forceProvideChoices: false);
selectionTreeView.Invalidate();
saveButton.Enabled = CanSaveSelections();
resetButton.Enabled = CanResetSelections();
}
private void OnUseSmokeAPIHelpButtonClicked(object sender, EventArgs e)
+99 -1
View File
@@ -1,13 +1,16 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using CreamInstaller.Forms;
using CreamInstaller.Platforms.Epic;
using CreamInstaller.Platforms.Steam;
using CreamInstaller.Platforms.Ubisoft;
using CreamInstaller.Resources;
using CreamInstaller.Utility;
using static CreamInstaller.Resources.Resources;
namespace CreamInstaller;
public enum Platform
@@ -41,6 +44,7 @@ internal sealed class Selection : IEquatable<Selection>
internal string Publisher;
internal string SubIcon;
internal string Website;
internal InstalledUnlocker InstalledUnlocker;
internal IEnumerable<string> GetAvailableProxies()
{
@@ -135,6 +139,100 @@ internal sealed class Selection : IEquatable<Selection>
internal static Selection FromId(Platform platform, string gameId) =>
All.Keys.FirstOrDefault(s => s.Platform == platform && s.Id == gameId);
internal InstalledUnlocker DetectInstalledUnlocker()
{
foreach (string directory in DllDirectories)
{
if (Platform is Platform.Steam or Platform.Paradox)
{
// Use uniquely-named config files to distinguish CreamAPI from SmokeAPI.
// Both share steam_api_o.dll so the _o files alone are ambiguous.
directory.GetSmokeApiComponents(out _, out _, out _, out _, out string smokeOldConfig,
out string smokeConfig, out _, out _, out _);
if (smokeConfig.FileExists() || smokeOldConfig.FileExists())
return InstalledUnlocker.SmokeAPI;
directory.GetCreamApiComponents(out _, out _, out _, out _, out string creamConfig);
if (creamConfig.FileExists())
{
ReadCreamApiConfig(creamConfig);
return InstalledUnlocker.CreamAPI;
}
// Fallback: config was deleted but _o files remain — identify by replacement DLL content
directory.GetSmokeApiComponents(out string smokeApi32, out string api32_o,
out string smokeApi64, out string api64_o, out _, out _, out _, out _, out _);
if (api32_o.FileExists() || api64_o.FileExists())
{
if ((smokeApi32.FileExists() && smokeApi32.IsResourceFile(ResourceIdentifier.Steamworks32))
|| (smokeApi64.FileExists() && smokeApi64.IsResourceFile(ResourceIdentifier.Steamworks64)))
return InstalledUnlocker.SmokeAPI;
return InstalledUnlocker.CreamAPI;
}
}
if (Platform is Platform.Epic or Platform.Paradox)
{
directory.GetScreamApiComponents(out _, out string api32_o, out _, out string api64_o,
out _, out string config, out _, out _);
if (config.FileExists() || api32_o.FileExists() || api64_o.FileExists())
return InstalledUnlocker.ScreamAPI;
}
if (Platform is Platform.Ubisoft)
{
directory.GetUplayR1Components(out _, out string api32_o, out _, out string api64_o,
out string config, out _);
if (config.FileExists() || api32_o.FileExists() || api64_o.FileExists())
return InstalledUnlocker.UplayR1;
directory.GetUplayR2Components(out _, out _, out _, out api32_o, out _, out api64_o,
out config, out _);
if (config.FileExists() || api32_o.FileExists() || api64_o.FileExists())
return InstalledUnlocker.UplayR2;
}
}
foreach ((string directory, _) in ExecutableDirectories)
{
directory.GetKoaloaderComponents(out _, out string config, out _);
if (directory.GetKoaloaderProxies().Any(proxy =>
proxy.FileExists() && proxy.IsResourceFile(ResourceIdentifier.Koaloader))
|| config.FileExists())
return InstalledUnlocker.Koaloader;
}
return InstalledUnlocker.None;
}
private void ReadCreamApiConfig(string configPath)
{
try
{
if (!configPath.FileExists())
return;
string[] lines = File.ReadAllLines(configPath);
foreach (string line in lines)
{
string trimmed = line.Trim();
if (trimmed.StartsWith("extraprotection", StringComparison.OrdinalIgnoreCase))
{
string[] parts = trimmed.Split('=');
if (parts.Length == 2)
{
string value = parts[1].Trim();
UseExtraProtection = value.Equals("true", StringComparison.OrdinalIgnoreCase);
}
break;
}
}
}
catch
{
// If we can't read the config, leave UseExtraProtection at its default value
}
}
public override bool Equals(object obj) => ReferenceEquals(this, obj) || obj is Selection other && Equals(other);
public override int GetHashCode() => HashCode.Combine(Id, (int)Platform);
+8 -5
View File
@@ -22,17 +22,20 @@ internal sealed class SelectionDLC : IEquatable<SelectionDLC>
internal readonly string Name;
internal readonly TreeNode TreeNode;
internal readonly DLCType Type;
internal readonly string GameId;
internal string Icon;
internal string Product;
internal string Publisher;
private Selection selection;
private SelectionDLC(DLCType type, string id, string name)
private SelectionDLC(DLCType type, string gameId, string id, string name)
{
Type = type;
GameId = gameId;
Id = id;
Name = name;
TreeNode = new() { Tag = Type, Name = Id, Text = Name };
_ = All.TryAdd(this, 0);
}
internal bool Enabled
@@ -65,15 +68,15 @@ internal sealed class SelectionDLC : IEquatable<SelectionDLC>
public bool Equals(SelectionDLC other)
=> other is not null && (ReferenceEquals(this, other) ||
Type == other.Type && Selection?.Id == other.Selection?.Id && Id == other.Id);
Type == other.Type && GameId == other.GameId && Id == other.Id);
internal static SelectionDLC GetOrCreate(DLCType type, string gameId, string id, string name)
=> FromId(type, gameId, id) ?? new SelectionDLC(type, id, name);
=> FromId(type, gameId, id) ?? new SelectionDLC(type, gameId, id, name);
internal static SelectionDLC FromId(DLCType type, string gameId, string dlcId)
=> All.Keys.FirstOrDefault(dlc => dlc.Type == type && dlc.Selection?.Id == gameId && dlc.Id == dlcId);
=> All.Keys.FirstOrDefault(dlc => dlc.Type == type && dlc.GameId == gameId && dlc.Id == dlcId);
public override bool Equals(object obj) => ReferenceEquals(this, obj) || obj is SelectionDLC other && Equals(other);
public override int GetHashCode() => HashCode.Combine((int)Type, Selection?.Id, Id);
public override int GetHashCode() => HashCode.Combine((int)Type, GameId, Id);
}
+81
View File
@@ -6,10 +6,42 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using CreamInstaller;
using Newtonsoft.Json;
namespace CreamInstaller.Utility;
internal enum InstalledUnlocker
{
None = 0,
CreamAPI,
SmokeAPI,
ScreamAPI,
UplayR1,
UplayR2,
Koaloader
}
internal sealed class InstalledDlcRecord
{
public string DlcType { get; set; }
public string Id { get; set; }
public string Name { get; set; }
}
internal sealed class InstalledGameRecord
{
public Platform Platform { get; set; }
public string Id { get; set; }
public string Name { get; set; }
public string RootDirectory { get; set; }
public InstalledUnlocker Unlocker { get; set; }
public bool UseProxy { get; set; }
public string Proxy { get; set; }
public bool UseExtraProtection { get; set; }
public List<InstalledDlcRecord> Dlc { get; set; } = [];
}
internal static class ProgramData
{
private static readonly string DirectoryPathOld =
@@ -30,6 +62,7 @@ internal static class ProgramData
private static readonly string DlcChoicesPath = DirectoryPath + @"\dlc.json";
private static readonly string KoaloaderProxyChoicesPath = DirectoryPath + @"\proxies.json";
private static readonly string ExtraProtectionChoicesPath = DirectoryPath + @"\extraprotection.json";
private static readonly string InstalledGamesPath = DirectoryPath + @"\installed.json";
internal static readonly string LogPath = DirectoryPath + @"\scan.log";
@@ -263,4 +296,52 @@ internal static class ProgramData
// ignored
}
}
internal static List<InstalledGameRecord> ReadInstalledGames()
{
if (InstalledGamesPath.FileExists())
try
{
if (JsonConvert.DeserializeObject<List<InstalledGameRecord>>(InstalledGamesPath.ReadFile()) is
{ } records)
return records;
}
catch
{
// ignored
}
return [];
}
internal static void WriteInstalledGames(IEnumerable<InstalledGameRecord> records)
{
try
{
List<InstalledGameRecord> list = records?.ToList() ?? [];
if (list.Count == 0)
InstalledGamesPath.DeleteFile();
else
InstalledGamesPath.WriteFile(JsonConvert.SerializeObject(list, Formatting.Indented));
}
catch
{
// ignored
}
}
internal static void UpsertInstalledGame(InstalledGameRecord record)
{
List<InstalledGameRecord> records = ReadInstalledGames();
_ = records.RemoveAll(r => r.Platform == record.Platform && r.Id == record.Id);
records.Add(record);
WriteInstalledGames(records);
}
internal static void RemoveInstalledGame(Platform platform, string id)
{
List<InstalledGameRecord> records = ReadInstalledGames();
if (records.RemoveAll(r => r.Platform == platform && r.Id == id) > 0)
WriteInstalledGames(records);
}
}
+22
View File
@@ -31,6 +31,17 @@ internal static class ThemeManager
private static readonly Color DarkComboBorder = DarkBorder; // #3F3F46
private static readonly Color DarkComboText = DarkFore; // #D4D4D4
// Badge colors for unlockers
private static readonly Color CreamAPIBadgeBack = ColorTranslator.FromHtml("#C8A078"); // Creamy latte
private static readonly Color CreamAPIBadgeBackHighlight = ColorTranslator.FromHtml("#B48C64");
private static readonly Color CreamAPIBadgeBorder = ColorTranslator.FromHtml("#DCB48C");
private static readonly Color SmokeAPIBadgeBack = ColorTranslator.FromHtml("#69696E"); // Smoky grey
private static readonly Color SmokeAPIBadgeBackHighlight = ColorTranslator.FromHtml("#5A5A5F");
private static readonly Color SmokeAPIBadgeBorder = ColorTranslator.FromHtml("#8C8C91");
private static readonly Color DefaultBadgeBack = ColorTranslator.FromHtml("#008C46"); // Default green
private static readonly Color DefaultBadgeBackHighlight = ColorTranslator.FromHtml("#00783C");
private static readonly Color DefaultBadgeBorder = ColorTranslator.FromHtml("#00B45A");
// ----------------------------
// Light mode colors (system defaults)
// ----------------------------
@@ -76,6 +87,17 @@ internal static class ThemeManager
internal static Color CustomTreeViewComboTextColor => IsDark ? DarkComboText : LightComboText;
// Badge colors for unlockers
internal static Color CreamAPIBadgeBackgroundColor => CreamAPIBadgeBack;
internal static Color CreamAPIBadgeBackgroundHighlightColor => CreamAPIBadgeBackHighlight;
internal static Color CreamAPIBadgeBorderColor => CreamAPIBadgeBorder;
internal static Color SmokeAPIBadgeBackgroundColor => SmokeAPIBadgeBack;
internal static Color SmokeAPIBadgeBackgroundHighlightColor => SmokeAPIBadgeBackHighlight;
internal static Color SmokeAPIBadgeBorderColor => SmokeAPIBadgeBorder;
internal static Color DefaultBadgeBackgroundColor => DefaultBadgeBack;
internal static Color DefaultBadgeBackgroundHighlightColor => DefaultBadgeBackHighlight;
internal static Color DefaultBadgeBorderColor => DefaultBadgeBorder;
// -----------------------------------------------------------------
// Public / Internal API
// -----------------------------------------------------------------