Improve Logging / Add Forced Proxy for Steam Games with no SteamAPI DLL Closes #28

- Adds logic to allow for a Steam Game with no SteamAPI dll to use forced proxy methods. (This is not guranteed to work and thus these games are tagged with the "Proxy Only" platform tag.
- Adds tooltips, and displays them for Unsupported/Proxy Only games if hovered with the mouse.
- Adds improved logging to the application, breaks logs into 3 primary logs: game-scan.log, cream-steamcmd.log and CreamInstaller.log
This commit is contained in:
Frog
2026-06-12 23:53:20 -07:00
parent 55120e4640
commit dea8a24c30
14 changed files with 191 additions and 197 deletions
+14 -2
View File
@@ -34,7 +34,8 @@ internal sealed class CustomTreeView : TreeView
internal CustomTreeView()
{
DrawMode = TreeViewDrawMode.OwnerDrawAll;
ShowNodeToolTips = true;
DrawMode = TreeViewDrawMode.OwnerDrawAll;
Invalidated += OnInvalidated;
DrawNode += DrawTreeNode;
Disposed += OnDisposed;
@@ -218,7 +219,16 @@ internal sealed class CustomTreeView : TreeView
SelectionDLC dlc = SelectionDLC.FromId(dlcType, node.Parent?.Name, id);
text = dlc?.Selection != null ? dlc.Selection.Platform.ToString() : dlcType.ToString();
}
else text = platform.ToString();
else
{
text = platform.ToString();
if (platform is Platform.Steam)
{
Selection selection = Selection.FromId(platform, id);
if (selection is not null && selection.SteamApiDllMissing)
text = "Proxy Only";
}
}
Size size = TextRenderer.MeasureText(graphics, text, font);
bounds = bounds with { X = bounds.X + bounds.Width, Width = size.Width };
@@ -485,6 +495,8 @@ internal sealed class CustomTreeView : TreeView
_ = checkBoxBounds.Remove(pair.Key);
else if (pair.Value.Contains(clickPoint))
{
if (pair.Key.SteamApiDllMissing)
return;
pair.Key.UseProxy = !pair.Key.UseProxy;
selectForm?.OnProxyChanged();
break;
+9
View File
@@ -11,6 +11,8 @@ internal sealed partial class DebugForm : CustomForm
private static DebugForm current;
private static readonly object currentLock = new();
internal static bool IsOpen { get; private set; }
private Form attachedForm;
private DebugForm()
@@ -62,6 +64,13 @@ internal sealed partial class DebugForm : CustomForm
attachedForm.SizeChanged += OnChange;
attachedForm.VisibleChanged += OnChange;
UpdateAttachment();
if (!IsOpen)
{
IsOpen = true;
ProgramData.OnLogWarning += msg => Log(msg, LogTextBox.Warning);
ProgramData.OnLogError += msg => Log(msg, LogTextBox.Error);
}
}
private void OnChange(object sender, EventArgs args) => UpdateAttachment();
+25 -9
View File
@@ -189,11 +189,16 @@ internal sealed partial class SelectForm : CustomForm
return;
HashSet<string> dllDirectories =
await gameDirectory.GetDllDirectoriesFromGameDirectory(Platform.Steam);
if (dllDirectories is null)
bool steamApiDllMissing = dllDirectories is null;
if (steamApiDllMissing)
{
_ = Interlocked.Decrement(ref steamGamesToCheck);
RemoveFromRemainingGames(name);
return;
dllDirectories = [];
if (uninstallAll)
{
_ = Interlocked.Decrement(ref steamGamesToCheck);
RemoveFromRemainingGames(name);
return;
}
}
if (uninstallAll)
@@ -348,6 +353,20 @@ internal sealed partial class SelectForm : CustomForm
Selection selection = Selection.GetOrCreate(Platform.Steam, appId, storeAppData?.Name ?? name,
gameDirectory, dllDirectories,
await gameDirectory.GetExecutableDirectories(true));
selection.SteamApiDllMissing = steamApiDllMissing;
if (steamApiDllMissing)
{
selection.UseProxy = true;
bool has64 = selection.ExecutableDirectories.Any(d => d.binaryType == BinaryType.BIT64);
bool has32 = selection.ExecutableDirectories.Any(d => d.binaryType == BinaryType.BIT32);
string dllName = (has64, has32) switch
{
(true, true) => "steam_api.dll / steam_api64.dll",
(true, false) => "steam_api64.dll",
_ => "steam_api.dll"
};
selection.TreeNode.ToolTipText = dllName + " was not detected in the game directory. Only proxy installation is available.";
}
selection.Product = "https://store.steampowered.com/app/" + appId;
selection.Icon = IconGrabber.SteamAppImagesPath + @$"\{appId}\{cmdAppData?.Common?.Icon}.jpg";
selection.SubIcon = storeAppData?.HeaderImage ?? IconGrabber.SteamAppImagesPath
@@ -717,10 +736,7 @@ internal sealed partial class SelectForm : CustomForm
}
catch (Exception ex)
{
// Handle exceptions in async void to prevent unobserved exceptions
#if DEBUG
System.Diagnostics.Debug.WriteLine($"OnLoad exception: {ex.Message}");
#endif
ProgramData.LogError("SelectForm OnLoad failed", ex);
// Show error and clean up
ex.HandleException(this);
HideProgressBar();
@@ -1263,7 +1279,7 @@ internal sealed partial class SelectForm : CustomForm
selection.Proxy = currentProxy == Selection.DefaultProxy ? currentProxy : proxy;
}
}
else
else if (!selection.SteamApiDllMissing)
{
selection.UseProxy = false;
selection.Proxy = null;
+2 -2
View File
@@ -92,7 +92,7 @@ internal sealed partial class TestGameForm : CustomForm
if (!string.IsNullOrWhiteSpace(title))
return title;
}
catch { /* fall through to SteamCMD */ }
catch (Exception ex) { ProgramData.LogWarning($"[TestGame] Store name lookup failed for AppID {appId}: {ex.Message}"); /* fall through to SteamCMD */ }
CmdAppData cmdData = await SteamCMD.GetAppInfo(appId);
return cmdData?.Common?.Name;
@@ -320,7 +320,7 @@ internal sealed partial class TestGameForm : CustomForm
SteamLibrary.TestGames.Clear();
EpicLibrary.TestManifests.Clear();
foreach (string dir in CreatedDirectories)
try { Directory.Delete(dir, true); } catch { /* best-effort */ }
try { Directory.Delete(dir, true); } catch (Exception ex) { ProgramData.LogWarning($"[TestGame] Cleanup deletion failed for {dir}: {ex.Message}"); }
CreatedDirectories.Clear();
dlcEntries.Clear();
RefreshDlcList();
+2 -7
View File
@@ -106,12 +106,10 @@ internal sealed partial class UpdateForm : CustomForm
}
catch (Exception ex)
{
// Handle exceptions in async void to prevent unobserved exceptions
ProgramData.LogError("UpdateForm OnLoad failed", ex);
#if DEBUG
System.Diagnostics.Debug.WriteLine($"OnLoad exception: {ex.Message}");
ex.HandleFatalException();
#else
// In release, try to continue gracefully
StartProgram();
#endif
}
@@ -262,10 +260,7 @@ internal sealed partial class UpdateForm : CustomForm
}
catch (Exception ex)
{
// Handle exceptions in async void event handler to prevent unobserved exceptions
#if DEBUG
System.Diagnostics.Debug.WriteLine($"OnUpdate exception: {ex.Message}");
#endif
ProgramData.LogError("UpdateForm OnUpdate failed", ex);
// Show error to user
ex.HandleException(this, Program.Name + " encountered an unexpected error during update");
StartProgram();
+3 -9
View File
@@ -8,9 +8,7 @@ using CreamInstaller.Platforms.Epic.GraphQL;
using CreamInstaller.Utility;
using Newtonsoft.Json;
#if DEBUG
using CreamInstaller.Forms;
#endif
namespace CreamInstaller.Platforms.Epic;
@@ -33,12 +31,10 @@ internal static class EpicStore
if (!cachedExists || ProgramData.CheckCooldown(categoryNamespace, Cooldown))
{
response = await QueryGraphQL(categoryNamespace);
#if DEBUG
if (response is null)
{
DebugForm.Current.Log("ES: QueryGraphQL returned null");
ProgramData.LogWarning("Epic QueryGraphQL returned null for " + categoryNamespace);
}
#endif
try
{
cacheFile.WriteFile(JsonConvert.SerializeObject(response, Formatting.Indented));
@@ -191,9 +187,7 @@ internal static class EpicStore
HttpClient client = HttpClientManager.HttpClient;
if (client is null)
{
#if DEBUG
DebugForm.Current.Log("ES: Client returned null");
#endif
ProgramData.LogWarning("Epic GraphQL client returned null");
return null;
}
HttpResponseMessage httpResponse =
@@ -2,7 +2,6 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CreamInstaller.Forms;
using CreamInstaller.Utility;
using Newtonsoft.Json;
@@ -38,61 +37,38 @@ internal static partial class SteamCMD
{
cacheFile.WriteFile(JsonConvert.SerializeObject(data, Formatting.Indented));
}
catch
#if DEBUG
(Exception e)
catch (Exception e)
{
DebugForm.Current.Log("SteamCMD web API query failed on attempt #" + attempts +
" for " + appId + (isDlc ? " (DLC)" : "")
+ ": Unsuccessful serialization (" + e.Message + ")");
ProgramData.LogSteamCmd("SteamCMD web API query failed on attempt #" + attempts +
" for " + appId + (isDlc ? " (DLC)" : "")
+ ": Unsuccessful serialization (" + e.Message + ")");
}
#else
{
// ignored
}
#endif
return data;
}
#if DEBUG
else
DebugForm.Current.Log(
ProgramData.LogSteamCmd(
"SteamCMD web API query failed on attempt #" + attempts + " for " + appId +
(isDlc ? " (DLC)" : "")
+ ": No data",
LogTextBox.Warning);
#endif
+ ": No data");
}
#if DEBUG
else
DebugForm.Current.Log(
ProgramData.LogSteamCmd(
"SteamCMD web API query failed on attempt #" + attempts + " for " + appId +
(isDlc ? " (DLC)" : "")
+ ": Status not success (" + appDetails?.Status + ")",
LogTextBox.Warning);
#endif
+ ": Status not success (" + appDetails?.Status + ")");
}
catch
#if DEBUG
(Exception e)
catch (Exception e)
{
DebugForm.Current.Log("SteamCMD web API query failed on attempt #" + attempts + " for " +
appId + (isDlc ? " (DLC)" : "")
+ ": Unsuccessful deserialization (" + e.Message + ")");
ProgramData.LogSteamCmd("SteamCMD web API query failed on attempt #" + attempts + " for " +
appId + (isDlc ? " (DLC)" : "")
+ ": Unsuccessful deserialization (" + e.Message + ")");
}
#else
{
// ignored
}
#endif
}
#if DEBUG
else
DebugForm.Current.Log(
ProgramData.LogSteamCmd(
"SteamCMD web API query failed on attempt #" + attempts + " for " + appId +
(isDlc ? " (DLC)" : "") +
": Response null",
LogTextBox.Warning);
#endif
": Response null");
}
if (cachedExists)
@@ -109,9 +85,7 @@ internal static partial class SteamCMD
break;
if (attempts > 10)
{
#if DEBUG
DebugForm.Current.Log("Failed to query SteamCMD web API after 10 tries: " + appId);
#endif
ProgramData.LogSteamCmd("Failed to query SteamCMD web API after 10 tries: " + appId);
break;
}
+12 -35
View File
@@ -12,9 +12,6 @@ using CreamInstaller.Resources;
using CreamInstaller.Utility;
using Gameloop.Vdf.JsonConverter;
using Gameloop.Vdf.Linq;
#if DEBUG
using CreamInstaller.Forms;
#endif
namespace CreamInstaller.Platforms.Steam;
@@ -210,10 +207,7 @@ internal static partial class SteamCMD
attempts++;
if (attempts > 10)
{
#if DEBUG
DebugForm.Current.Log("Failed to query SteamCMD after 10 tries: " + appId + " (" + branch + ")",
LogTextBox.Warning);
#endif
ProgramData.LogSteamCmd("Failed to query SteamCMD after 10 tries: " + appId + " (" + branch + ")");
break;
}
@@ -232,12 +226,9 @@ internal static partial class SteamCMD
}
else
{
#if DEBUG
DebugForm.Current.Log(
ProgramData.LogSteamCmd(
"SteamCMD query failed on attempt #" + attempts + " for " + appId + " (" + branch +
"): Bad output",
LogTextBox.Warning);
#endif
"): Bad output");
continue;
}
}
@@ -245,12 +236,9 @@ internal static partial class SteamCMD
if (!ValveDataFile.TryDeserialize(output, out VProperty appInfo) || appInfo.Value is VValue)
{
appUpdateFile.DeleteFile();
#if DEBUG
DebugForm.Current.Log(
ProgramData.LogSteamCmd(
"SteamCMD query failed on attempt #" + attempts + " for " + appId + " (" + branch +
"): Deserialization failed",
LogTextBox.Warning);
#endif
"): Deserialization failed");
continue;
}
@@ -260,29 +248,20 @@ internal static partial class SteamCMD
if (appInfo.ToJson().Value.ToObject<CmdAppData>() is not { } cmdAppData)
{
appUpdateFile.DeleteFile();
#if DEBUG
DebugForm.Current.Log(
ProgramData.LogSteamCmd(
"SteamCMD query failed on attempt #" + attempts + " for " + appId + " (" + branch +
"): VDF-JSON conversion failed",
LogTextBox.Warning);
#endif
"): VDF-JSON conversion failed");
continue;
}
appData = cmdAppData;
}
catch
#if DEBUG
(Exception e)
#endif
catch (Exception e)
{
appUpdateFile.DeleteFile();
#if DEBUG
DebugForm.Current.Log(
ProgramData.LogSteamCmd(
"SteamCMD query failed on attempt #" + attempts + " for " + appId + " (" + branch +
"): VDF-JSON conversion failed (" + e.Message + ")",
LogTextBox.Warning);
#endif
"): VDF-JSON conversion failed (" + e.Message + ")");
continue;
}
@@ -300,11 +279,9 @@ internal static partial class SteamCMD
foreach (string dlcAppUpdateFile in dlcAppIds.Select(id => $@"{AppInfoPath}\{id}.vdf"))
dlcAppUpdateFile.DeleteFile();
appUpdateFile.DeleteFile();
#if DEBUG
DebugForm.Current.Log(
ProgramData.LogSteamCmd(
"SteamCMD query skipped on attempt #" + attempts + " for " + appId + " (" + branch +
"): Outdated cache", LogTextBox.Warning);
#endif
"): Outdated cache");
}
return null;
+13 -46
View File
@@ -3,13 +3,10 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System;
using CreamInstaller.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
#if DEBUG
using System;
using CreamInstaller.Forms;
#endif
namespace CreamInstaller.Platforms.Steam;
@@ -18,7 +15,6 @@ internal static class SteamStore
private const int CooldownGame = 600;
private const int CooldownDlc = 1200;
#if DEBUG
private static string FormatErrorLog(int attempts, string appId, string gameName, bool isDlc, string reason,
string parentGameName = null, string parentGameAppId = null)
{
@@ -35,7 +31,6 @@ internal static class SteamStore
string type = isDlc ? "DLC" : "Game";
return $"[SteamQuery][Attempt {attempts}][FAILED] AppId: {appId} | Name: \"{gameName}\" | Type: {type} | Reason: {reason}";
}
#endif
internal static async Task<HashSet<string>> ParseDlcAppIds(StoreAppData storeAppData)
=> await Task.Run(() =>
@@ -80,11 +75,8 @@ internal static class SteamStore
if (!storeAppDetails.Success)
{
#if DEBUG
DebugForm.Current.Log(
FormatErrorLog(attempts, appId, gameName, isDlc, "Query unsuccessful", parentGameName, parentGameAppId),
LogTextBox.Warning);
#endif
ProgramData.LogSteamCmd(
FormatErrorLog(attempts, appId, gameName, isDlc, "Query unsuccessful", parentGameName, parentGameAppId));
if (data is null)
return null;
}
@@ -95,61 +87,38 @@ internal static class SteamStore
{
cacheFile.WriteFile(JsonConvert.SerializeObject(data, Formatting.Indented));
}
catch
#if DEBUG
(Exception e)
catch (Exception e)
{
DebugForm.Current.Log(
ProgramData.LogSteamCmd(
FormatErrorLog(attempts, appId, gameName, isDlc, $"Unsuccessful serialization ({e.Message})", parentGameName, parentGameAppId));
}
#else
{
// ignored
}
#endif
return data;
}
#if DEBUG
DebugForm.Current.Log(
ProgramData.LogSteamCmd(
FormatErrorLog(attempts, appId, gameName, isDlc, "Response data null", parentGameName, parentGameAppId));
#endif
}
#if DEBUG
else
{
DebugForm.Current.Log(
ProgramData.LogSteamCmd(
FormatErrorLog(attempts, appId, gameName, isDlc, "Response details null", parentGameName, parentGameAppId));
}
#endif
}
catch
#if DEBUG
(Exception e)
catch (Exception e)
{
DebugForm.Current.Log(
ProgramData.LogSteamCmd(
FormatErrorLog(attempts, appId, gameName, isDlc, $"Unsuccessful deserialization ({e.Message})", parentGameName, parentGameAppId));
}
#else
{
// ignored
}
#endif
#if DEBUG
else
{
DebugForm.Current.Log(
ProgramData.LogSteamCmd(
FormatErrorLog(attempts, appId, gameName, isDlc, "Response deserialization null", parentGameName, parentGameAppId));
}
#endif
}
#if DEBUG
else
{
DebugForm.Current.Log(
FormatErrorLog(attempts, appId, gameName, isDlc, "Null or empty response", parentGameName, parentGameAppId),
LogTextBox.Warning);
ProgramData.LogSteamCmd(
FormatErrorLog(attempts, appId, gameName, isDlc, "Null or empty response", parentGameName, parentGameAppId));
}
#endif
}
if (cachedExists)
@@ -166,10 +135,8 @@ internal static class SteamStore
break;
if (attempts > 10)
{
#if DEBUG
DebugForm.Current.Log(
ProgramData.LogSteamCmd(
FormatErrorLog(attempts, appId, gameName, isDlc, "Maximum retry attempts exceeded (10)", parentGameName, parentGameAppId));
#endif
break;
}
+3 -11
View File
@@ -130,10 +130,7 @@ internal static class Program
}
catch (Exception ex)
{
#if DEBUG
System.Diagnostics.Debug.WriteLine($"Cleanup failed: {ex.Message}");
#endif
// Swallow exceptions during fire-and-forget cleanup
ProgramData.LogWarning($"Cleanup failed: {ex.Message}");
}
});
}
@@ -149,17 +146,12 @@ internal static class Program
// Wait up to 5 seconds for graceful cleanup
if (!cleanupTask.Wait(TimeSpan.FromSeconds(5)))
{
#if DEBUG
System.Diagnostics.Debug.WriteLine("Cleanup timed out during application exit");
#endif
ProgramData.LogWarning("Cleanup timed out during application exit");
}
}
catch (Exception ex)
{
#if DEBUG
System.Diagnostics.Debug.WriteLine($"Cleanup exception during exit: {ex.Message}");
#endif
// Ignore exceptions during shutdown
ProgramData.LogWarning($"Cleanup exception during exit: {ex.Message}");
}
finally
{
+25 -3
View File
@@ -45,6 +45,7 @@ internal sealed class Selection : IEquatable<Selection>
internal string SubIcon;
internal string Website;
internal InstalledUnlocker InstalledUnlocker;
internal bool SteamApiDllMissing;
internal IEnumerable<string> GetAvailableProxies()
{
@@ -125,9 +126,12 @@ internal sealed class Selection : IEquatable<Selection>
return;
}
_ = DllDirectories.RemoveWhere(directory => !directory.DirectoryExists());
if (DllDirectories.Count < 1)
Remove();
if (!SteamApiDllMissing)
{
_ = DllDirectories.RemoveWhere(directory => !directory.DirectoryExists());
if (DllDirectories.Count < 1)
Remove();
}
}
internal static void ValidateAll(List<(Platform platform, string id, string name)> programsToScan)
@@ -199,6 +203,24 @@ internal sealed class Selection : IEquatable<Selection>
proxy.FileExists() && proxy.IsResourceFile(ResourceIdentifier.Koaloader))
|| config.FileExists())
return InstalledUnlocker.Koaloader;
if (Platform is Platform.Steam or Platform.Paradox)
{
directory.GetSmokeApiComponents(out _, out _, out _, out _, out _, out string smokeConfig, out _, out _, out _);
if (smokeConfig.FileExists())
return InstalledUnlocker.SmokeAPI;
directory.GetCreamApiComponents(out _, out _, out _, out _, out string creamConfig);
if (creamConfig.FileExists())
return InstalledUnlocker.CreamAPI;
if (directory.GetSmokeApiProxies().Any(proxy =>
proxy.FileExists() && (proxy.IsResourceFile(ResourceIdentifier.Steamworks32) ||
proxy.IsResourceFile(ResourceIdentifier.Steamworks64))))
return InstalledUnlocker.SmokeAPI;
if (directory.GetCreamApiProxies().Any(proxy =>
proxy.FileExists() && (proxy.IsResourceFile(ResourceIdentifier.Steamworks32) ||
proxy.IsResourceFile(ResourceIdentifier.Steamworks64))))
return InstalledUnlocker.CreamAPI;
}
}
return InstalledUnlocker.None;
+6 -24
View File
@@ -5,9 +5,7 @@ using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
#if DEBUG
using CreamInstaller.Forms;
#endif
namespace CreamInstaller.Utility;
@@ -87,44 +85,28 @@ internal static class HttpClientManager
{
if (e.StatusCode != HttpStatusCode.TooManyRequests)
{
#if DEBUG
string statusInfo = e.StatusCode.HasValue ? $" (HTTP {(int)e.StatusCode.Value})" : "";
DebugForm.Current.Log($"Get request failed to {url}{statusInfo}: {e}", LogTextBox.Warning);
#endif
ProgramData.LogWarning($"Get request failed to {url}{statusInfo}: {e.Message}");
return null;
}
#if DEBUG
DebugForm.Current.Log($"Too many requests to {url} (HTTP 429 - Rate Limited)", LogTextBox.Error);
#endif
// do something special?
ProgramData.LogWarning($"Too many requests to {url} (HTTP 429 - Rate Limited)");
return null;
}
catch (TaskCanceledException)
{
#if DEBUG
DebugForm.Current.Log("Get request timed out for " + url, LogTextBox.Warning);
#endif
ProgramData.LogWarning("Get request timed out for " + url);
return null;
}
catch (OperationCanceledException)
{
#if DEBUG
DebugForm.Current.Log("Get request was cancelled for " + url, LogTextBox.Warning);
#endif
ProgramData.LogWarning("Get request was cancelled for " + url);
return null;
}
#if DEBUG
catch (Exception e)
{
DebugForm.Current.Log("Get request failed to " + url + ": " + e, LogTextBox.Warning);
ProgramData.LogWarning("Get request failed to " + url + ": " + e.Message);
return null;
}
#else
catch
{
return null;
}
#endif
}
internal static async Task<Image> GetImageFromUrl(string url)
+58 -4
View File
@@ -64,7 +64,12 @@ internal static class ProgramData
private static readonly string ExtraProtectionChoicesPath = DirectoryPath + @"\extraprotection.json";
private static readonly string InstalledGamesPath = DirectoryPath + @"\installed.json";
internal static readonly string LogPath = DirectoryPath + @"\scan.log";
internal static readonly string ScanLogPath = Path.Combine(DirectoryPath, "game-scan.log");
internal static readonly string SteamCmdLogPath = Path.Combine(DirectoryPath, "cream-steamcmd.log");
internal static readonly string AppLogPath = Path.Combine(DirectoryPath, "CreamInstaller.log");
internal static event Action<string> OnLogWarning;
internal static event Action<string> OnLogError;
private static readonly object LogLock = new();
@@ -75,7 +80,7 @@ internal static class ProgramData
string timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
string entry = $"[{timestamp}] {message}{Environment.NewLine}";
lock (LogLock)
File.AppendAllText(LogPath, entry, Encoding.UTF8);
File.AppendAllText(ScanLogPath, entry, Encoding.UTF8);
}
catch
{
@@ -83,12 +88,61 @@ internal static class ProgramData
}
}
internal static void LogSteamCmd(string message)
{
try
{
string timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
string entry = $"[{timestamp}] [SteamCMD] {message}{Environment.NewLine}";
lock (LogLock)
File.AppendAllText(SteamCmdLogPath, entry, Encoding.UTF8);
}
catch
{
// ignored — logging must never crash the application
}
}
internal static void LogWarning(string message)
{
try
{
string timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
string entry = $"[{timestamp}] [WARN] {message}{Environment.NewLine}";
lock (LogLock)
File.AppendAllText(AppLogPath, entry, Encoding.UTF8);
}
catch
{
// ignored — logging must never crash the application
}
OnLogWarning?.Invoke(message);
}
internal static void LogError(string message, Exception ex = null)
{
try
{
string timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
string entry = ex is not null
? $"[{timestamp}] [ERROR] {message}{Environment.NewLine}[{timestamp}] [ERROR] Exception: {ex}{Environment.NewLine}"
: $"[{timestamp}] [ERROR] {message}{Environment.NewLine}";
lock (LogLock)
File.AppendAllText(AppLogPath, entry, Encoding.UTF8);
}
catch
{
// ignored — logging must never crash the application
}
OnLogError?.Invoke(message);
}
internal static void ClearLog()
{
try
{
if (File.Exists(LogPath))
File.Delete(LogPath);
if (File.Exists(ScanLogPath))
File.Delete(ScanLogPath);
}
catch
{
+4 -4
View File
@@ -335,7 +335,7 @@ internal static class ThemeManager
int useDark = IsDark ? 1 : 0;
NativeMethods.EnableDarkTitleBar(form.Handle, useDark);
}
catch { }
catch (Exception ex) { ProgramData.LogWarning($"[Theme] Title bar theming failed: {ex.Message}"); }
}
private static void TryApplyScrollbarTheme(Control control, bool dark)
@@ -345,7 +345,7 @@ internal static class ThemeManager
string theme = dark ? "DarkMode_Explorer" : null;
NativeImports.SetWindowTheme(control.Handle, theme, null);
}
catch { }
catch (Exception ex) { ProgramData.LogWarning($"[Theme] Scrollbar theming failed: {ex.Message}"); }
}
// -----------------------------------------------------------------
@@ -454,7 +454,7 @@ internal static class ThemeManager
// button is centralized here so theming resides in ThemeManager.
// -----------------------------------------------------------------
// Dark checkbox colors matched to how the system renders the "All" CheckBox control
// Dark checkbox colors matched to how the system renders the "All" CheckBox control
// in dark mode: dark fill, mid-gray border, light foreground tick.
private static readonly Color DarkCbBorder = ColorTranslator.FromHtml("#6B6B6B");
private static readonly Color DarkCbDisabledBorder = ColorTranslator.FromHtml("#454545");
@@ -478,7 +478,7 @@ internal static class ThemeManager
if (isChecked && enabled)
{
// Checked + enabled: accent fill, no border, white tick matches Windows 11 dark CheckBox
// Checked + enabled: accent fill, no border, white tick matches Windows 11 dark CheckBox
using SolidBrush fillBrush = new(Accent);
g.FillPath(fillBrush, path);