8 Commits

Author SHA1 Message Date
Frog ee19990b5b Logging Infrastructure / Normalize Paths
- Added base logging infrastructure
- Create scan log during library/game scan in %ProgramData%\CreamInstaller\scan.log
- Normalize Steam library paths (libraryfolders.vdf) using Path.GetFullPath + ResolvePath to handle slashes, casing, and drive changes
- Diagnostics.ResolvePath: wrap GetFileSystemInfos in try/catch and guard against empty results to prevent IndexOutOfRangeException (May assist with issues on slow or intermittently accessible external drives)
2026-03-24 00:23:08 -07:00
Frog 39097c27ef Add Dev CI Builds
- Added CI action workflow for dev branch
2026-03-23 23:55:06 -07:00
Frog 3ba4747be3 Increment Version 5.0.2.0
- Increment version number to 5.0.2.0
2026-03-17 11:24:03 -07:00
Frog 322490d0b2 Additional Changes to Correct Null Exception for #12
- Additional changes to prevent Null Exception when catalog mapping property is null.
2026-03-16 22:58:00 -07:00
Frog 3dae7508f0 Merge branch 'main' of https://github.com/FroggMaster/CreamInstaller 2026-03-15 03:15:25 -07:00
Frog 8f8e893e84 Fix NullReferenceException in EpicStore See #12
- Fix a NullReferenceException that could occur when the Epic GraphQL API returns a partial response with missing fields (Data, Catalog, SearchStore, or CatalogOffers).
2026-03-15 03:15:17 -07:00
Frog 0cec730c1e Update README.md
- Additional clarification about DLC files because people apparently can't fucking read.
2026-02-10 12:15:52 -08:00
Frog df7dc0e019 Update create-release.yml
- Added generate_release_notes > Should automatically add the full change-log in the release description.
2026-01-31 03:09:58 -08:00
9 changed files with 171 additions and 21 deletions
+1
View File
@@ -62,6 +62,7 @@ jobs:
tag_name: v${{ inputs.version }}
name: ${{ inputs.title || format('Release v{0}', inputs.version) }}
body: ${{ inputs.notes }}
generate_release_notes: true
files: |
./publish/CreamInstaller.exe
./publish/CreamInstaller.zip
+49
View File
@@ -0,0 +1,49 @@
name: Dev CI Builds
on:
push:
branches:
- dev
workflow_dispatch:
jobs:
build:
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore CreamInstaller.sln
- name: Build Release
run: dotnet build CreamInstaller.sln --configuration Release --no-restore
- name: Publish single-file
run: dotnet publish CreamInstaller.sln -c Release -r win-x64 -p:PublishSingleFile=true --self-contained true --output ./publish
- name: Set short commit SHA and branch name
id: vars
run: |
$shortSha = $env:GITHUB_SHA.Substring(0,7)
$branch = $env:GITHUB_REF_NAME
Write-Output "shortSha=$shortSha" >> $env:GITHUB_ENV
Write-Output "branch=$branch" >> $env:GITHUB_ENV
shell: pwsh
- name: Rename EXE with branch and short commit SHA
run: |
Rename-Item -Path ./publish/CreamInstaller.exe -NewName "CreamInstaller-CI-$env:branch-$env:shortSha.exe"
shell: pwsh
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: CreamInstaller-CI-${{ env.branch }}-${{ env.shortSha }}
path: ./publish/CreamInstaller-CI-${{ env.branch }}-${{ env.shortSha }}.exe
+1 -1
View File
@@ -4,7 +4,7 @@
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
<UseWindowsForms>True</UseWindowsForms>
<ApplicationIcon>Resources\program.ico</ApplicationIcon>
<Version>5.0.1.9</Version>
<Version>5.0.2.0</Version>
<Copyright>2025, FroggMaster (https://github.com/FroggMaster)</Copyright>
<Company>CreamInstaller</Company>
<Product>Automatic DLC Unlocker Installer &amp; Configuration Generator</Product>
+2
View File
@@ -569,6 +569,8 @@ internal sealed partial class SelectForm : CustomForm
progressLabel.Text = "Waiting for user to select which programs/games to scan . . .";
ShowProgressBar();
await ProgramData.Setup(this);
ProgramData.ClearLog();
ProgramData.Log($"[Scan] CreamInstaller {Program.Version} — scan started at {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
bool scan = forceScan;
if (!scan && (programsToScan is null || programsToScan.Count < 1 || forceProvideChoices))
{
+6 -5
View File
@@ -58,13 +58,13 @@ internal static class EpicStore
cacheFile.DeleteFile();
}
if (response is null)
if (response is null || response.Data?.Catalog is null)
return dlcIds;
List<Element> searchStore = [..response.Data.Catalog.SearchStore.Elements];
List<Element> searchStore = [..response.Data.Catalog.SearchStore?.Elements ?? []];
foreach (Element element in searchStore)
{
string title = element.Title;
string product = element.CatalogNs is not null && element.CatalogNs.Mappings.Length > 0
string product = element.CatalogNs?.Mappings is { Length: > 0 }
? element.CatalogNs.Mappings.First().PageSlug
: null;
string icon = null;
@@ -81,11 +81,11 @@ internal static class EpicStore
dlcIds.Populate(item.Id, title, product, icon, null, element.Items.Length == 1);
}
List<Element> catalogOffers = [..response.Data.Catalog.CatalogOffers.Elements];
List<Element> catalogOffers = [..response.Data.Catalog.CatalogOffers?.Elements ?? []];
foreach (Element element in catalogOffers)
{
string title = element.Title;
string product = element.CatalogNs is not null && element.CatalogNs.Mappings.Length > 0
string product = element.CatalogNs?.Mappings is { Length: > 0 }
? element.CatalogNs.Mappings.First().PageSlug
: null;
string icon = null;
@@ -118,6 +118,7 @@ internal static class EpicStore
(string id, string name, string product, string icon, string developer) app = dlcIds[i];
if (app.id != id)
continue;
found = true;
dlcIds[i] = canOverwrite
? (app.id, title ?? app.name, product ?? app.product, icon ?? app.icon, developer ?? app.developer)
+59 -8
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using CreamInstaller.Utility;
@@ -28,16 +29,19 @@ internal static class SteamLibrary
{
List<(string appId, string name, string branch, int buildId, string gameDirectory)> games = new();
HashSet<string> gameLibraryDirectories = await GetLibraryDirectories();
ProgramData.Log($"[Steam] Found {gameLibraryDirectories.Count} library folder(s).");
foreach (string libraryDirectory in gameLibraryDirectories)
{
if (Program.Canceled)
return games;
ProgramData.Log($"[Steam] Scanning library: {libraryDirectory}");
foreach ((string appId, string name, string branch, int buildId, string gameDirectory) game in (await
GetGamesFromLibraryDirectory(
libraryDirectory)).Where(game => games.All(_game => _game.appId != game.appId)))
games.Add(game);
}
ProgramData.Log($"[Steam] Total games detected: {games.Count}");
return games;
});
@@ -47,13 +51,21 @@ internal static class SteamLibrary
{
List<(string appId, string name, string branch, int buildId, string gameDirectory)> games = new();
if (Program.Canceled || !libraryDirectory.DirectoryExists())
{
ProgramData.Log($"[Steam] Skipping library (not found or canceled): {libraryDirectory}");
return games;
}
foreach (string file in libraryDirectory.EnumerateDirectory("*.acf"))
{
if (Program.Canceled)
return games;
if (!ValveDataFile.TryDeserialize(file.ReadFile(), out VProperty result))
{
ProgramData.Log($"[Steam] Failed to deserialize ACF: {file}");
continue;
}
string appId = result.Value.GetChild("appid")?.ToString();
string installdir = result.Value.GetChild("installdir")?.ToString();
string name = result.Value.GetChild("name")?.ToString();
@@ -61,11 +73,23 @@ internal static class SteamLibrary
if (string.IsNullOrWhiteSpace(appId) || string.IsNullOrWhiteSpace(installdir) ||
string.IsNullOrWhiteSpace(name)
|| string.IsNullOrWhiteSpace(buildId))
{
ProgramData.Log($"[Steam] Skipping ACF with missing fields: {file}");
continue;
string gameDirectory = (libraryDirectory + @"\common\" + installdir).ResolvePath();
if (gameDirectory is null || !int.TryParse(appId, out int _) ||
!int.TryParse(buildId, out int buildIdInt) || games.Any(g => g.appId == appId))
}
string rawGameDirectory = libraryDirectory + @"\common\" + installdir;
string gameDirectory = rawGameDirectory.ResolvePath();
if (gameDirectory is null)
{
ProgramData.Log($"[Steam] Game directory not found (drive may be slow or disconnected): {rawGameDirectory} | App: {name} ({appId})");
continue;
}
if (!int.TryParse(appId, out int _) || !int.TryParse(buildId, out int buildIdInt) ||
games.Any(g => g.appId == appId))
continue;
VToken userConfig = result.Value.GetChild("UserConfig");
string branch = userConfig?.GetChild("BetaKey")?.ToString();
branch ??= userConfig?.GetChild("betakey")?.ToString();
@@ -78,6 +102,8 @@ internal static class SteamLibrary
if (string.IsNullOrWhiteSpace(branch))
branch = "public";
ProgramData.Log($"[Steam] Detected game: {name} ({appId}) | Branch: {branch} | Dir: {gameDirectory}");
games.Add((appId, name, branch, buildIdInt, gameDirectory));
}
@@ -92,25 +118,50 @@ internal static class SteamLibrary
return libraryDirectories;
string steamInstallPath = InstallPath;
if (steamInstallPath == null || !steamInstallPath.DirectoryExists())
{
ProgramData.Log($"[Steam] Steam install path not found or inaccessible: {steamInstallPath ?? "(null)"}");
return libraryDirectories;
}
string libraryFolder = steamInstallPath + @"\steamapps";
if (!libraryFolder.DirectoryExists())
{
ProgramData.Log($"[Steam] Default steamapps folder not found: {libraryFolder}");
return libraryDirectories;
}
_ = libraryDirectories.Add(libraryFolder);
ProgramData.Log($"[Steam] Default library folder: {libraryFolder}");
string libraryFolders = libraryFolder + @"\libraryfolders.vdf";
if (!libraryFolders.FileExists() ||
!ValveDataFile.TryDeserialize(libraryFolders.ReadFile(), out VProperty result))
{
ProgramData.Log($"[Steam] libraryfolders.vdf not found or failed to parse: {libraryFolders}");
return libraryDirectories;
}
foreach (VToken vToken in result.Value.Where(p =>
p is VProperty property && int.TryParse(property.Key, out int _)))
{
VProperty property = (VProperty)vToken;
string path = property.Value.GetChild("path")?.ToString();
if (string.IsNullOrWhiteSpace(path))
string rawPath = property.Value.GetChild("path")?.ToString();
if (string.IsNullOrWhiteSpace(rawPath))
continue;
path += @"\steamapps";
if (path.DirectoryExists())
_ = libraryDirectories.Add(path);
// Normalize the path from VDF (may use forward slashes or wrong casing)
string normalizedPath = Path.GetFullPath(rawPath);
string steamappsPath = normalizedPath + @"\steamapps";
string resolvedPath = steamappsPath.ResolvePath();
if (resolvedPath is null)
{
ProgramData.Log($"[Steam] External library not accessible (drive may be disconnected or letter changed): {steamappsPath}");
continue;
}
if (libraryDirectories.Add(resolvedPath))
ProgramData.Log($"[Steam] Additional library folder found: {resolvedPath}");
}
return libraryDirectories;
+11 -2
View File
@@ -54,7 +54,16 @@ internal static class Diagnostics
if (info.Parent is null)
return info.Name.ToUpperInvariant();
string parent = ResolvePath(info.Parent.FullName);
string name = info.Parent.GetFileSystemInfos(info.Name)[0].Name;
return parent is null ? name : Path.Combine(parent, name);
try
{
FileSystemInfo[] infos = info.Parent.GetFileSystemInfos(info.Name);
string name = infos.Length > 0 ? infos[0].Name : info.Name;
return parent is null ? name : Path.Combine(parent, name);
}
catch
{
// Fall back to the raw name if the filesystem call fails (e.g. on a slow external drive)
return parent is null ? info.Name : Path.Combine(parent, info.Name);
}
}
}
+34
View File
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json;
@@ -28,6 +30,38 @@ internal static class ProgramData
private static readonly string DlcChoicesPath = DirectoryPath + @"\dlc.json";
private static readonly string KoaloaderProxyChoicesPath = DirectoryPath + @"\proxies.json";
internal static readonly string LogPath = DirectoryPath + @"\scan.log";
private static readonly object LogLock = new();
internal static void Log(string message)
{
try
{
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);
}
catch
{
// ignored — logging must never crash the application
}
}
internal static void ClearLog()
{
try
{
if (File.Exists(LogPath))
File.Delete(LogPath);
}
catch
{
// ignored
}
}
internal static async Task Setup(Form form = null)
=> await Task.Run(() =>
{
+8 -5
View File
@@ -74,14 +74,17 @@ If none of these work, your system may not support .NET 8 or may have underlying
### DLCs aren't unlocking in my game
CreamInstaller only installs the unlockers — it does **not** guarantee they will work for every game.
CreamInstaller only installs DLC **unlockers** — it does **not** guarantee they will work for every game.
If the program successfully installed the unlockers but DLCs still arent unlocking, this is **not an issue with CreamInstaller itself** and isnt something I can directly fix.
If the program successfully installs the unlockers but DLCs still arent unlocking, this is **not an issue with CreamInstaller itself** and isnt something I can directly fix. DLC Unlocker compatibility and behavior vary from game to game.
Try the following:
**DLC Files:** _This program does **not** automatically download or install actual DLC files for you. As the name implies, it is only a *DLC unlocker installer*. If the game you wish to unlock DLC for does not already include the DLC files (which is the case for many games), you must manually obtain and install those files yourself. This includes manually installing new DLCs and manually updating or reinstalling previously installed DLCs after game updates._
If youre having trouble, try the following:
- Review the [Usage section](https://github.com/FroggMaster/CreamInstaller#usage) for proper setup
- Visit the [CS.RIN.RU forum](https://cs.rin.ru/forum/viewforum.php?f=10) for game-specific troubleshooting and compatibility info
- Review the [Usage section](https://github.com/FroggMaster/CreamInstaller#usage) for proper setup
- Visit the [CS.RIN.RU forum](https://cs.rin.ru/forum/viewforum.php?f=10) for game-specific troubleshooting
---