mirror of
https://github.com/FroggMaster/CreamInstaller.git
synced 2026-06-12 19:11:25 -07:00
Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9f8222d8e | |||
| 68842aad9f | |||
| 1d5dc4ac8c | |||
| 31ca8a947f | |||
| 558612f098 | |||
| b7067c2621 | |||
| fe55efc072 | |||
| 39097c27ef | |||
| 3ba4747be3 | |||
| 322490d0b2 | |||
| 3dae7508f0 | |||
| 8f8e893e84 | |||
| 0cec730c1e | |||
| df7dc0e019 | |||
| 455a290051 | |||
| 6e8326b84f | |||
| f20ca0d833 | |||
| 788e7f5293 | |||
| ce566cfa47 | |||
| d2a5549878 | |||
| e79aecc023 | |||
| 4b9897bde2 | |||
| 034951e4d2 | |||
| 4075078790 | |||
| 46df791c19 | |||
| b26a5aec48 | |||
| e824ebd713 | |||
| d723d1c0c7 | |||
| 956b6d0c1c | |||
| 028dd1586b | |||
| f5d6007404 | |||
| 1c7ffb215d | |||
| 1bd5501869 | |||
| 1db70541f9 | |||
| ae08e990cc | |||
| 12c7c9a9d2 | |||
| 701ca5627d | |||
| c0af3b85bb | |||
| 8577e6df7f | |||
| 961b7153f4 | |||
| e640b8b15d | |||
| bcf3ff84fe | |||
| eb1fee38f3 | |||
| cffc4cce07 | |||
| b7a9505599 | |||
| 094d60b003 | |||
| 09cafa27fb | |||
| e6fa7b4a39 | |||
| 8a24bdad81 | |||
| 21bcfae688 | |||
| 4063e482dd | |||
| bdb1d9ffd2 | |||
| 800cb2b9f6 | |||
| 8b6013e1c0 | |||
| 668f367838 | |||
| 6613b777a7 | |||
| 1036f8a8b4 | |||
| c5a3a98827 | |||
| 4751a3bf76 | |||
| de43eb9561 | |||
| 6931e43874 | |||
| 8958c0626f | |||
| 63d6879465 | |||
| 14d71b1746 | |||
| 7bf5276109 | |||
| 637015a509 | |||
| b45b5c6570 | |||
| 27ebdd8331 | |||
| 3f910b9d07 | |||
| c8a65f35bf | |||
| 12e9a4b66f | |||
| 0d6243956d | |||
| 8d5e2487d4 |
@@ -1,10 +1,40 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Report a program exception or general bug, not including those explained within the FAQ and/or template issues.
|
||||
title: ''
|
||||
about: Report a program exception or general bug (not covered in FAQ or existing issues)
|
||||
title: '[Bug] - '
|
||||
labels: Bug
|
||||
assignees: pointfeev
|
||||
assignees: FroggMaster
|
||||
|
||||
---
|
||||
|
||||
###### Describe the bug and/or provide an image of the exception dialog box:
|
||||
## Bug Description
|
||||
<!-- Provide a clear and concise description of what the bug is -->
|
||||
|
||||
|
||||
## Steps to Reproduce
|
||||
<!-- How can the issue be reproduced? -->
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
|
||||
## Exception Details
|
||||
<!-- If you received an error dialog, provide a screenshot or paste the full error message below -->
|
||||
<details>
|
||||
<summary>Click to expand error message/screenshot</summary>
|
||||
|
||||
```
|
||||
Paste error text here, or drag and drop screenshot below
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## Generated Config
|
||||
<!-- If a configuration was generated, please provide the configuration file -->
|
||||
|
||||
|
||||
|
||||
## Affected Version
|
||||
<!-- Please specify the version of the application you experience the issue with -->
|
||||
- **CreamInstaller Version:** [e.g. v5.0.1.7, CI build #21]
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
name: CI Builds
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
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
|
||||
id: vars
|
||||
run: |
|
||||
$shortSha = $env:GITHUB_SHA.Substring(0,7)
|
||||
Write-Output "shortSha=$shortSha" >> $env:GITHUB_ENV
|
||||
shell: pwsh
|
||||
|
||||
- name: Rename EXE with short commit SHA
|
||||
run: |
|
||||
Rename-Item -Path ./publish/CreamInstaller.exe -NewName "CreamInstaller-CI-$env:shortSha.exe"
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CreamInstaller-CI-Release-${{ env.shortSha }}
|
||||
path: ./publish/CreamInstaller-CI-${{ env.shortSha }}.exe
|
||||
@@ -0,0 +1,70 @@
|
||||
name: Create Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version number (example: 5.0.1.3)"
|
||||
required: true
|
||||
type: string
|
||||
title:
|
||||
description: "Release title (optional)"
|
||||
required: false
|
||||
type: string
|
||||
notes:
|
||||
description: "Release notes (optional)"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Needed for tag creation and pushing
|
||||
|
||||
- name: Create and push tag
|
||||
shell: pwsh
|
||||
run: |
|
||||
git config user.name "github-actions"
|
||||
git config user.email "github-actions@github.com"
|
||||
|
||||
$tag = "v${{ inputs.version }}"
|
||||
Write-Host "Creating tag $tag"
|
||||
|
||||
git tag $tag
|
||||
git push origin $tag
|
||||
|
||||
- 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: Zip Release
|
||||
run: |
|
||||
Compress-Archive -Path ./publish/* -DestinationPath ./publish/CreamInstaller.zip
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
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
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -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
|
||||
@@ -44,95 +44,85 @@ internal sealed class ContextMenuItem : ToolStripMenuItem
|
||||
}
|
||||
|
||||
private static async Task TryImageIdentifier(ContextMenuItem item, string imageIdentifier)
|
||||
=> await Task.Run(async () =>
|
||||
{
|
||||
if (Images.TryGetValue(imageIdentifier, out Image image) && image is not null)
|
||||
{
|
||||
if (Images.TryGetValue(imageIdentifier, out Image image) && image is not null)
|
||||
item.Image = image;
|
||||
else
|
||||
item.Image = image;
|
||||
return;
|
||||
}
|
||||
|
||||
image = await Task.Run(async () =>
|
||||
{
|
||||
switch (imageIdentifier)
|
||||
{
|
||||
switch (imageIdentifier)
|
||||
{
|
||||
case "Paradox Launcher":
|
||||
if (ParadoxLauncher.InstallPath.DirectoryExists())
|
||||
foreach (string file in ParadoxLauncher.InstallPath.EnumerateDirectory("*.exe"))
|
||||
{
|
||||
image = file.GetFileIconImage();
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case "Notepad":
|
||||
image = IconGrabber.GetNotepadImage();
|
||||
break;
|
||||
case "Command Prompt":
|
||||
image = IconGrabber.GetCommandPromptImage();
|
||||
break;
|
||||
case "File Explorer":
|
||||
image = IconGrabber.GetFileExplorerImage();
|
||||
break;
|
||||
case "SteamDB":
|
||||
image = await HttpClientManager.GetImageFromUrl(
|
||||
IconGrabber.GetDomainFaviconUrl("steamdb.info"));
|
||||
break;
|
||||
case "Steam Store":
|
||||
image = await HttpClientManager.GetImageFromUrl(
|
||||
IconGrabber.GetDomainFaviconUrl("store.steampowered.com"));
|
||||
break;
|
||||
case "Steam Community":
|
||||
image = await HttpClientManager.GetImageFromUrl(
|
||||
IconGrabber.GetDomainFaviconUrl("steamcommunity.com"));
|
||||
break;
|
||||
case "ScreamDB":
|
||||
image = await HttpClientManager.GetImageFromUrl(
|
||||
IconGrabber.GetDomainFaviconUrl("scream-db.web.app"));
|
||||
break;
|
||||
case "Epic Games":
|
||||
image = await HttpClientManager.GetImageFromUrl(
|
||||
IconGrabber.GetDomainFaviconUrl("epicgames.com"));
|
||||
break;
|
||||
case "Ubisoft Store":
|
||||
image = await HttpClientManager.GetImageFromUrl(
|
||||
IconGrabber.GetDomainFaviconUrl("store.ubi.com"));
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (image is not null)
|
||||
{
|
||||
Images[imageIdentifier] = image;
|
||||
item.Image = image;
|
||||
}
|
||||
case "Paradox Launcher":
|
||||
if (ParadoxLauncher.InstallPath.DirectoryExists())
|
||||
foreach (string file in ParadoxLauncher.InstallPath.EnumerateDirectory("*.exe"))
|
||||
return file.GetFileIconImage();
|
||||
break;
|
||||
case "Notepad":
|
||||
return IconGrabber.GetNotepadImage();
|
||||
case "Command Prompt":
|
||||
return IconGrabber.GetCommandPromptImage();
|
||||
case "File Explorer":
|
||||
return IconGrabber.GetFileExplorerImage();
|
||||
case "SteamDB":
|
||||
return await HttpClientManager.GetImageFromUrl(
|
||||
IconGrabber.GetDomainFaviconUrl("steamdb.info"));
|
||||
case "Steam Store":
|
||||
return await HttpClientManager.GetImageFromUrl(
|
||||
IconGrabber.GetDomainFaviconUrl("store.steampowered.com"));
|
||||
case "Steam Community":
|
||||
return await HttpClientManager.GetImageFromUrl(
|
||||
IconGrabber.GetDomainFaviconUrl("steamcommunity.com"));
|
||||
case "ScreamDB":
|
||||
return await HttpClientManager.GetImageFromUrl(
|
||||
IconGrabber.GetDomainFaviconUrl("scream-db.web.app"));
|
||||
case "Epic Games":
|
||||
return await HttpClientManager.GetImageFromUrl(
|
||||
IconGrabber.GetDomainFaviconUrl("epicgames.com"));
|
||||
case "Ubisoft Store":
|
||||
return await HttpClientManager.GetImageFromUrl(
|
||||
IconGrabber.GetDomainFaviconUrl("store.ubi.com"));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (image is not null)
|
||||
{
|
||||
Images[imageIdentifier] = image;
|
||||
item.Image = image;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task TryImageIdentifierInfo(ContextMenuItem item,
|
||||
(string id, string iconUrl) imageIdentifierInfo, Action onFail = null)
|
||||
=> await Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
(string id, string iconUrl) = imageIdentifierInfo;
|
||||
string imageIdentifier = "Icon_" + id;
|
||||
|
||||
if (Images.TryGetValue(imageIdentifier, out Image image) && image is not null)
|
||||
{
|
||||
(string id, string iconUrl) = imageIdentifierInfo;
|
||||
string imageIdentifier = "Icon_" + id;
|
||||
if (Images.TryGetValue(imageIdentifier, out Image image) && image is not null)
|
||||
item.Image = image;
|
||||
else
|
||||
{
|
||||
image = await HttpClientManager.GetImageFromUrl(iconUrl);
|
||||
if (image is not null)
|
||||
{
|
||||
Images[imageIdentifier] = image;
|
||||
item.Image = image;
|
||||
}
|
||||
else
|
||||
onFail?.Invoke();
|
||||
}
|
||||
item.Image = image;
|
||||
return;
|
||||
}
|
||||
catch
|
||||
|
||||
image = await HttpClientManager.GetImageFromUrl(iconUrl);
|
||||
if (image is not null)
|
||||
{
|
||||
// ignored
|
||||
Images[imageIdentifier] = image;
|
||||
item.Image = image;
|
||||
}
|
||||
});
|
||||
else
|
||||
onFail?.Invoke();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnClick(EventArgs e)
|
||||
{
|
||||
|
||||
@@ -42,6 +42,18 @@ internal class CustomForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnHandleCreated(EventArgs e)
|
||||
{
|
||||
base.OnHandleCreated(e);
|
||||
ThemeManager.Apply(this); // apply current theme (initial or toggled)
|
||||
}
|
||||
|
||||
protected override void OnShown(EventArgs e)
|
||||
{
|
||||
base.OnShown(e);
|
||||
ThemeManager.Apply(this); // ensure late-added controls also themed
|
||||
}
|
||||
|
||||
private void OnHelpButtonClicked(object sender, EventArgs args)
|
||||
{
|
||||
using DialogForm helpDialog = new(this);
|
||||
@@ -52,7 +64,7 @@ internal class CustomForm : Form
|
||||
"Automatically finds all installed Steam, Epic and Ubisoft games with their respective DLC-related DLL locations on the user's computer,\n"
|
||||
+ "parses SteamCMD, Steam Store and Epic Games Store for user-selected games' DLCs, then provides a very simple graphical interface\n"
|
||||
+ "utilizing the gathered information for the maintenance of DLC unlockers.\n\n"
|
||||
+ $"The program utilizes the latest version of [CreamAPI](https://cs.rin.ru/forum/viewtopic.php?f=29&t=70576) by [deadmau5](https://cs.rin.ru/forum/viewtopic.php?f=29&t=70576). It also utilizes the latest versions of [Koaloader]({acidicoala}/Koaloader), [ScreamAPI]({acidicoala}/ScreamAPI), [Uplay R1\n"
|
||||
+ $"The program utilizes the latest version of [CreamAPI](https://cs.rin.ru/forum/viewtopic.php?f=29&t=70576) by [deadmau5](https://cs.rin.ru/forum/viewtopic.php?f=29&t=70576). It also utilizes the latest versions of [SmokeAPI]({acidicoala}/SmokeAPI), [Koaloader]({acidicoala}/Koaloader), [ScreamAPI]({acidicoala}/ScreamAPI), [Uplay R1\n"
|
||||
+ $"Unlocker]({acidicoala}/UplayR1Unlocker) and [Uplay R2 Unlocker]({acidicoala}/UplayR2Unlocker), all by [acidicoala]({acidicoala}). All unlockers are downloaded and embedded into the program itself; no further\n"
|
||||
+ "downloads necessary on your part!\n\n"
|
||||
+ "USAGE:\n" + " 1. Choose which programs and/or games the program should scan for DLC.\n"
|
||||
|
||||
@@ -15,20 +15,17 @@ internal sealed class CustomTreeView : TreeView
|
||||
{
|
||||
private const string ProxyToggleString = "Proxy";
|
||||
|
||||
private static readonly Color C1 = ColorTranslator.FromHtml("#FFFF99");
|
||||
private static readonly Color C2 = ColorTranslator.FromHtml("#696900");
|
||||
private static readonly Color C3 = ColorTranslator.FromHtml("#AAAA69");
|
||||
private static readonly Color C4 = ColorTranslator.FromHtml("#99FFFF");
|
||||
private static readonly Color C5 = ColorTranslator.FromHtml("#006969");
|
||||
private static readonly Color C6 = ColorTranslator.FromHtml("#69AAAA");
|
||||
private static readonly Color C7 = ColorTranslator.FromHtml("#006900");
|
||||
private static readonly Color C8 = ColorTranslator.FromHtml("#69AA69");
|
||||
|
||||
private readonly Dictionary<Selection, Rectangle> checkBoxBounds = [];
|
||||
private readonly Dictionary<Selection, Rectangle> comboBoxBounds = [];
|
||||
|
||||
private readonly Dictionary<TreeNode, Rectangle> selectionBounds = [];
|
||||
private SolidBrush backBrush;
|
||||
private Color lastBackColor; // Tracks the last background color
|
||||
|
||||
// Selection background brush (used instead of SystemBrushes.Highlight to support dark mode)
|
||||
private SolidBrush selectionBrush;
|
||||
private Color lastSelectionBackColor;
|
||||
|
||||
private ToolStripDropDown comboBoxDropDown;
|
||||
private Font comboBoxFont;
|
||||
private Form form;
|
||||
@@ -54,6 +51,8 @@ internal sealed class CustomTreeView : TreeView
|
||||
{
|
||||
backBrush?.Dispose();
|
||||
backBrush = null;
|
||||
selectionBrush?.Dispose();
|
||||
selectionBrush = null;
|
||||
comboBoxFont?.Dispose();
|
||||
comboBoxFont = null;
|
||||
comboBoxDropDown?.Dispose();
|
||||
@@ -65,24 +64,85 @@ internal sealed class CustomTreeView : TreeView
|
||||
checkBoxBounds.Clear();
|
||||
comboBoxBounds.Clear();
|
||||
selectionBounds.Clear();
|
||||
backBrush?.Dispose();
|
||||
backBrush = null;
|
||||
lastBackColor = Color.Empty;
|
||||
|
||||
selectionBrush?.Dispose();
|
||||
selectionBrush = null;
|
||||
lastSelectionBackColor = Color.Empty;
|
||||
}
|
||||
|
||||
private void DrawTreeNode(object sender, DrawTreeNodeEventArgs e)
|
||||
{
|
||||
e.DrawDefault = true;
|
||||
TreeNode node = e.Node;
|
||||
if (node is not { IsVisible: true })
|
||||
{
|
||||
e.DrawDefault = true;
|
||||
return;
|
||||
}
|
||||
|
||||
bool dark = Program.DarkModeEnabled;
|
||||
bool highlighted = (e.State & TreeNodeStates.Selected) == TreeNodeStates.Selected && Focused;
|
||||
Graphics graphics = e.Graphics;
|
||||
backBrush ??= new(BackColor);
|
||||
|
||||
// Recreate back brush if background color changed
|
||||
if (backBrush == null || lastBackColor != BackColor)
|
||||
{
|
||||
backBrush?.Dispose();
|
||||
backBrush = new(BackColor);
|
||||
lastBackColor = BackColor;
|
||||
}
|
||||
|
||||
// If highlighted, prepare a selection brush that respects the theme
|
||||
if (highlighted)
|
||||
{
|
||||
Color selColor = ThemeManager.CustomTreeViewSelectionBackColor;
|
||||
if (selectionBrush == null || lastSelectionBackColor != selColor)
|
||||
{
|
||||
selectionBrush?.Dispose();
|
||||
selectionBrush = new(selColor);
|
||||
lastSelectionBackColor = selColor;
|
||||
}
|
||||
}
|
||||
|
||||
Form form = FindForm();
|
||||
|
||||
if (dark && CheckBoxes)
|
||||
{
|
||||
// In dark mode we take full ownership of the row so the system never
|
||||
// gets a chance to paint a light-background checkbox.
|
||||
e.DrawDefault = false;
|
||||
|
||||
// Row background
|
||||
Rectangle rowRect = new(0, node.Bounds.Top, ClientSize.Width, node.Bounds.Height);
|
||||
graphics.FillRectangle(highlighted ? selectionBrush : backBrush, rowRect);
|
||||
|
||||
// Node text
|
||||
Font nodeFont = node.NodeFont ?? Font;
|
||||
Color textColor = Enabled ? ForeColor : SystemColors.GrayText;
|
||||
TextRenderer.DrawText(graphics, node.Text, nodeFont,
|
||||
new Point(node.Bounds.Left, node.Bounds.Top + 1), textColor, TextFormatFlags.Default);
|
||||
|
||||
// Checkbox glyph – pure GDI so it matches the dark-themed CheckBox controls
|
||||
CheckBoxState cbState = node.Checked
|
||||
? (Enabled ? CheckBoxState.CheckedNormal : CheckBoxState.CheckedDisabled)
|
||||
: (Enabled ? CheckBoxState.UncheckedNormal : CheckBoxState.UncheckedDisabled);
|
||||
Size cbSize = CheckBoxRenderer.GetGlyphSize(graphics, cbState);
|
||||
int cbX = node.Bounds.Left - cbSize.Width - 2;
|
||||
int cbY = node.Bounds.Top + node.Bounds.Height / 2 - cbSize.Height / 2;
|
||||
ThemeManager.DrawDarkCheckBox(graphics, new Point(cbX, cbY), cbSize, node.Checked, Enabled);
|
||||
}
|
||||
else
|
||||
{
|
||||
e.DrawDefault = true;
|
||||
}
|
||||
|
||||
Font font = node.NodeFont ?? Font;
|
||||
Brush brush = highlighted ? SystemBrushes.Highlight : backBrush;
|
||||
Brush brush = highlighted ? (Brush)selectionBrush : backBrush;
|
||||
Rectangle bounds = node.Bounds;
|
||||
Rectangle selectionBounds = bounds;
|
||||
|
||||
Form form = FindForm();
|
||||
if (form is not SelectForm and not SelectDialogForm)
|
||||
return;
|
||||
|
||||
@@ -93,10 +153,10 @@ internal sealed class CustomTreeView : TreeView
|
||||
return;
|
||||
|
||||
Color color = highlighted
|
||||
? C1
|
||||
? ThemeManager.CustomTreeViewHighlightPlatformColor
|
||||
: Enabled
|
||||
? C2
|
||||
: C3;
|
||||
? ThemeManager.CustomTreeViewPlatformColor
|
||||
: ThemeManager.CustomTreeViewDisabledPlatformColor;
|
||||
string text;
|
||||
if (dlcType is not DLCType.None)
|
||||
{
|
||||
@@ -115,10 +175,10 @@ internal sealed class CustomTreeView : TreeView
|
||||
if (platform is not Platform.Paradox)
|
||||
{
|
||||
color = highlighted
|
||||
? C4
|
||||
? ThemeManager.CustomTreeViewHighlightIdColor
|
||||
: Enabled
|
||||
? C5
|
||||
: C6;
|
||||
? ThemeManager.CustomTreeViewIdColor
|
||||
: ThemeManager.CustomTreeViewDisabledIdColor;
|
||||
text = id;
|
||||
size = TextRenderer.MeasureText(graphics, text, font);
|
||||
const int left = -4;
|
||||
@@ -142,18 +202,19 @@ internal sealed class CustomTreeView : TreeView
|
||||
graphics.FillRectangle(brush, bounds);
|
||||
}
|
||||
|
||||
CheckBoxState checkBoxState = selection.UseProxy
|
||||
? Enabled ? CheckBoxState.CheckedPressed : CheckBoxState.CheckedDisabled
|
||||
: Enabled
|
||||
? CheckBoxState.UncheckedPressed
|
||||
: CheckBoxState.UncheckedDisabled;
|
||||
size = CheckBoxRenderer.GetGlyphSize(graphics, checkBoxState);
|
||||
CheckBoxState proxyState = selection.UseProxy
|
||||
? (Enabled ? CheckBoxState.CheckedNormal : CheckBoxState.CheckedDisabled)
|
||||
: (Enabled ? CheckBoxState.UncheckedNormal : CheckBoxState.UncheckedDisabled);
|
||||
size = CheckBoxRenderer.GetGlyphSize(graphics, proxyState);
|
||||
bounds = bounds with { X = bounds.X + bounds.Width, Width = size.Width };
|
||||
selectionBounds = new(selectionBounds.Location, selectionBounds.Size + bounds.Size with { Height = 0 });
|
||||
Rectangle checkBoxBounds = bounds;
|
||||
graphics.FillRectangle(backBrush, bounds);
|
||||
point = new(bounds.Left, bounds.Top + bounds.Height / 2 - size.Height / 2 - 1);
|
||||
CheckBoxRenderer.DrawCheckBox(graphics, point, checkBoxState);
|
||||
if (dark)
|
||||
ThemeManager.DrawDarkCheckBox(graphics, point, size, selection.UseProxy, Enabled);
|
||||
else
|
||||
CheckBoxRenderer.DrawCheckBox(graphics, point, proxyState);
|
||||
|
||||
text = ProxyToggleString;
|
||||
size = TextRenderer.MeasureText(graphics, text, font);
|
||||
@@ -163,7 +224,9 @@ internal sealed class CustomTreeView : TreeView
|
||||
checkBoxBounds = new(checkBoxBounds.Location, checkBoxBounds.Size + bounds.Size with { Height = 0 });
|
||||
graphics.FillRectangle(backBrush, bounds);
|
||||
point = new(bounds.Location.X - 1 + left, bounds.Location.Y + 1);
|
||||
TextRenderer.DrawText(graphics, text, font, point, Enabled ? C7 : C8, TextFormatFlags.Default);
|
||||
TextRenderer.DrawText(graphics, text, font, point,
|
||||
Enabled ? ThemeManager.CustomTreeViewProxyColor : ThemeManager.CustomTreeViewDisabledProxyColor,
|
||||
TextFormatFlags.Default);
|
||||
|
||||
this.checkBoxBounds[selection] = RectangleToClient(checkBoxBounds);
|
||||
|
||||
@@ -171,8 +234,11 @@ internal sealed class CustomTreeView : TreeView
|
||||
{
|
||||
comboBoxFont ??= new(font.FontFamily, 6, font.Style, font.Unit, font.GdiCharSet,
|
||||
font.GdiVerticalFont);
|
||||
ComboBoxState comboBoxState = Enabled ? ComboBoxState.Normal : ComboBoxState.Disabled;
|
||||
ButtonState buttonState = Enabled ? ButtonState.Normal : ButtonState.Inactive;
|
||||
|
||||
bool darkMode = Program.DarkModeEnabled;
|
||||
Color comboBackColor = ThemeManager.CustomTreeViewComboBackColor;
|
||||
Color comboBorderColor = ThemeManager.CustomTreeViewComboBorderColor;
|
||||
Color comboTextColor = ThemeManager.CustomTreeViewComboTextColor;
|
||||
|
||||
text = (selection.Proxy ?? Selection.DefaultProxy) + ".dll";
|
||||
size = TextRenderer.MeasureText(graphics, text, comboBoxFont) + new Size(6, 0);
|
||||
@@ -181,18 +247,9 @@ internal sealed class CustomTreeView : TreeView
|
||||
selectionBounds = new(selectionBounds.Location,
|
||||
selectionBounds.Size + bounds.Size with { Height = 0 });
|
||||
Rectangle comboBoxBounds = bounds;
|
||||
graphics.FillRectangle(backBrush, bounds);
|
||||
if (ComboBoxRenderer.IsSupported)
|
||||
ComboBoxRenderer.DrawTextBox(graphics, bounds, text, comboBoxFont, comboBoxState);
|
||||
else
|
||||
{
|
||||
graphics.FillRectangle(SystemBrushes.ControlText, bounds);
|
||||
ControlPaint.DrawButton(graphics, bounds, buttonState);
|
||||
point = new(bounds.Location.X + 3 + bounds.Width / 2 - size.Width / 2,
|
||||
bounds.Location.Y + bounds.Height / 2 - size.Height / 2);
|
||||
TextRenderer.DrawText(graphics, text, comboBoxFont, point,
|
||||
Enabled ? SystemColors.ControlText : SystemColors.GrayText, TextFormatFlags.Default);
|
||||
}
|
||||
|
||||
// Themed combobox background + text (centralized in ThemeManager)
|
||||
ThemeManager.DrawCustomComboBox(graphics, bounds, comboBoxFont, text);
|
||||
|
||||
size = new(14, 0);
|
||||
left = -1;
|
||||
@@ -201,10 +258,9 @@ internal sealed class CustomTreeView : TreeView
|
||||
selectionBounds.Size + new Size(bounds.Size.Width + left, 0));
|
||||
comboBoxBounds = new(comboBoxBounds.Location,
|
||||
comboBoxBounds.Size + new Size(bounds.Size.Width + left, 0));
|
||||
if (ComboBoxRenderer.IsSupported)
|
||||
ComboBoxRenderer.DrawDropDownButton(graphics, bounds, comboBoxState);
|
||||
else
|
||||
ControlPaint.DrawComboButton(graphics, bounds, buttonState);
|
||||
|
||||
// Themed combobox dropdown button (centralized in ThemeManager)
|
||||
ThemeManager.DrawCustomComboBoxButton(graphics, bounds);
|
||||
|
||||
this.comboBoxBounds[selection] = RectangleToClient(comboBoxBounds);
|
||||
}
|
||||
@@ -246,6 +302,7 @@ internal sealed class CustomTreeView : TreeView
|
||||
comboBoxDropDown ??= new();
|
||||
comboBoxDropDown.ShowItemToolTips = false;
|
||||
comboBoxDropDown.Items.Clear();
|
||||
|
||||
foreach (string proxy in proxies)
|
||||
{
|
||||
bool canUse = true;
|
||||
@@ -261,13 +318,22 @@ internal sealed class CustomTreeView : TreeView
|
||||
}
|
||||
|
||||
if (canUse)
|
||||
_ = comboBoxDropDown.Items.Add(new ToolStripButton(proxy + ".dll", null, (_, _) =>
|
||||
{
|
||||
ToolStripMenuItem menuItem = new(proxy + ".dll", null, (_, _) =>
|
||||
{
|
||||
pair.Key.Proxy = proxy == Selection.DefaultProxy ? null : proxy;
|
||||
selectForm.OnProxyChanged();
|
||||
}) { Font = comboBoxFont });
|
||||
})
|
||||
{
|
||||
Font = comboBoxFont
|
||||
};
|
||||
_ = comboBoxDropDown.Items.Add(menuItem);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply theme using ThemeManager
|
||||
ThemeManager.ApplyToolStripDropDown(comboBoxDropDown);
|
||||
|
||||
comboBoxDropDown.Show(this, PointToScreen(new(pair.Value.Left, pair.Value.Bottom - 1)));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ internal sealed class StringComparer : IComparer<string>
|
||||
{
|
||||
public int Compare(string a, string b)
|
||||
=> !int.TryParse(a, out _) && !int.TryParse(b, out _)
|
||||
? string.Compare(a, b, StringComparison.CurrentCulture)
|
||||
? string.Compare(a, b, StringComparison.Ordinal)
|
||||
: !int.TryParse(a, out int A)
|
||||
? 1
|
||||
: !int.TryParse(b, out int B)
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
|
||||
<UseWindowsForms>True</UseWindowsForms>
|
||||
<ApplicationIcon>Resources\program.ico</ApplicationIcon>
|
||||
<Version>5.0.1.0</Version>
|
||||
<Copyright>2021, pointfeev (https://github.com/pointfeev)</Copyright>
|
||||
<Version>5.0.2.2</Version>
|
||||
<Copyright>2026, FroggMaster (https://github.com/FroggMaster)</Copyright>
|
||||
<Company>CreamInstaller</Company>
|
||||
<Product>Automatic DLC Unlocker Installer & Configuration Generator</Product>
|
||||
<StartupObject>CreamInstaller.Program</StartupObject>
|
||||
@@ -24,6 +24,7 @@
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion>
|
||||
<Platforms>x64</Platforms>
|
||||
<ForceDesignerDpiUnaware>true</ForceDesignerDpiUnaware>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||
<AssemblyName>$(Company)</AssemblyName>
|
||||
@@ -207,4 +208,4 @@
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -68,6 +68,9 @@ internal sealed partial class InstallForm : CustomForm
|
||||
bool useCreamApiProxy = selection.UseProxy && !Program.UseSmokeAPI &&
|
||||
(selection.Platform is Platform.Steam || selection.Platform is Platform.Paradox &&
|
||||
selection.ExtraSelections.Any(s => s.Platform is Platform.Steam));
|
||||
bool useSmokeApiProxy = selection.UseProxy && Program.UseSmokeAPI &&
|
||||
(selection.Platform is Platform.Steam || selection.Platform is Platform.Paradox &&
|
||||
selection.ExtraSelections.Any(s => s.Platform is Platform.Steam));
|
||||
|
||||
UpdateUser(
|
||||
$"{(uninstalling ? "Uninstalling" : "Installing")}" + $" {(uninstalling ? "from" : "for")} " +
|
||||
@@ -84,7 +87,7 @@ internal sealed partial class InstallForm : CustomForm
|
||||
if (Program.Canceled)
|
||||
return;
|
||||
|
||||
directory.GetKoaloaderComponents(out string old_config, out string config);
|
||||
directory.GetKoaloaderComponents(out string old_config, out string config, out _);
|
||||
if (directory.GetKoaloaderProxies().Any(proxy =>
|
||||
proxy.FileExists() && proxy.IsResourceFile(ResourceIdentifier.Koaloader))
|
||||
|| directory != selection.RootDirectory &&
|
||||
@@ -97,19 +100,36 @@ internal sealed partial class InstallForm : CustomForm
|
||||
await Koaloader.Uninstall(directory, selection.RootDirectory, this);
|
||||
}
|
||||
|
||||
directory.GetCreamApiComponents(out _, out _, out _, out _, out config);
|
||||
if (directory.GetCreamApiProxies().Any(proxy =>
|
||||
proxy.FileExists() && (proxy.IsResourceFile(ResourceIdentifier.Steamworks32) ||
|
||||
proxy.IsResourceFile(ResourceIdentifier.Steamworks64))))
|
||||
if (!Program.UseSmokeAPI)
|
||||
{
|
||||
UpdateUser(
|
||||
"Uninstalling CreamAPI in proxy mode from " + selection.Name +
|
||||
$" in incorrect directory \"{directory}\" . . . ", LogTextBox.Operation);
|
||||
await CreamAPI.ProxyUninstall(directory, this);
|
||||
directory.GetCreamApiComponents(out _, out _, out _, out _, out config);
|
||||
if (directory.GetCreamApiProxies().Any(proxy =>
|
||||
proxy.FileExists() && (proxy.IsResourceFile(ResourceIdentifier.Steamworks32) ||
|
||||
proxy.IsResourceFile(ResourceIdentifier.Steamworks64))))
|
||||
{
|
||||
UpdateUser(
|
||||
"Uninstalling CreamAPI in proxy mode from " + selection.Name +
|
||||
$" in incorrect directory \"{directory}\" . . . ", LogTextBox.Operation);
|
||||
await CreamAPI.ProxyUninstall(directory, this);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
directory.GetSmokeApiComponents(out _, out _, out _, out _, out old_config, out config, out _,
|
||||
out _, out _);
|
||||
if (directory.GetSmokeApiProxies().Any(proxy =>
|
||||
proxy.FileExists() && (proxy.IsResourceFile(ResourceIdentifier.Steamworks32) ||
|
||||
proxy.IsResourceFile(ResourceIdentifier.Steamworks64))))
|
||||
{
|
||||
UpdateUser(
|
||||
"Uninstalling SmokeAPI in proxy mode from " + selection.Name +
|
||||
$" in incorrect directory \"{directory}\" . . . ", LogTextBox.Operation);
|
||||
await SmokeAPI.ProxyUninstall(directory, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (uninstalling || !useKoaloader || !useCreamApiProxy)
|
||||
if (uninstalling || !useKoaloader || !useCreamApiProxy || !useSmokeApiProxy)
|
||||
foreach ((string directory, _) in selection.ExecutableDirectories)
|
||||
{
|
||||
if (Program.Canceled)
|
||||
@@ -117,7 +137,7 @@ internal sealed partial class InstallForm : CustomForm
|
||||
|
||||
if (uninstalling || !useKoaloader)
|
||||
{
|
||||
directory.GetKoaloaderComponents(out string old_config, out string config);
|
||||
directory.GetKoaloaderComponents(out string old_config, out string config, out _);
|
||||
if (directory.GetKoaloaderProxies().Any(proxy =>
|
||||
proxy.FileExists() && proxy.IsResourceFile(ResourceIdentifier.Koaloader))
|
||||
|| Koaloader.AutoLoadDLLs.Any(pair => (directory + @"\" + pair.dll).FileExists()) ||
|
||||
@@ -130,23 +150,44 @@ internal sealed partial class InstallForm : CustomForm
|
||||
}
|
||||
}
|
||||
|
||||
if (uninstalling || !useCreamApiProxy)
|
||||
if (!Program.UseSmokeAPI)
|
||||
{
|
||||
directory.GetCreamApiComponents(out _, out _, out _, out _, out string config);
|
||||
if (directory.GetCreamApiProxies().Any(proxy =>
|
||||
proxy.FileExists() && (proxy.IsResourceFile(ResourceIdentifier.Steamworks32) ||
|
||||
proxy.IsResourceFile(ResourceIdentifier.Steamworks64))) ||
|
||||
config.FileExists())
|
||||
if (uninstalling || !useCreamApiProxy)
|
||||
{
|
||||
UpdateUser(
|
||||
"Uninstalling CreamAPI in proxy mode from " + selection.Name +
|
||||
$" in directory \"{directory}\" . . . ", LogTextBox.Operation);
|
||||
await CreamAPI.ProxyUninstall(directory, this);
|
||||
directory.GetCreamApiComponents(out _, out _, out _, out _, out string config);
|
||||
if (directory.GetCreamApiProxies().Any(proxy =>
|
||||
proxy.FileExists() && (proxy.IsResourceFile(ResourceIdentifier.Steamworks32) ||
|
||||
proxy.IsResourceFile(ResourceIdentifier.Steamworks64))) ||
|
||||
config.FileExists())
|
||||
{
|
||||
UpdateUser(
|
||||
"Uninstalling CreamAPI in proxy mode from " + selection.Name +
|
||||
$" in directory \"{directory}\" . . . ", LogTextBox.Operation);
|
||||
await CreamAPI.ProxyUninstall(directory, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (uninstalling || !useSmokeApiProxy)
|
||||
{
|
||||
directory.GetSmokeApiComponents(out _, out _, out _, out _, out string old_config, out string config, out _,
|
||||
out _, out _);
|
||||
if (directory.GetSmokeApiProxies().Any(proxy =>
|
||||
proxy.FileExists() && (proxy.IsResourceFile(ResourceIdentifier.Steamworks32) ||
|
||||
proxy.IsResourceFile(ResourceIdentifier.Steamworks64))) ||
|
||||
config.FileExists())
|
||||
{
|
||||
UpdateUser(
|
||||
"Uninstalling SmokeAPI in proxy mode from " + selection.Name +
|
||||
$" in directory \"{directory}\" . . . ", LogTextBox.Operation);
|
||||
await SmokeAPI.ProxyUninstall(directory, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool uninstallingForProxy = uninstalling || useKoaloader || useCreamApiProxy;
|
||||
bool uninstallingForProxy = uninstalling || useKoaloader || useCreamApiProxy || useSmokeApiProxy;
|
||||
int count = selection.DllDirectories.Count, cur = 0;
|
||||
foreach (string directory in selection.DllDirectories)
|
||||
{
|
||||
@@ -199,7 +240,7 @@ internal sealed partial class InstallForm : CustomForm
|
||||
if (selection.Platform is Platform.Epic or Platform.Paradox)
|
||||
{
|
||||
directory.GetScreamApiComponents(out string api32, out string api32_o, out string api64,
|
||||
out string api64_o, out string config, out string log);
|
||||
out string api64_o, out string old_config, out string config, out string old_log, out string log);
|
||||
if (uninstallingForProxy
|
||||
? api32_o.FileExists() || api64_o.FileExists() || config.FileExists() || log.FileExists()
|
||||
: api32.FileExists() || api64.FileExists())
|
||||
@@ -253,13 +294,13 @@ internal sealed partial class InstallForm : CustomForm
|
||||
UpdateProgress(++cur / count * 100);
|
||||
}
|
||||
|
||||
if ((useCreamApiProxy || useKoaloader) && !uninstalling)
|
||||
if ((useCreamApiProxy || useSmokeApiProxy || useKoaloader) && !uninstalling)
|
||||
foreach ((string directory, BinaryType binaryType) in selection.ExecutableDirectories)
|
||||
{
|
||||
if (Program.Canceled)
|
||||
return;
|
||||
|
||||
if (useCreamApiProxy)
|
||||
if (useCreamApiProxy && !Program.UseSmokeAPI)
|
||||
{
|
||||
UpdateUser(
|
||||
"Installing CreamAPI in proxy mode for " + selection.Name +
|
||||
@@ -267,6 +308,14 @@ internal sealed partial class InstallForm : CustomForm
|
||||
LogTextBox.Operation);
|
||||
await CreamAPI.ProxyInstall(directory, binaryType, selection, this);
|
||||
}
|
||||
else if (useSmokeApiProxy && Program.UseSmokeAPI)
|
||||
{
|
||||
UpdateUser(
|
||||
"Installing SmokeAPI in proxy mode for " + selection.Name +
|
||||
$" in directory \"{directory}\" . . . ",
|
||||
LogTextBox.Operation);
|
||||
await SmokeAPI.ProxyInstall(directory, binaryType, selection, this);
|
||||
}
|
||||
else if (useKoaloader)
|
||||
{
|
||||
UpdateUser("Installing Koaloader for " + selection.Name + $" in directory \"{directory}\" . . . ",
|
||||
|
||||
+16
-2
@@ -33,6 +33,7 @@ namespace CreamInstaller.Forms
|
||||
saveButton = new Button();
|
||||
uninstallAllButton = new Button();
|
||||
selectionTreeView = new CustomTreeView();
|
||||
filterTextBox = new System.Windows.Forms.TextBox();
|
||||
groupBox.SuspendLayout();
|
||||
allCheckBoxFlowPanel.SuspendLayout();
|
||||
SuspendLayout();
|
||||
@@ -51,15 +52,25 @@ namespace CreamInstaller.Forms
|
||||
acceptButton.Text = "OK";
|
||||
acceptButton.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// filterTextBox
|
||||
//
|
||||
filterTextBox.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
|
||||
filterTextBox.Location = new System.Drawing.Point(12, 14);
|
||||
filterTextBox.Name = "filterTextBox";
|
||||
filterTextBox.PlaceholderText = "Enter the name of a game to search";
|
||||
filterTextBox.Size = new System.Drawing.Size(524, 23);
|
||||
filterTextBox.TabIndex = 0;
|
||||
filterTextBox.TextChanged += OnFilterTextChanged;
|
||||
//
|
||||
// groupBox
|
||||
//
|
||||
groupBox.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
|
||||
groupBox.Controls.Add(selectionTreeView);
|
||||
groupBox.Controls.Add(allCheckBoxFlowPanel);
|
||||
groupBox.Location = new System.Drawing.Point(12, 12);
|
||||
groupBox.Location = new System.Drawing.Point(12, 43);
|
||||
groupBox.MinimumSize = new System.Drawing.Size(240, 40);
|
||||
groupBox.Name = "groupBox";
|
||||
groupBox.Size = new System.Drawing.Size(524, 225);
|
||||
groupBox.Size = new System.Drawing.Size(524, 194);
|
||||
groupBox.TabIndex = 3;
|
||||
groupBox.TabStop = false;
|
||||
groupBox.Text = "Choices";
|
||||
@@ -106,6 +117,7 @@ namespace CreamInstaller.Forms
|
||||
//
|
||||
sortCheckBox.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
|
||||
sortCheckBox.AutoSize = true;
|
||||
sortCheckBox.Checked = true; // Enable Sort By Name by default
|
||||
sortCheckBox.Location = new System.Drawing.Point(220, 247);
|
||||
sortCheckBox.Margin = new Padding(3, 0, 0, 0);
|
||||
sortCheckBox.Name = "sortCheckBox";
|
||||
@@ -187,6 +199,7 @@ namespace CreamInstaller.Forms
|
||||
Controls.Add(cancelButton);
|
||||
Controls.Add(acceptButton);
|
||||
Controls.Add(groupBox);
|
||||
Controls.Add(filterTextBox);
|
||||
FormBorderStyle = FormBorderStyle.FixedSingle;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
@@ -214,5 +227,6 @@ namespace CreamInstaller.Forms
|
||||
private Button saveButton;
|
||||
private CheckBox sortCheckBox;
|
||||
private Button uninstallAllButton;
|
||||
private System.Windows.Forms.TextBox filterTextBox;
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,12 @@ namespace CreamInstaller.Forms;
|
||||
internal sealed partial class SelectDialogForm : CustomForm
|
||||
{
|
||||
private readonly List<(Platform platform, string id, string name)> selected = new();
|
||||
private readonly List<(Platform platform, string id, string name, bool alreadySelected)> allChoices = new();
|
||||
|
||||
internal SelectDialogForm(IWin32Window owner) : base(owner)
|
||||
{
|
||||
InitializeComponent();
|
||||
selectionTreeView.TreeViewNodeSorter = PlatformIdComparer.NodeName;
|
||||
selectionTreeView.TreeViewNodeSorter = sortCheckBox.Checked ? PlatformIdComparer.NodeText : PlatformIdComparer.NodeName;
|
||||
}
|
||||
|
||||
internal DialogResult QueryUser(string groupBoxText,
|
||||
@@ -28,12 +29,12 @@ internal sealed partial class SelectDialogForm : CustomForm
|
||||
allCheckBox.Enabled = false;
|
||||
acceptButton.Enabled = false;
|
||||
selectionTreeView.AfterCheck += OnTreeNodeChecked;
|
||||
foreach ((Platform platform, string id, string name, bool alreadySelected) in potentialChoices)
|
||||
{
|
||||
TreeNode node = new() { Tag = platform, Name = id, Text = name, Checked = alreadySelected };
|
||||
OnTreeNodeChecked(node);
|
||||
_ = selectionTreeView.Nodes.Add(node);
|
||||
}
|
||||
allChoices.Clear();
|
||||
allChoices.AddRange(potentialChoices);
|
||||
foreach ((Platform platform, string id, string name, bool alreadySelected) in allChoices)
|
||||
if (alreadySelected)
|
||||
selected.Add((platform, id, name));
|
||||
ApplyFilter();
|
||||
|
||||
if (selected.Count < 1)
|
||||
OnLoad(null, null);
|
||||
@@ -70,6 +71,32 @@ internal sealed partial class SelectDialogForm : CustomForm
|
||||
allCheckBox.CheckedChanged += OnAllCheckBoxChanged;
|
||||
}
|
||||
|
||||
private void OnFilterTextChanged(object sender, EventArgs e) => ApplyFilter();
|
||||
|
||||
private void ApplyFilter()
|
||||
{
|
||||
string filter = filterTextBox.Text.Trim();
|
||||
selectionTreeView.AfterCheck -= OnTreeNodeChecked;
|
||||
selectionTreeView.BeginUpdate();
|
||||
selectionTreeView.Nodes.Clear();
|
||||
bool hasSelections = selected.Count > 0;
|
||||
foreach ((Platform platform, string id, string name, bool alreadySelected) in allChoices)
|
||||
{
|
||||
if (filter.Length > 0 && name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0)
|
||||
continue;
|
||||
bool checkedState = hasSelections
|
||||
? selected.Any(s => s.platform == platform && s.id == id)
|
||||
: alreadySelected;
|
||||
TreeNode node = new() { Tag = platform, Name = id, Text = name, Checked = checkedState };
|
||||
_ = selectionTreeView.Nodes.Add(node);
|
||||
}
|
||||
selectionTreeView.EndUpdate();
|
||||
selectionTreeView.AfterCheck += OnTreeNodeChecked;
|
||||
allCheckBox.CheckedChanged -= OnAllCheckBoxChanged;
|
||||
allCheckBox.Checked = selectionTreeView.Nodes.Count > 0 && selectionTreeView.Nodes.Cast<TreeNode>().All(n => n.Checked);
|
||||
allCheckBox.CheckedChanged += OnAllCheckBoxChanged;
|
||||
}
|
||||
|
||||
private void OnResize(object s, EventArgs e)
|
||||
=> Text = TextRenderer.MeasureText(Program.ApplicationName, Font).Width > Size.Width - 100
|
||||
? Program.ApplicationNameShort
|
||||
|
||||
+121
-23
@@ -1,8 +1,8 @@
|
||||
using System.ComponentModel;
|
||||
using CreamInstaller.Components;
|
||||
using CreamInstaller.Resources;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using CreamInstaller.Components;
|
||||
|
||||
namespace CreamInstaller.Forms
|
||||
{
|
||||
partial class SelectForm
|
||||
@@ -28,6 +28,11 @@ namespace CreamInstaller.Forms
|
||||
blockedGamesFlowPanel = new FlowLayoutPanel();
|
||||
blockedGamesCheckBox = new CheckBox();
|
||||
blockProtectedHelpButton = new Button();
|
||||
useSmokeAPILayoutPanel = new FlowLayoutPanel();
|
||||
useSmokeAPICheckBox = new CheckBox();
|
||||
useSmokeAPIHelpButton = new Button();
|
||||
darkModeFlowPanel = new FlowLayoutPanel();
|
||||
darkModeCheckBox = new CheckBox();
|
||||
allCheckBoxLayoutPanel = new FlowLayoutPanel();
|
||||
allCheckBox = new CheckBox();
|
||||
progressBar = new ProgressBar();
|
||||
@@ -42,9 +47,12 @@ namespace CreamInstaller.Forms
|
||||
resetButton = new Button();
|
||||
saveFlowPanel = new FlowLayoutPanel();
|
||||
selectionTreeView = new CustomTreeView();
|
||||
topOptionsTable = new TableLayoutPanel();
|
||||
programsGroupBox.SuspendLayout();
|
||||
proxyFlowPanel.SuspendLayout();
|
||||
blockedGamesFlowPanel.SuspendLayout();
|
||||
useSmokeAPILayoutPanel.SuspendLayout();
|
||||
darkModeFlowPanel.SuspendLayout();
|
||||
allCheckBoxLayoutPanel.SuspendLayout();
|
||||
saveFlowPanel.SuspendLayout();
|
||||
SuspendLayout();
|
||||
@@ -81,25 +89,20 @@ namespace CreamInstaller.Forms
|
||||
// programsGroupBox
|
||||
//
|
||||
programsGroupBox.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
|
||||
programsGroupBox.Controls.Add(proxyFlowPanel);
|
||||
programsGroupBox.Controls.Add(noneFoundLabel);
|
||||
programsGroupBox.Controls.Add(blockedGamesFlowPanel);
|
||||
programsGroupBox.Controls.Add(allCheckBoxLayoutPanel);
|
||||
programsGroupBox.Controls.Add(selectionTreeView);
|
||||
programsGroupBox.Location = new System.Drawing.Point(12, 12);
|
||||
programsGroupBox.Location = new System.Drawing.Point(12, 47);
|
||||
programsGroupBox.Name = "programsGroupBox";
|
||||
programsGroupBox.Size = new System.Drawing.Size(610, 287);
|
||||
programsGroupBox.Size = new System.Drawing.Size(610, 252);
|
||||
programsGroupBox.TabIndex = 8;
|
||||
programsGroupBox.TabStop = false;
|
||||
programsGroupBox.Text = "Programs / Games";
|
||||
//
|
||||
// proxyFlowPanel
|
||||
//
|
||||
proxyFlowPanel.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||
proxyFlowPanel.AutoSize = true;
|
||||
proxyFlowPanel.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
proxyFlowPanel.Controls.Add(proxyAllCheckBox);
|
||||
proxyFlowPanel.Location = new System.Drawing.Point(478, -1);
|
||||
proxyFlowPanel.Margin = new Padding(0);
|
||||
proxyFlowPanel.Name = "proxyFlowPanel";
|
||||
proxyFlowPanel.Size = new System.Drawing.Size(75, 19);
|
||||
@@ -108,7 +111,6 @@ namespace CreamInstaller.Forms
|
||||
//
|
||||
// proxyAllCheckBox
|
||||
//
|
||||
proxyAllCheckBox.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||
proxyAllCheckBox.AutoSize = true;
|
||||
proxyAllCheckBox.Enabled = false;
|
||||
proxyAllCheckBox.Location = new System.Drawing.Point(2, 0);
|
||||
@@ -124,7 +126,7 @@ namespace CreamInstaller.Forms
|
||||
noneFoundLabel.Dock = DockStyle.Fill;
|
||||
noneFoundLabel.Location = new System.Drawing.Point(3, 19);
|
||||
noneFoundLabel.Name = "noneFoundLabel";
|
||||
noneFoundLabel.Size = new System.Drawing.Size(604, 265);
|
||||
noneFoundLabel.Size = new System.Drawing.Size(604, 230);
|
||||
noneFoundLabel.TabIndex = 1002;
|
||||
noneFoundLabel.Text = "No applicable programs nor games were found on your computer!";
|
||||
noneFoundLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||
@@ -132,12 +134,10 @@ namespace CreamInstaller.Forms
|
||||
//
|
||||
// blockedGamesFlowPanel
|
||||
//
|
||||
blockedGamesFlowPanel.Anchor = AnchorStyles.Top;
|
||||
blockedGamesFlowPanel.AutoSize = true;
|
||||
blockedGamesFlowPanel.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
blockedGamesFlowPanel.Controls.Add(blockedGamesCheckBox);
|
||||
blockedGamesFlowPanel.Controls.Add(blockProtectedHelpButton);
|
||||
blockedGamesFlowPanel.Location = new System.Drawing.Point(150, -1);
|
||||
blockedGamesFlowPanel.Margin = new Padding(0);
|
||||
blockedGamesFlowPanel.Name = "blockedGamesFlowPanel";
|
||||
blockedGamesFlowPanel.Size = new System.Drawing.Size(170, 19);
|
||||
@@ -172,22 +172,83 @@ namespace CreamInstaller.Forms
|
||||
blockProtectedHelpButton.UseVisualStyleBackColor = true;
|
||||
blockProtectedHelpButton.Click += OnBlockProtectedGamesHelpButtonClicked;
|
||||
//
|
||||
// useSmokeAPILayoutPanel
|
||||
//
|
||||
useSmokeAPILayoutPanel.AutoSize = true;
|
||||
useSmokeAPILayoutPanel.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
useSmokeAPILayoutPanel.Controls.Add(useSmokeAPICheckBox);
|
||||
useSmokeAPILayoutPanel.Controls.Add(useSmokeAPIHelpButton);
|
||||
useSmokeAPILayoutPanel.Margin = new Padding(12, 0, 0, 0);
|
||||
useSmokeAPILayoutPanel.Name = "useSmokeAPILayoutPanel";
|
||||
useSmokeAPILayoutPanel.Size = new System.Drawing.Size(124, 19);
|
||||
useSmokeAPILayoutPanel.TabIndex = 1006;
|
||||
useSmokeAPILayoutPanel.WrapContents = false;
|
||||
//
|
||||
// useSmokeAPICheckBox
|
||||
//
|
||||
useSmokeAPICheckBox.AutoSize = true;
|
||||
useSmokeAPICheckBox.Checked = true;
|
||||
useSmokeAPICheckBox.CheckState = CheckState.Checked;
|
||||
useSmokeAPICheckBox.Enabled = false;
|
||||
useSmokeAPICheckBox.Location = new System.Drawing.Point(2, 0);
|
||||
useSmokeAPICheckBox.Margin = new Padding(2, 0, 0, 0);
|
||||
useSmokeAPICheckBox.Name = "useSmokeAPICheckBox";
|
||||
useSmokeAPICheckBox.Size = new System.Drawing.Size(102, 19);
|
||||
useSmokeAPICheckBox.TabIndex = 1;
|
||||
useSmokeAPICheckBox.Text = "Use SmokeAPI";
|
||||
useSmokeAPICheckBox.UseVisualStyleBackColor = true;
|
||||
useSmokeAPICheckBox.CheckedChanged += OnUseSmokeAPICheckBoxChanged;
|
||||
//
|
||||
// useSmokeAPIHelpButton
|
||||
//
|
||||
useSmokeAPIHelpButton.Enabled = false;
|
||||
useSmokeAPIHelpButton.Font = new System.Drawing.Font("Segoe UI", 7F);
|
||||
useSmokeAPIHelpButton.Location = new System.Drawing.Point(104, 0);
|
||||
useSmokeAPIHelpButton.Margin = new Padding(0, 0, 1, 0);
|
||||
useSmokeAPIHelpButton.Name = "useSmokeAPIHelpButton";
|
||||
useSmokeAPIHelpButton.Size = new System.Drawing.Size(19, 19);
|
||||
useSmokeAPIHelpButton.TabIndex = 2;
|
||||
useSmokeAPIHelpButton.Text = "?";
|
||||
useSmokeAPIHelpButton.UseVisualStyleBackColor = true;
|
||||
useSmokeAPIHelpButton.Click += OnUseSmokeAPIHelpButtonClicked;
|
||||
//
|
||||
// darkModeFlowPanel
|
||||
//
|
||||
darkModeFlowPanel.AutoSize = true;
|
||||
darkModeFlowPanel.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
darkModeFlowPanel.Margin = new Padding(12, 0, 0, 0);
|
||||
darkModeFlowPanel.Name = "darkModeFlowPanel";
|
||||
darkModeFlowPanel.Size = new System.Drawing.Size(98, 19);
|
||||
darkModeFlowPanel.TabIndex = 10011;
|
||||
darkModeFlowPanel.WrapContents = false;
|
||||
//
|
||||
// darkModeCheckBox
|
||||
//
|
||||
darkModeCheckBox.AutoSize = true;
|
||||
darkModeCheckBox.Enabled = true;
|
||||
darkModeCheckBox.Location = new System.Drawing.Point(2, 0);
|
||||
darkModeCheckBox.Margin = new Padding(2, 0, 0, 0);
|
||||
darkModeCheckBox.Name = "darkModeCheckBox";
|
||||
darkModeCheckBox.Size = new System.Drawing.Size(96, 19);
|
||||
darkModeCheckBox.TabIndex = 1;
|
||||
darkModeCheckBox.Text = "Enable Dark Mode";
|
||||
darkModeCheckBox.UseVisualStyleBackColor = true;
|
||||
darkModeCheckBox.CheckedChanged += OnDarkModeCheckBoxChanged;
|
||||
darkModeFlowPanel.Controls.Add(darkModeCheckBox);
|
||||
//
|
||||
// allCheckBoxLayoutPanel
|
||||
//
|
||||
allCheckBoxLayoutPanel.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||
allCheckBoxLayoutPanel.AutoSize = true;
|
||||
allCheckBoxLayoutPanel.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
allCheckBoxLayoutPanel.Controls.Add(allCheckBox);
|
||||
allCheckBoxLayoutPanel.Location = new System.Drawing.Point(562, -1);
|
||||
allCheckBoxLayoutPanel.Margin = new Padding(0);
|
||||
allCheckBoxLayoutPanel.Margin = new Padding(12, 0, 0, 0);
|
||||
allCheckBoxLayoutPanel.Name = "allCheckBoxLayoutPanel";
|
||||
allCheckBoxLayoutPanel.Size = new System.Drawing.Size(42, 19);
|
||||
allCheckBoxLayoutPanel.TabIndex = 1006;
|
||||
allCheckBoxLayoutPanel.TabIndex = 1007;
|
||||
allCheckBoxLayoutPanel.WrapContents = false;
|
||||
//
|
||||
// allCheckBox
|
||||
//
|
||||
allCheckBox.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||
allCheckBox.AutoSize = true;
|
||||
allCheckBox.Checked = true;
|
||||
allCheckBox.CheckState = CheckState.Checked;
|
||||
@@ -211,7 +272,7 @@ namespace CreamInstaller.Forms
|
||||
selectionTreeView.FullRowSelect = true;
|
||||
selectionTreeView.Location = new System.Drawing.Point(3, 19);
|
||||
selectionTreeView.Name = "selectionTreeView";
|
||||
selectionTreeView.Size = new System.Drawing.Size(604, 265);
|
||||
selectionTreeView.Size = new System.Drawing.Size(604, 230);
|
||||
selectionTreeView.TabIndex = 1001;
|
||||
//
|
||||
// progressBar
|
||||
@@ -285,6 +346,7 @@ namespace CreamInstaller.Forms
|
||||
//
|
||||
sortCheckBox.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
|
||||
sortCheckBox.AutoSize = true;
|
||||
sortCheckBox.Checked = true; // Enable Sort By Name by default
|
||||
sortCheckBox.Location = new System.Drawing.Point(84, 380);
|
||||
sortCheckBox.Margin = new Padding(3, 0, 0, 0);
|
||||
sortCheckBox.Name = "sortCheckBox";
|
||||
@@ -352,12 +414,39 @@ namespace CreamInstaller.Forms
|
||||
saveFlowPanel.TabIndex = 10008;
|
||||
saveFlowPanel.WrapContents = false;
|
||||
//
|
||||
// topOptionsTable
|
||||
//
|
||||
topOptionsTable.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
|
||||
topOptionsTable.AutoSize = true;
|
||||
topOptionsTable.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
topOptionsTable.ColumnCount = 6;
|
||||
topOptionsTable.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
topOptionsTable.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
topOptionsTable.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
topOptionsTable.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); // spacer
|
||||
topOptionsTable.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
topOptionsTable.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
topOptionsTable.Location = new System.Drawing.Point(12, 12);
|
||||
topOptionsTable.Margin = new Padding(0);
|
||||
topOptionsTable.Name = "topOptionsTable";
|
||||
topOptionsTable.RowCount = 1;
|
||||
topOptionsTable.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
topOptionsTable.Size = new System.Drawing.Size(610, 25);
|
||||
topOptionsTable.TabIndex = 10009;
|
||||
topOptionsTable.Controls.Clear();
|
||||
topOptionsTable.Controls.Add(blockedGamesFlowPanel, 0, 0);
|
||||
topOptionsTable.Controls.Add(useSmokeAPILayoutPanel, 1, 0);
|
||||
topOptionsTable.Controls.Add(darkModeFlowPanel, 2, 0);
|
||||
topOptionsTable.Controls.Add(proxyFlowPanel, 4, 0);
|
||||
topOptionsTable.Controls.Add(allCheckBoxLayoutPanel, 5, 0);
|
||||
//
|
||||
// SelectForm
|
||||
//
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
AutoScaleMode = AutoScaleMode.Dpi;
|
||||
AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
ClientSize = new System.Drawing.Size(634, 411);
|
||||
Controls.Add(topOptionsTable);
|
||||
Controls.Add(saveFlowPanel);
|
||||
Controls.Add(sortCheckBox);
|
||||
Controls.Add(progressLabelDLCs);
|
||||
@@ -378,11 +467,14 @@ namespace CreamInstaller.Forms
|
||||
Text = "SelectForm";
|
||||
Load += OnLoad;
|
||||
programsGroupBox.ResumeLayout(false);
|
||||
programsGroupBox.PerformLayout();
|
||||
proxyFlowPanel.ResumeLayout(false);
|
||||
proxyFlowPanel.PerformLayout();
|
||||
blockedGamesFlowPanel.ResumeLayout(false);
|
||||
blockedGamesFlowPanel.PerformLayout();
|
||||
useSmokeAPILayoutPanel.ResumeLayout(false);
|
||||
useSmokeAPILayoutPanel.PerformLayout();
|
||||
darkModeFlowPanel.ResumeLayout(false);
|
||||
darkModeFlowPanel.PerformLayout();
|
||||
allCheckBoxLayoutPanel.ResumeLayout(false);
|
||||
allCheckBoxLayoutPanel.PerformLayout();
|
||||
saveFlowPanel.ResumeLayout(false);
|
||||
@@ -404,7 +496,11 @@ namespace CreamInstaller.Forms
|
||||
private CustomTreeView selectionTreeView;
|
||||
private CheckBox blockedGamesCheckBox;
|
||||
private Button blockProtectedHelpButton;
|
||||
private CheckBox useSmokeAPICheckBox;
|
||||
private Button useSmokeAPIHelpButton;
|
||||
private FlowLayoutPanel blockedGamesFlowPanel;
|
||||
private FlowLayoutPanel useSmokeAPILayoutPanel;
|
||||
private FlowLayoutPanel darkModeFlowPanel;
|
||||
private FlowLayoutPanel allCheckBoxLayoutPanel;
|
||||
private Button uninstallButton;
|
||||
private Label progressLabelGames;
|
||||
@@ -416,6 +512,8 @@ namespace CreamInstaller.Forms
|
||||
private Button loadButton;
|
||||
private Button resetButton;
|
||||
private FlowLayoutPanel saveFlowPanel;
|
||||
private TableLayoutPanel topOptionsTable;
|
||||
private CheckBox darkModeCheckBox;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ internal sealed partial class SelectForm : CustomForm
|
||||
private SelectForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
selectionTreeView.TreeViewNodeSorter = PlatformIdComparer.NodeName;
|
||||
selectionTreeView.TreeViewNodeSorter = sortCheckBox.Checked ? PlatformIdComparer.NodeText : PlatformIdComparer.NodeName;
|
||||
Text = Program.ApplicationName;
|
||||
}
|
||||
|
||||
@@ -109,7 +109,12 @@ internal sealed partial class SelectForm : CustomForm
|
||||
UpdateRemainingDLCs();
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<T> WithTimeout<T>(Task<T> task, int millisecondsTimeout)
|
||||
{
|
||||
if (await Task.WhenAny(task, Task.Delay(millisecondsTimeout)) == task)
|
||||
return await task;
|
||||
return default;
|
||||
}
|
||||
private async Task GetApplicablePrograms(IProgress<int> progress, bool uninstallAll = false)
|
||||
{
|
||||
if (!uninstallAll && (programsToScan is null || programsToScan.Count < 1))
|
||||
@@ -199,7 +204,7 @@ internal sealed partial class SelectForm : CustomForm
|
||||
return;
|
||||
StoreAppData storeAppData = await SteamStore.QueryStoreAPI(appId);
|
||||
_ = Interlocked.Decrement(ref steamGamesToCheck);
|
||||
CmdAppData cmdAppData = await SteamCMD.GetAppInfo(appId, branch, buildId);
|
||||
CmdAppData cmdAppData = await WithTimeout(SteamCMD.GetAppInfo(appId, branch, buildId), 16000);
|
||||
if (storeAppData is null && cmdAppData is null)
|
||||
{
|
||||
RemoveFromRemainingGames(name);
|
||||
@@ -246,7 +251,7 @@ internal sealed partial class SelectForm : CustomForm
|
||||
}
|
||||
else
|
||||
{
|
||||
CmdAppData dlcCmdAppData = await SteamCMD.GetAppInfo(dlcAppId);
|
||||
CmdAppData dlcCmdAppData = await WithTimeout(SteamCMD.GetAppInfo(dlcAppId), 16000);
|
||||
if (dlcCmdAppData is not null)
|
||||
{
|
||||
dlcName = dlcCmdAppData.Common?.Name;
|
||||
@@ -341,7 +346,7 @@ internal sealed partial class SelectForm : CustomForm
|
||||
selection.Icon = IconGrabber.SteamAppImagesPath + @$"\{appId}\{cmdAppData?.Common?.Icon}.jpg";
|
||||
selection.SubIcon = storeAppData?.HeaderImage ?? IconGrabber.SteamAppImagesPath
|
||||
+ @$"\{appId}\{cmdAppData?.Common?.ClientIcon}.ico";
|
||||
selection.Publisher = storeAppData?.Publishers[0] ?? cmdAppData?.Extended?.Publisher;
|
||||
selection.Publisher = storeAppData?.Publishers?.FirstOrDefault() ?? cmdAppData?.Extended?.Publisher;
|
||||
selection.Website = storeAppData?.Website;
|
||||
if (Program.Canceled)
|
||||
return;
|
||||
@@ -548,6 +553,8 @@ internal sealed partial class SelectForm : CustomForm
|
||||
Program.Canceled = false;
|
||||
blockedGamesCheckBox.Enabled = false;
|
||||
blockProtectedHelpButton.Enabled = false;
|
||||
useSmokeAPICheckBox.Enabled = false;
|
||||
useSmokeAPIHelpButton.Enabled = false;
|
||||
cancelButton.Enabled = true;
|
||||
scanButton.Enabled = false;
|
||||
noneFoundLabel.Visible = false;
|
||||
@@ -562,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))
|
||||
{
|
||||
@@ -694,6 +703,8 @@ internal sealed partial class SelectForm : CustomForm
|
||||
scanButton.Enabled = true;
|
||||
blockedGamesCheckBox.Enabled = true;
|
||||
blockProtectedHelpButton.Enabled = true;
|
||||
useSmokeAPICheckBox.Enabled = true;
|
||||
useSmokeAPIHelpButton.Enabled = true;
|
||||
}
|
||||
|
||||
private void OnTreeViewNodeCheckedChanged(object sender, TreeViewEventArgs e)
|
||||
@@ -782,6 +793,7 @@ internal sealed partial class SelectForm : CustomForm
|
||||
=> Invoke(() =>
|
||||
{
|
||||
ContextMenuStrip contextMenuStrip = new();
|
||||
ThemeManager.ApplyContextMenu(contextMenuStrip);
|
||||
ToolStripItemCollection items = contextMenuStrip.Items;
|
||||
string id = node.Name;
|
||||
Platform platform = (Platform)node.Tag;
|
||||
@@ -887,8 +899,8 @@ internal sealed partial class SelectForm : CustomForm
|
||||
foreach (string directory in directories)
|
||||
{
|
||||
directory.GetScreamApiComponents(out string api32, out string api32_o, out string api64,
|
||||
out string api64_o, out string config,
|
||||
out string log);
|
||||
out string api64_o, out string old_config, out string config,
|
||||
out string old_log, out string log);
|
||||
if (api32.FileExists() || api32_o.FileExists() || api64.FileExists() || api64_o.FileExists() ||
|
||||
config.FileExists() || log.FileExists())
|
||||
_ = items.Add(new ContextMenuItem($"Open EOS Directory #{++epic}", "File Explorer",
|
||||
@@ -964,7 +976,7 @@ internal sealed partial class SelectForm : CustomForm
|
||||
|
||||
private void OnLoad(object sender, EventArgs _)
|
||||
{
|
||||
retry:
|
||||
retry:
|
||||
try
|
||||
{
|
||||
HideProgressBar();
|
||||
@@ -1095,7 +1107,7 @@ internal sealed partial class SelectForm : CustomForm
|
||||
|
||||
private static bool CanLoadProxy() => ProgramData.ReadProxyChoices().Any();
|
||||
|
||||
private bool CanLoadSelections() => CanLoadDlc() || CanLoadProxy();
|
||||
private static bool CanLoadSelections() => CanLoadDlc() || CanLoadProxy();
|
||||
|
||||
private void OnLoadSelections(object sender, EventArgs e)
|
||||
{
|
||||
@@ -1171,7 +1183,7 @@ internal sealed partial class SelectForm : CustomForm
|
||||
saveButton.Enabled = CanSaveSelections();
|
||||
resetButton.Enabled = CanResetSelections();
|
||||
proxyAllCheckBox.CheckedChanged -= OnProxyAllCheckBoxChanged;
|
||||
proxyAllCheckBox.Checked = Selection.All.Keys.All(selection => selection.UseProxy);
|
||||
proxyAllCheckBox.Checked = Selection.All.Keys.Count != 0 && Selection.All.Keys.All(selection => selection.UseProxy);
|
||||
proxyAllCheckBox.CheckedChanged += OnProxyAllCheckBoxChanged;
|
||||
}
|
||||
|
||||
@@ -1206,8 +1218,42 @@ internal sealed partial class SelectForm : CustomForm
|
||||
: blockedDirectoryExceptions),
|
||||
customFormText: "Block Protected Games");
|
||||
}
|
||||
private void OnUseSmokeAPICheckBoxChanged(object sender, EventArgs e)
|
||||
{
|
||||
Program.UseSmokeAPI = useSmokeAPICheckBox.Checked;
|
||||
OnLoad(forceProvideChoices: false);
|
||||
}
|
||||
|
||||
private void OnUseSmokeAPIHelpButtonClicked(object sender, EventArgs e)
|
||||
{
|
||||
using DialogForm form = new(this);
|
||||
_ = form.Show(SystemIcons.Information,
|
||||
"[Experimental] WARNING: This may still be unstable.\n" +
|
||||
"This setting restores the use of SmokeAPI.\n" +
|
||||
"If some games don't launch with SmokeAPI enabled, try disabling this setting then Generate and Install again.",
|
||||
customFormText: "Use SmokeAPI");
|
||||
}
|
||||
|
||||
private void OnSortCheckBoxChanged(object sender, EventArgs e)
|
||||
=> selectionTreeView.TreeViewNodeSorter =
|
||||
sortCheckBox.Checked ? PlatformIdComparer.NodeText : PlatformIdComparer.NodeName;
|
||||
|
||||
private void programsGroupBox_Enter(object sender, EventArgs e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void OnDarkModeCheckBoxChanged(object sender, EventArgs e)
|
||||
{
|
||||
Program.DarkModeEnabled = darkModeCheckBox.Checked;
|
||||
ThemeManager.ApplyToAllOpenForms();
|
||||
}
|
||||
|
||||
protected override void OnShown(EventArgs e)
|
||||
{
|
||||
base.OnShown(e);
|
||||
ThemeManager.Apply(this);
|
||||
if (darkModeCheckBox is not null)
|
||||
darkModeCheckBox.Checked = Program.DarkModeEnabled;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
|
||||
@@ -40,6 +40,7 @@ internal sealed partial class UpdateForm : CustomForm
|
||||
#if DEBUG
|
||||
DebugForm.Current.Attach(form);
|
||||
#endif
|
||||
ThemeManager.Apply(form); // apply current theme when transitioning
|
||||
}
|
||||
|
||||
private async void OnLoad()
|
||||
|
||||
@@ -20,7 +20,7 @@ internal static class EpicLibrary
|
||||
epicManifestsPath ??=
|
||||
Registry.GetValue(@"HKEY_CURRENT_USER\Software\Epic Games\EOS", "ModSdkMetadataDir", null) as string;
|
||||
epicManifestsPath ??=
|
||||
Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Epic Games\EpicGamesLauncher", "AppDataPath",
|
||||
Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Epic Games\EpicGamesLauncher", "AppDataPath",
|
||||
null) as string;
|
||||
if (epicManifestsPath is not null && epicManifestsPath.EndsWith(@"\Data", StringComparison.Ordinal))
|
||||
epicManifestsPath += @"\Manifests";
|
||||
|
||||
@@ -8,6 +8,10 @@ using CreamInstaller.Platforms.Epic.GraphQL;
|
||||
using CreamInstaller.Utility;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
#if DEBUG
|
||||
using CreamInstaller.Forms;
|
||||
#endif
|
||||
|
||||
namespace CreamInstaller.Platforms.Epic;
|
||||
|
||||
internal static class EpicStore
|
||||
@@ -19,11 +23,22 @@ internal static class EpicStore
|
||||
{
|
||||
List<(string id, string name, string product, string icon, string developer)> dlcIds = [];
|
||||
string cacheFile = ProgramData.AppInfoPath + @$"\{categoryNamespace}.json";
|
||||
string fileContent = cacheFile.ReadFile();
|
||||
if (string.IsNullOrWhiteSpace(fileContent) || fileContent.Trim() == "null")
|
||||
{
|
||||
cacheFile.DeleteFile();
|
||||
}
|
||||
bool cachedExists = cacheFile.FileExists();
|
||||
Response response = null;
|
||||
if (!cachedExists || ProgramData.CheckCooldown(categoryNamespace, Cooldown))
|
||||
{
|
||||
response = await QueryGraphQL(categoryNamespace);
|
||||
#if DEBUG
|
||||
if (response is null)
|
||||
{
|
||||
DebugForm.Current.Log("ES: QueryGraphQL returned null");
|
||||
}
|
||||
#endif
|
||||
try
|
||||
{
|
||||
cacheFile.WriteFile(JsonConvert.SerializeObject(response, Formatting.Indented));
|
||||
@@ -43,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;
|
||||
@@ -66,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;
|
||||
@@ -103,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)
|
||||
@@ -114,6 +130,8 @@ internal static class EpicStore
|
||||
dlcIds.Add((id, title, product, icon, developer));
|
||||
}
|
||||
|
||||
public static bool EpicBool = true;
|
||||
|
||||
private static async Task<Response> QueryGraphQL(string categoryNamespace)
|
||||
{
|
||||
try
|
||||
@@ -125,9 +143,14 @@ internal static class EpicStore
|
||||
content.Headers.ContentType = new("application/json");
|
||||
HttpClient client = HttpClientManager.HttpClient;
|
||||
if (client is null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugForm.Current.Log("ES: Client returned null");
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
HttpResponseMessage httpResponse =
|
||||
await client.PostAsync(new Uri("https://graphql.epicgames.com/graphql"), content);
|
||||
await client.PostAsync(new Uri("https://launcher.store.epicgames.com/graphql"), content);
|
||||
_ = httpResponse.EnsureSuccessStatusCode();
|
||||
string response = await httpResponse.Content.ReadAsStringAsync();
|
||||
return JsonConvert.DeserializeObject<Response>(response);
|
||||
|
||||
@@ -111,7 +111,7 @@ internal static class ParadoxLauncher
|
||||
if (steamOriginalSdk64 is null && api64.FileExists() &&
|
||||
!api64.IsResourceFile(ResourceIdentifier.Steamworks64))
|
||||
steamOriginalSdk64 = api64.ReadFileBytes(true);
|
||||
directory.GetScreamApiComponents(out api32, out api32_o, out api64, out api64_o, out _, out _);
|
||||
directory.GetScreamApiComponents(out api32, out api32_o, out api64, out api64_o, out _, out _, out _, out _);
|
||||
screamInstalled = screamInstalled || api32_o.FileExists() || api64_o.FileExists()
|
||||
|| api32.FileExists() && api32.IsResourceFile(ResourceIdentifier.EpicOnlineServices32)
|
||||
|| api64.FileExists() && api64.IsResourceFile(ResourceIdentifier.EpicOnlineServices64);
|
||||
@@ -165,7 +165,7 @@ internal static class ParadoxLauncher
|
||||
else
|
||||
await CreamAPI.Install(directory, selection, generateConfig: false);
|
||||
|
||||
directory.GetScreamApiComponents(out api32, out _, out api64, out _, out _, out _);
|
||||
directory.GetScreamApiComponents(out api32, out _, out api64, out _, out _, out _, out _, out _);
|
||||
if (epicOriginalSdk32 is not null && api32.IsResourceFile(ResourceIdentifier.EpicOnlineServices32))
|
||||
{
|
||||
epicOriginalSdk32.WriteResource(api32);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -19,7 +19,7 @@ internal static class Program
|
||||
? index
|
||||
: Application.ProductVersion.Length)];
|
||||
|
||||
internal const string RepositoryOwner = "pointfeev";
|
||||
internal const string RepositoryOwner = "FroggMaster";
|
||||
internal static readonly string RepositoryName = Name;
|
||||
internal static readonly string RepositoryPackage = Name + ".zip";
|
||||
internal static readonly string RepositoryExecutable = Name + ".exe";
|
||||
@@ -35,14 +35,17 @@ internal static class Program
|
||||
internal static readonly string CurrentProcessFilePath = CurrentProcess.MainModule?.FileName;
|
||||
internal static readonly int CurrentProcessId = CurrentProcess.Id;
|
||||
|
||||
// this may forever be false, but who knows, maybe acidicoala makes it once again better than CreamAPI some day
|
||||
internal const bool UseSmokeAPI = false;
|
||||
// Setting is now toggleable. Huzzah!
|
||||
internal static bool UseSmokeAPI = true;
|
||||
|
||||
internal static bool BlockProtectedGames = true;
|
||||
internal static readonly string[] ProtectedGames = ["PAYDAY 2"];
|
||||
internal static readonly string[] ProtectedGameDirectories = [@"\EasyAntiCheat", @"\BattlEye"];
|
||||
internal static readonly string[] ProtectedGameDirectoryExceptions = [];
|
||||
|
||||
// Dark mode enabled by default
|
||||
internal static bool DarkModeEnabled = true;
|
||||
|
||||
internal static bool IsGameBlocked(string name, string directory = null)
|
||||
=> BlockProtectedGames && (ProtectedGames.Contains(name) || directory is not null &&
|
||||
!ProtectedGameDirectoryExceptions.Contains(name)
|
||||
@@ -70,6 +73,8 @@ internal static class Program
|
||||
#if DEBUG
|
||||
DebugForm.Current.Attach(form);
|
||||
#endif
|
||||
// Apply initial theme (dark by default)
|
||||
Utility.ThemeManager.Apply(form);
|
||||
Application.Run(form);
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -97,4 +102,4 @@ internal static class Program
|
||||
Cleanup();
|
||||
HttpClientManager.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,19 +37,14 @@ internal static class CreamAPI
|
||||
_ = dlc.Add(extraDlc);
|
||||
|
||||
config.DeleteFile();
|
||||
if (dlc.Count > 0)
|
||||
{
|
||||
config.CreateFile(true, installForm)?.Close();
|
||||
StreamWriter writer = new(config, true, Encoding.Default);
|
||||
WriteConfig(writer, selection.Name, !int.TryParse(selection.Id, out _) ? "0" : selection.Id,
|
||||
new(dlc.ToDictionary(_dlc => _dlc.Id, _dlc => _dlc.Name), PlatformIdComparer.String), installForm);
|
||||
writer.Flush();
|
||||
writer.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
installForm?.UpdateUser($"Deleted unnecessary configuration: {Path.GetFileName(config)}", LogTextBox.Action,
|
||||
false);
|
||||
installForm?.UpdateUser($"Deleted unnecessary configuration: {Path.GetFileName(config)}", LogTextBox.Action, false);
|
||||
config.CreateFile(true, installForm)?.Close();
|
||||
StreamWriter writer = new(config, true, Encoding.Default);
|
||||
WriteConfig(writer, selection.Name, !int.TryParse(selection.Id, out _) ? "0" : selection.Id,
|
||||
new(dlc.ToDictionary(_dlc => _dlc.Id, _dlc => _dlc.Name), PlatformIdComparer.String), installForm);
|
||||
writer.Flush();
|
||||
writer.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
private static void WriteConfig(StreamWriter writer, string name, string appId,
|
||||
|
||||
@@ -35,10 +35,11 @@ internal static class Koaloader
|
||||
into resource
|
||||
select directory + @"\" + resource;
|
||||
|
||||
internal static void GetKoaloaderComponents(this string directory, out string old_config, out string config)
|
||||
internal static void GetKoaloaderComponents(this string directory, out string old_config, out string config, out string log)
|
||||
{
|
||||
old_config = directory + @"\Koaloader.json";
|
||||
config = directory + @"\Koaloader.config.json";
|
||||
log = directory + @"\Koaloader.log";
|
||||
}
|
||||
|
||||
private static void WriteProxy(this string path, string proxyName, BinaryType binaryType)
|
||||
@@ -66,7 +67,7 @@ internal static class Koaloader
|
||||
|
||||
private static void CheckConfig(string directory, InstallForm installForm = null)
|
||||
{
|
||||
directory.GetKoaloaderComponents(out string old_config, out string config);
|
||||
directory.GetKoaloaderComponents(out string old_config, out string config, out _);
|
||||
if (old_config.FileExists())
|
||||
{
|
||||
if (!config.FileExists())
|
||||
@@ -86,28 +87,26 @@ internal static class Koaloader
|
||||
|
||||
SortedList<string, string> targets = new(PlatformIdComparer.String);
|
||||
SortedList<string, string> modules = new(PlatformIdComparer.String);
|
||||
if (targets.Count > 0 || modules.Count > 0)
|
||||
{
|
||||
/*if (installForm is not null)
|
||||
installForm.UpdateUser("Generating Koaloader configuration for " + selection.Name + $" in directory \"{directory}\" . . . ", LogTextBox.Operation);*/
|
||||
config.CreateFile(true, installForm)?.Close();
|
||||
StreamWriter writer = new(config, true, Encoding.UTF8);
|
||||
WriteConfig(writer, targets, modules, installForm);
|
||||
writer.Flush();
|
||||
writer.Close();
|
||||
}
|
||||
else if (config.FileExists())
|
||||
if (config.FileExists())
|
||||
{
|
||||
config.DeleteFile();
|
||||
installForm?.UpdateUser($"Deleted unnecessary configuration: {Path.GetFileName(config)}", LogTextBox.Action,
|
||||
false);
|
||||
}
|
||||
/*if (installForm is not null)
|
||||
installForm.UpdateUser("Generating Koaloader configuration for " + selection.Name + $" in directory \"{directory}\" . . . ", LogTextBox.Operation);*/
|
||||
config.CreateFile(true, installForm)?.Close();
|
||||
StreamWriter writer = new(config, true, Encoding.UTF8);
|
||||
WriteConfig(writer, targets, modules, installForm);
|
||||
writer.Flush();
|
||||
writer.Close();
|
||||
}
|
||||
|
||||
private static void WriteConfig(StreamWriter writer, SortedList<string, string> targets,
|
||||
SortedList<string, string> modules, InstallForm installForm = null)
|
||||
{
|
||||
writer.WriteLine("{");
|
||||
/*writer.WriteLine(" \"$schema\": \"https://raw.githubusercontent.com/acidicoala/Koaloader/refs/tags/v3.0.4/res/Koaloader.schema.json\",");*/
|
||||
writer.WriteLine(" \"logging\": false,");
|
||||
writer.WriteLine(" \"enabled\": true,");
|
||||
writer.WriteLine(" \"auto_load\": " + (modules.Count > 0 ? "false" : "true") + ",");
|
||||
@@ -122,10 +121,10 @@ internal static class Koaloader
|
||||
installForm?.UpdateUser($"Added target to Koaloader.json with path {path}", LogTextBox.Action, false);
|
||||
}
|
||||
|
||||
writer.WriteLine(" ]");
|
||||
writer.WriteLine(" ],");
|
||||
}
|
||||
else
|
||||
writer.WriteLine(" \"targets\": []");
|
||||
writer.WriteLine(" \"targets\": [],");
|
||||
|
||||
if (modules.Count > 0)
|
||||
{
|
||||
@@ -153,7 +152,7 @@ internal static class Koaloader
|
||||
bool deleteConfig = true)
|
||||
=> await Task.Run(async () =>
|
||||
{
|
||||
directory.GetKoaloaderComponents(out string old_config, out string config);
|
||||
directory.GetKoaloaderComponents(out string old_config, out string config, out string log);
|
||||
foreach (string proxyPath in directory.GetKoaloaderProxies().Where(proxyPath
|
||||
=> proxyPath.FileExists() && proxyPath.IsResourceFile(ResourceIdentifier.Koaloader)))
|
||||
{
|
||||
@@ -771,7 +770,51 @@ internal static class Koaloader
|
||||
"85AD3B263735871F4606EF4AB98B9BBC", // Koaloader v3.0.2
|
||||
"4207947D0452C1E33428ED098DC23D26", // Koaloader v3.0.2
|
||||
"BDD0DCAE7A5FBBBA0D8B857AC34BD43C", // Koaloader v3.0.2
|
||||
"A0933D21552CC5C835416DFD7604548D" // Koaloader v3.0.2
|
||||
"A0933D21552CC5C835416DFD7604548D", // Koaloader v3.0.2
|
||||
"51C6BF6DA8C9B2E80249A3F74F5F2836", // Koaloader v3.0.4
|
||||
"9DCB246925667F0C34B306D356AEE8DA", // Koaloader v3.0.4
|
||||
"1BC45223E417D4AE2A95F46DB71DDD8B", // Koaloader v3.0.4
|
||||
"0286878E70A95C6AB93F8E2A944501CD", // Koaloader v3.0.4
|
||||
"FAC5739AF157D3245DEF458D6304D902", // Koaloader v3.0.4
|
||||
"2EAA174056BFA68AA22C43CBE6388329", // Koaloader v3.0.4
|
||||
"FA736AC428269ADE76F8795F8104FA39", // Koaloader v3.0.4
|
||||
"69A71B497A01147744CD204810ABBAD2", // Koaloader v3.0.4
|
||||
"ABDD0483D79276362A66E48869CFCF6B", // Koaloader v3.0.4
|
||||
"B544F93FFC46AFD8E5AC3FBA7930DB58", // Koaloader v3.0.4
|
||||
"6A11F1E24CBD0518EDDEB28A7237CE87", // Koaloader v3.0.4
|
||||
"7344E1F82B8E8AC08CC135F3858A1C59", // Koaloader v3.0.4
|
||||
"126DE0C021297F826F91176962B2EDF5", // Koaloader v3.0.4
|
||||
"2AC92E07DD269C5027A13E3F80C4A89A", // Koaloader v3.0.4
|
||||
"7EB835E0B131D9A4F3283D15290A16CF", // Koaloader v3.0.4
|
||||
"0E478B8AACDB2A8ED6038707CFBFC8E4", // Koaloader v3.0.4
|
||||
"88A12C9B394C5EC2A72BDAE57F10A814", // Koaloader v3.0.4
|
||||
"F4440A71D0F5D49C1ECC860DE9E3EEE6", // Koaloader v3.0.4
|
||||
"C74AD1D21DFD141A91B9699DB298FFB3", // Koaloader v3.0.4
|
||||
"3D7AFD5BAED9BB023A8BAB8715857C45", // Koaloader v3.0.4
|
||||
"D982F9E2066877C26193109BD0C90815", // Koaloader v3.0.4
|
||||
"708A3AA98C53CC1E23104544F54C6C22", // Koaloader v3.0.4
|
||||
"614119A6452F1EC5B855BBBF3D66D6DE", // Koaloader v3.0.4
|
||||
"D9971AE010AA8262AED30A4376CF32B2", // Koaloader v3.0.4
|
||||
"045F9154B561705A88DB2F1C6E44D412", // Koaloader v3.0.4
|
||||
"46629A0974EFFF141C7295B4204F5B60", // Koaloader v3.0.4
|
||||
"A8AABF2BC2711BA73F9BAFEC9C9D848D", // Koaloader v3.0.4
|
||||
"FF540D977C0DD9E32D9D452824ECDB96", // Koaloader v3.0.4
|
||||
"E3F2E6E347BA407D189C8E3DAC58178C", // Koaloader v3.0.4
|
||||
"383025E324DA842C9F6499E39B1AAB5B", // Koaloader v3.0.4
|
||||
"20143E97CA1B2B9F729F84FDEE1C0CE4", // Koaloader v3.0.4
|
||||
"9E6F063F0966C6848E7C15214ACD8774", // Koaloader v3.0.4
|
||||
"A5EAC4FD9D174EDB8C8570A6FC018254", // Koaloader v3.0.4
|
||||
"C73B276EBA0B3EBB4ECF0D4BED5E18E6", // Koaloader v3.0.4
|
||||
"6A6E803C1C8CC93902166BA949FBFF5A", // Koaloader v3.0.4
|
||||
"6DD915CF4FE47037F9BCBACE2CD36115", // Koaloader v3.0.4
|
||||
"458AC3E7DD29824A418D23A2B09CC23A", // Koaloader v3.0.4
|
||||
"F90EB4C46FB1B7ECA985C1CA245F9CBA", // Koaloader v3.0.4
|
||||
"73F4D0F5C8B3033A04660CD2B27646FC", // Koaloader v3.0.4
|
||||
"894F5D28EEA75A1F0B8172D0B06FA40E", // Koaloader v3.0.4
|
||||
"4ECE60B4EAC0461BF33AF0A6359D3679", // Koaloader v3.0.4
|
||||
"615A6A863A2F8876A1B55B09F5363A03", // Koaloader v3.0.4
|
||||
"C5BD334FA0DDDD7E5A6FBDBF836C2FE5", // Koaloader v3.0.4
|
||||
"77E8E2F05AA94D5AB03F5DE506D6B639" // Koaloader v3.0.4
|
||||
]
|
||||
};
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -174,8 +174,8 @@ internal static class Resources
|
||||
if (platform is Platform.Epic or Platform.Paradox)
|
||||
{
|
||||
subDirectory.GetScreamApiComponents(out string api32, out string api32_o, out string api64,
|
||||
out string api64_o, out string config,
|
||||
out string log);
|
||||
out string api64_o, out string old_config, out string config,
|
||||
out string old_log, out string log);
|
||||
if (api32.FileExists() || api32_o.FileExists() || api64.FileExists() || api64_o.FileExists()
|
||||
|| (config.FileExists() || log.FileExists()) && !koaloaderInstalled)
|
||||
_ = dllDirectories.Add(subDirectory);
|
||||
|
||||
@@ -14,19 +14,21 @@ internal static class ScreamAPI
|
||||
{
|
||||
internal static void GetScreamApiComponents(this string directory, out string api32, out string api32_o,
|
||||
out string api64, out string api64_o,
|
||||
out string config, out string log)
|
||||
out string old_config, out string config, out string old_log, out string log)
|
||||
{
|
||||
api32 = directory + @"\EOSSDK-Win32-Shipping.dll";
|
||||
api32_o = directory + @"\EOSSDK-Win32-Shipping_o.dll";
|
||||
api64 = directory + @"\EOSSDK-Win64-Shipping.dll";
|
||||
api64_o = directory + @"\EOSSDK-Win64-Shipping_o.dll";
|
||||
config = directory + @"\ScreamAPI.json";
|
||||
log = directory + @"\ScreamAPI.log";
|
||||
old_config = directory + @"\ScreamAPI.json";
|
||||
config = directory + @"\ScreamAPI.config.json";
|
||||
old_log = directory + @"\ScreamAPI.log";
|
||||
log = directory + @"\ScreamAPI.log.log";
|
||||
}
|
||||
|
||||
internal static void CheckConfig(string directory, Selection selection, InstallForm installForm = null)
|
||||
{
|
||||
directory.GetScreamApiComponents(out _, out _, out _, out _, out string config, out _);
|
||||
directory.GetScreamApiComponents(out _, out _, out _, out _, out string old_config, out string config, out _, out _);
|
||||
HashSet<SelectionDLC> overrideCatalogItems =
|
||||
selection.DLC.Where(dlc => dlc.Type is DLCType.Epic && !dlc.Enabled).ToHashSet();
|
||||
int entitlementCount = 0;
|
||||
@@ -52,62 +54,60 @@ internal static class ScreamAPI
|
||||
|
||||
if (injectedEntitlements.Count == entitlementCount)
|
||||
injectedEntitlements.Clear();
|
||||
if (overrideCatalogItems.Count > 0 || injectedEntitlements.Count > 0)
|
||||
{
|
||||
/*if (installForm is not null)
|
||||
installForm.UpdateUser("Generating ScreamAPI configuration for " + selection.Name + $" in directory \"{directory}\" . . . ", LogTextBox.Operation);*/
|
||||
config.CreateFile(true, installForm)?.Close();
|
||||
StreamWriter writer = new(config, true, Encoding.UTF8);
|
||||
WriteConfig(writer,
|
||||
new(overrideCatalogItems.ToDictionary(dlc => dlc.Id, dlc => dlc), PlatformIdComparer.String),
|
||||
new(injectedEntitlements.ToDictionary(dlc => dlc.Id, dlc => dlc), PlatformIdComparer.String),
|
||||
installForm);
|
||||
writer.Flush();
|
||||
writer.Close();
|
||||
}
|
||||
else if (config.FileExists())
|
||||
|
||||
if (config.FileExists())
|
||||
{
|
||||
config.DeleteFile();
|
||||
installForm?.UpdateUser($"Deleted unnecessary configuration: {Path.GetFileName(config)}", LogTextBox.Action,
|
||||
false);
|
||||
}
|
||||
/*if (installForm is not null)
|
||||
installForm.UpdateUser("Generating ScreamAPI configuration for " + selection.Name + $" in directory \"{directory}\" . . . ", LogTextBox.Operation);*/
|
||||
config.CreateFile(true, installForm)?.Close();
|
||||
StreamWriter writer = new(config, true, Encoding.UTF8);
|
||||
WriteConfig(writer,
|
||||
new(overrideCatalogItems.ToDictionary(dlc => dlc.Id, dlc => dlc), PlatformIdComparer.String),
|
||||
new(injectedEntitlements.ToDictionary(dlc => dlc.Id, dlc => dlc), PlatformIdComparer.String),
|
||||
installForm);
|
||||
writer.Flush();
|
||||
writer.Close();
|
||||
}
|
||||
|
||||
private static void WriteConfig(StreamWriter writer, SortedList<string, SelectionDLC> overrideCatalogItems,
|
||||
SortedList<string, SelectionDLC> injectedEntitlements, InstallForm installForm = null)
|
||||
{
|
||||
writer.WriteLine("{");
|
||||
writer.WriteLine(" \"version\": 2,");
|
||||
/*writer.WriteLine(" \"$schema\": \"https://raw.githubusercontent.com/acidicoala/ScreamAPI/refs/tags/v4.0.0/res/ScreamAPI.schema.json\",");*/
|
||||
writer.WriteLine(" \"$version\": 3,");
|
||||
writer.WriteLine(" \"logging\": false,");
|
||||
writer.WriteLine(" \"eos_logging\": false,");
|
||||
writer.WriteLine(" \"log_eos\": false,");
|
||||
writer.WriteLine(" \"block_metrics\": false,");
|
||||
writer.WriteLine(" \"catalog_items\": {");
|
||||
writer.WriteLine(" \"unlock_all\": true,");
|
||||
writer.WriteLine(" \"namespace_id\": \"\",");
|
||||
writer.WriteLine(" \"default_dlc_status\": \"unlocked\",");
|
||||
if (overrideCatalogItems.Count > 0)
|
||||
{
|
||||
writer.WriteLine(" \"override\": [");
|
||||
writer.WriteLine(" \"override_dlc_status\": {");
|
||||
KeyValuePair<string, SelectionDLC> lastOverrideCatalogItem = overrideCatalogItems.Last();
|
||||
foreach (KeyValuePair<string, SelectionDLC> pair in overrideCatalogItems)
|
||||
{
|
||||
SelectionDLC selectionDlc = pair.Value;
|
||||
writer.WriteLine($" \"{selectionDlc.Id}\"{(pair.Equals(lastOverrideCatalogItem) ? "" : ",")}");
|
||||
writer.WriteLine($" \"{selectionDlc.Id}\": \"locked\"{(pair.Equals(lastOverrideCatalogItem) ? "" : ",")}");
|
||||
installForm?.UpdateUser(
|
||||
$"Added locked catalog item to ScreamAPI.json with id {selectionDlc.Id} ({selectionDlc.Name})",
|
||||
LogTextBox.Action,
|
||||
false);
|
||||
}
|
||||
|
||||
writer.WriteLine(" ]");
|
||||
writer.WriteLine(" },");
|
||||
}
|
||||
else
|
||||
writer.WriteLine(" \"override\": []");
|
||||
writer.WriteLine(" \"override_dlc_status\": {},");
|
||||
|
||||
writer.WriteLine(" },");
|
||||
writer.WriteLine(" \"entitlements\": {");
|
||||
if (injectedEntitlements.Count > 0)
|
||||
writer.WriteLine(" \"extra_graphql_endpoints\": [],");
|
||||
writer.WriteLine(" \"extra_entitlements\": {}");
|
||||
/*if (injectedEntitlements.Count > 0)
|
||||
{
|
||||
writer.WriteLine(" \"unlock_all\": false,");
|
||||
writer.WriteLine(" \"auto_inject\": false,");
|
||||
writer.WriteLine(" \"default_dlc_status\": original,");
|
||||
writer.WriteLine(" \"inject\": [");
|
||||
KeyValuePair<string, SelectionDLC> lastEntitlement = injectedEntitlements.Last();
|
||||
foreach (KeyValuePair<string, SelectionDLC> pair in injectedEntitlements)
|
||||
@@ -129,7 +129,7 @@ internal static class ScreamAPI
|
||||
writer.WriteLine(" \"inject\": []");
|
||||
}
|
||||
|
||||
writer.WriteLine(" }");
|
||||
writer.WriteLine(" }");*/
|
||||
writer.WriteLine("}");
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ internal static class ScreamAPI
|
||||
=> await Task.Run(() =>
|
||||
{
|
||||
directory.GetScreamApiComponents(out string api32, out string api32_o, out string api64, out string api64_o,
|
||||
out string config, out string log);
|
||||
out string old_config, out string config, out string old_log, out string log);
|
||||
if (api32_o.FileExists())
|
||||
{
|
||||
if (api32.FileExists())
|
||||
@@ -184,7 +184,7 @@ internal static class ScreamAPI
|
||||
=> await Task.Run(() =>
|
||||
{
|
||||
directory.GetScreamApiComponents(out string api32, out string api32_o, out string api64, out string api64_o,
|
||||
out _, out _);
|
||||
out _, out _, out _, out _);
|
||||
if (api32.FileExists() && !api32_o.FileExists())
|
||||
{
|
||||
api32.MoveFile(api32_o!, true);
|
||||
@@ -221,13 +221,15 @@ internal static class ScreamAPI
|
||||
[
|
||||
"069A57B1834A960193D2AD6B96926D70", // ScreamAPI v3.0.0
|
||||
"E2FB3A4A9583FDC215832E5F935E4440", // ScreamAPI v3.0.1
|
||||
"8B4B30AFAE8D7B06413EE2F2266B20DB" // ScreamAPI v4.0.0-rc01
|
||||
"8B4B30AFAE8D7B06413EE2F2266B20DB", // ScreamAPI v4.0.0-rc01
|
||||
"F2C1A6B3EF73ED14E810851DBF418453" // ScreamAPI v4.0.0
|
||||
],
|
||||
[ResourceIdentifier.EpicOnlineServices64] =
|
||||
[
|
||||
"0D62E57139F1A64F807A9934946A9474", // ScreamAPI v3.0.0
|
||||
"3875C7B735EE80C23239CC4749FDCBE6", // ScreamAPI v3.0.1
|
||||
"CBC89E2221713B0D4482F91282030A88" // ScreamAPI v4.0.0-rc01
|
||||
"CBC89E2221713B0D4482F91282030A88", // ScreamAPI v4.0.0-rc01
|
||||
"2F98D62283AA024CBD756921B9533489" // ScreamAPI v4.0.0
|
||||
]
|
||||
};
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -12,6 +12,11 @@ namespace CreamInstaller.Resources;
|
||||
|
||||
internal static class SmokeAPI
|
||||
{
|
||||
internal static readonly List<string> ProxyDLLs = ["winmm", "winhttp", "version"];
|
||||
|
||||
internal static IEnumerable<string> GetSmokeApiProxies(this string directory)
|
||||
=> from proxy in ProxyDLLs select directory + @"\" + proxy + ".dll";
|
||||
|
||||
internal static void GetSmokeApiComponents(this string directory, out string api32, out string api32_o,
|
||||
out string api64, out string api64_o,
|
||||
out string old_config, out string config, out string old_log, out string log, out string cache)
|
||||
@@ -59,27 +64,23 @@ internal static class SmokeAPI
|
||||
false);
|
||||
}
|
||||
|
||||
if (selection.ExtraSelections.Any(extraSelection => extraSelection.DLC.Any()) || overrideDlc.Count > 0 ||
|
||||
injectDlc.Count > 0)
|
||||
{
|
||||
/*if (installForm is not null)
|
||||
installForm.UpdateUser("Generating SmokeAPI configuration for " + selection.Name + $" in directory \"{directory}\" . . . ", LogTextBox.Operation);*/
|
||||
config.CreateFile(true, installForm)?.Close();
|
||||
StreamWriter writer = new(config, true, Encoding.UTF8);
|
||||
WriteConfig(writer, selection.Id,
|
||||
new(extraApps.ToDictionary(extraApp => extraApp.Key, extraApp => extraApp.Value),
|
||||
PlatformIdComparer.String),
|
||||
new(overrideDlc.ToDictionary(dlc => dlc.Id, dlc => dlc), PlatformIdComparer.String),
|
||||
new(injectDlc.ToDictionary(dlc => dlc.Id, dlc => dlc), PlatformIdComparer.String), installForm);
|
||||
writer.Flush();
|
||||
writer.Close();
|
||||
}
|
||||
else if (config.FileExists())
|
||||
if (config.FileExists())
|
||||
{
|
||||
config.DeleteFile();
|
||||
installForm?.UpdateUser($"Deleted unnecessary configuration: {Path.GetFileName(config)}", LogTextBox.Action,
|
||||
false);
|
||||
}
|
||||
/*if (installForm is not null)
|
||||
installForm.UpdateUser("Generating SmokeAPI configuration for " + selection.Name + $" in directory \"{directory}\" . . . ", LogTextBox.Operation);*/
|
||||
config.CreateFile(true, installForm)?.Close();
|
||||
StreamWriter writer = new(config, true, Encoding.UTF8);
|
||||
WriteConfig(writer, selection.Id,
|
||||
new(extraApps.ToDictionary(extraApp => extraApp.Key, extraApp => extraApp.Value),
|
||||
PlatformIdComparer.String),
|
||||
new(overrideDlc.ToDictionary(dlc => dlc.Id, dlc => dlc), PlatformIdComparer.String),
|
||||
new(injectDlc.ToDictionary(dlc => dlc.Id, dlc => dlc), PlatformIdComparer.String), installForm);
|
||||
writer.Flush();
|
||||
writer.Close();
|
||||
}
|
||||
|
||||
private static void WriteConfig(StreamWriter writer, string appId,
|
||||
@@ -88,9 +89,10 @@ internal static class SmokeAPI
|
||||
InstallForm installForm = null)
|
||||
{
|
||||
writer.WriteLine("{");
|
||||
writer.WriteLine(" \"$version\": 2,");
|
||||
/*writer.WriteLine(" \"$schema\": \"https://raw.githubusercontent.com/acidicoala/SmokeAPI/refs/tags/v4.0.0/res/SmokeAPI.schema.json\",");*/
|
||||
writer.WriteLine(" \"$version\": 4,");
|
||||
writer.WriteLine(" \"logging\": false,");
|
||||
writer.WriteLine(" \"unlock_family_sharing\": true,");
|
||||
writer.WriteLine(" \"log_steam_http\": false,");
|
||||
writer.WriteLine(" \"default_app_status\": \"unlocked\",");
|
||||
writer.WriteLine(" \"override_app_status\": {},");
|
||||
if (overrideDlc.Count > 0)
|
||||
@@ -165,12 +167,11 @@ internal static class SmokeAPI
|
||||
}
|
||||
}
|
||||
|
||||
writer.WriteLine(" },");
|
||||
writer.WriteLine(" }");
|
||||
}
|
||||
else
|
||||
writer.WriteLine(" \"extra_dlcs\": {},");
|
||||
writer.WriteLine(" \"extra_dlcs\": {}");
|
||||
|
||||
writer.WriteLine(" \"store_config\": null");
|
||||
writer.WriteLine("}");
|
||||
}
|
||||
|
||||
@@ -295,6 +296,60 @@ internal static class SmokeAPI
|
||||
CheckConfig(directory, selection, installForm);
|
||||
});
|
||||
|
||||
internal static async Task ProxyUninstall(string directory, InstallForm installForm = null,
|
||||
bool deleteOthers = true)
|
||||
=> await Task.Run(() =>
|
||||
{
|
||||
foreach (string proxy in directory.GetSmokeApiProxies().Where(proxy =>
|
||||
proxy.FileExists() && (proxy.IsResourceFile(ResourceIdentifier.Steamworks32) ||
|
||||
proxy.IsResourceFile(ResourceIdentifier.Steamworks64))))
|
||||
{
|
||||
proxy.DeleteFile(true);
|
||||
installForm?.UpdateUser($"Deleted SmokeAPI: {Path.GetFileName(proxy)}", LogTextBox.Action, false);
|
||||
}
|
||||
|
||||
if (!deleteOthers)
|
||||
return;
|
||||
directory.GetSmokeApiComponents(out _, out _, out _, out _, out string old_config, out string config, out _,
|
||||
out _, out _);
|
||||
if (config.FileExists())
|
||||
{
|
||||
config.DeleteFile();
|
||||
installForm?.UpdateUser($"Deleted configuration: {Path.GetFileName(config)}", LogTextBox.Action, false);
|
||||
}
|
||||
});
|
||||
|
||||
internal static async Task ProxyInstall(string directory, BinaryType binaryType, Selection selection,
|
||||
InstallForm installForm = null, bool generateConfig = true)
|
||||
=> await Task.Run(async () =>
|
||||
{
|
||||
await Koaloader.Uninstall(directory, selection.RootDirectory, installForm);
|
||||
|
||||
string proxy = selection.Proxy ?? Selection.DefaultProxy;
|
||||
string path = directory + @"\" + proxy + ".dll";
|
||||
foreach (string _path in directory.GetSmokeApiProxies().Where(p =>
|
||||
p != path && p.FileExists() && (p.IsResourceFile(ResourceIdentifier.Steamworks32) ||
|
||||
p.IsResourceFile(ResourceIdentifier.Steamworks64))))
|
||||
{
|
||||
_path.DeleteFile(true);
|
||||
installForm?.UpdateUser($"Deleted SmokeAPI: {Path.GetFileName(_path)}", LogTextBox.Action, false);
|
||||
}
|
||||
|
||||
if (path.FileExists() && !path.IsResourceFile(ResourceIdentifier.Steamworks32) &&
|
||||
!path.IsResourceFile(ResourceIdentifier.Steamworks64))
|
||||
throw new CustomMessageException("A non-SmokeAPI DLL named " + proxy +
|
||||
".dll already exists in this directory!");
|
||||
(binaryType == BinaryType.BIT32 ? "SmokeAPI.steam_api.dll" : "SmokeAPI.steam_api64.dll")
|
||||
.WriteManifestResource(path);
|
||||
installForm?.UpdateUser(
|
||||
$"Wrote {(binaryType == BinaryType.BIT32 ? "32-bit" : "64-bit")} SmokeAPI: {Path.GetFileName(path)}",
|
||||
LogTextBox.Action,
|
||||
false);
|
||||
|
||||
if (generateConfig)
|
||||
CheckConfig(directory, selection, installForm);
|
||||
});
|
||||
|
||||
internal static readonly Dictionary<ResourceIdentifier, HashSet<string>> ResourceMD5s = new()
|
||||
{
|
||||
[ResourceIdentifier.Steamworks32] =
|
||||
@@ -310,7 +365,13 @@ internal static class SmokeAPI
|
||||
"C8E796DDD74F2C28996EE3F41938565C", // SmokeAPI v2.0.2
|
||||
"8B075C6B272A172A014D5C9E60F13DF2", // SmokeAPI v2.0.3
|
||||
"A3873569DECAD08962C46E88352E6DB1", // SmokeAPI v2.0.4
|
||||
"4A1A823E5CF4FB861DD6BA94539D29C4" // SmokeAPI v2.0.5
|
||||
"4A1A823E5CF4FB861DD6BA94539D29C4", // SmokeAPI v2.0.5
|
||||
"EC153C0CCE476AFFB2458575930F11E6", // SmokeAPI v3.1.5
|
||||
"E833ACE855245D5939EE36FF25D8B4A4", // SmokeAPI v4.0.0
|
||||
"A2728FC65BFF3305F43F87CB6E3AE448", // SmokeAPI v4.1.0
|
||||
"CA6B8DE96022A70C45E11FF6D0B55857", // SmokeAPI v4.1.1
|
||||
"0438477117293DF1EAE1B4D87E8CE084", // SmokeAPI v4.1.2
|
||||
"2B2413E3CCDA93C3821711D089129D34" // SmokeAPI v4.1.3
|
||||
],
|
||||
[ResourceIdentifier.Steamworks64] =
|
||||
[
|
||||
@@ -325,7 +386,13 @@ internal static class SmokeAPI
|
||||
"CF9DF2E2EBA002DB98FE37FB1FB08FA8", // SmokeAPI v2.0.2
|
||||
"E4DC2AF2B8B77A0C9BF9BFBBAEA11CF7", // SmokeAPI v2.0.3
|
||||
"C0DDB49C9BFD3E05CBC1C61D117E93F9", // SmokeAPI v2.0.4
|
||||
"F7C3064D5E3C892B168F504C21AC4923" // SmokeAPI v2.0.5
|
||||
"F7C3064D5E3C892B168F504C21AC4923", // SmokeAPI v2.0.5
|
||||
"5A6712770EC7CE589252706245E62C72", // SmokeAPI v3.1.5
|
||||
"22DD39B16D3C10FDB044FDCB1BAE63B8", // SmokeAPI v4.0.0
|
||||
"997656BEB55D1D87918D0BF96BD5312F", // SmokeAPI v4.1.0
|
||||
"CD628177EC5D6303043E35DB6A83AB30", // SmokeAPI v4.1.1
|
||||
"3AC05641AA561C11BE706782B5D3C49D", // SmokeAPI v4.1.2
|
||||
"B87E96F9A52D98A957B252CDAB61CBE8" // SmokeAPI v4.1.3
|
||||
]
|
||||
};
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -31,23 +31,20 @@ internal static class UplayR1
|
||||
foreach (SelectionDLC extraDlc in selection.ExtraSelections.SelectMany(extraSelection =>
|
||||
extraSelection.DLC.Where(dlc => !dlc.Enabled)))
|
||||
_ = blacklistDlc.Add(extraDlc);
|
||||
if (blacklistDlc.Count > 0)
|
||||
{
|
||||
/*if (installForm is not null)
|
||||
installForm.UpdateUser("Generating Uplay R1 Unlocker configuration for " + selection.Name + $" in directory \"{directory}\" . . . ", LogTextBox.Operation);*/
|
||||
config.CreateFile(true, installForm)?.Close();
|
||||
StreamWriter writer = new(config, true, Encoding.UTF8);
|
||||
WriteConfig(writer, new(blacklistDlc.ToDictionary(dlc => dlc.Id, dlc => dlc), PlatformIdComparer.String),
|
||||
installForm);
|
||||
writer.Flush();
|
||||
writer.Close();
|
||||
}
|
||||
else if (config.FileExists())
|
||||
if (config.FileExists())
|
||||
{
|
||||
config.DeleteFile();
|
||||
installForm?.UpdateUser($"Deleted unnecessary configuration: {Path.GetFileName(config)}", LogTextBox.Action,
|
||||
false);
|
||||
}
|
||||
/*if (installForm is not null)
|
||||
installForm.UpdateUser("Generating Uplay R1 Unlocker configuration for " + selection.Name + $" in directory \"{directory}\" . . . ", LogTextBox.Operation);*/
|
||||
config.CreateFile(true, installForm)?.Close();
|
||||
StreamWriter writer = new(config, true, Encoding.UTF8);
|
||||
WriteConfig(writer, new(blacklistDlc.ToDictionary(dlc => dlc.Id, dlc => dlc), PlatformIdComparer.String),
|
||||
installForm);
|
||||
writer.Flush();
|
||||
writer.Close();
|
||||
}
|
||||
|
||||
private static void WriteConfig(StreamWriter writer, SortedList<string, SelectionDLC> blacklistDlc,
|
||||
|
||||
@@ -33,23 +33,20 @@ internal static class UplayR2
|
||||
foreach (SelectionDLC extraDlc in selection.ExtraSelections.SelectMany(extraSelection =>
|
||||
extraSelection.DLC.Where(dlc => !dlc.Enabled)))
|
||||
_ = blacklistDlc.Add(extraDlc);
|
||||
if (blacklistDlc.Count > 0)
|
||||
{
|
||||
/*if (installForm is not null)
|
||||
installForm.UpdateUser("Generating Uplay R2 Unlocker configuration for " + selection.Name + $" in directory \"{directory}\" . . . ", LogTextBox.Operation);*/
|
||||
config.CreateFile(true, installForm)?.Close();
|
||||
StreamWriter writer = new(config, true, Encoding.UTF8);
|
||||
WriteConfig(writer, new(blacklistDlc.ToDictionary(dlc => dlc.Id, dlc => dlc), PlatformIdComparer.String),
|
||||
installForm);
|
||||
writer.Flush();
|
||||
writer.Close();
|
||||
}
|
||||
else if (config.FileExists())
|
||||
if (config.FileExists())
|
||||
{
|
||||
config.DeleteFile();
|
||||
installForm?.UpdateUser($"Deleted unnecessary configuration: {Path.GetFileName(config)}", LogTextBox.Action,
|
||||
false);
|
||||
}
|
||||
/*if (installForm is not null)
|
||||
installForm.UpdateUser("Generating Uplay R2 Unlocker configuration for " + selection.Name + $" in directory \"{directory}\" . . . ", LogTextBox.Operation);*/
|
||||
config.CreateFile(true, installForm)?.Close();
|
||||
StreamWriter writer = new(config, true, Encoding.UTF8);
|
||||
WriteConfig(writer, new(blacklistDlc.ToDictionary(dlc => dlc.Id, dlc => dlc), PlatformIdComparer.String),
|
||||
installForm);
|
||||
writer.Flush();
|
||||
writer.Close();
|
||||
}
|
||||
|
||||
private static void WriteConfig(StreamWriter writer, SortedList<string, SelectionDLC> blacklistDlc,
|
||||
|
||||
@@ -45,6 +45,8 @@ internal sealed class Selection : IEquatable<Selection>
|
||||
{
|
||||
if (!Program.UseSmokeAPI && Platform is Platform.Steam or Platform.Paradox)
|
||||
return CreamAPI.ProxyDLLs;
|
||||
if (Program.UseSmokeAPI && Platform is Platform.Steam or Platform.Paradox)
|
||||
return SmokeAPI.ProxyDLLs;
|
||||
return EmbeddedResources.Where(r => r.StartsWith("Koaloader", StringComparison.Ordinal)).Select(p =>
|
||||
{
|
||||
p.GetProxyInfoFromIdentifier(out string proxyName, out _);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,15 @@ internal static class HttpClientManager
|
||||
internal static void Setup()
|
||||
{
|
||||
HttpClient = new();
|
||||
HttpClient.DefaultRequestHeaders.UserAgent.Add(new(Program.Name, Program.Version));
|
||||
if (CreamInstaller.Platforms.Epic.EpicStore.EpicBool)
|
||||
{
|
||||
HttpClient.DefaultRequestHeaders.UserAgent.Add(new("EpicGamesLauncher", "18.9.0-45233261+++Portal+Release-Live"));
|
||||
CreamInstaller.Platforms.Epic.EpicStore.EpicBool = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpClient.DefaultRequestHeaders.UserAgent.Add(new(Program.Name, Program.Version));
|
||||
}
|
||||
HttpClient.DefaultRequestHeaders.AcceptLanguage.Add(new(CultureInfo.CurrentCulture.ToString()));
|
||||
}
|
||||
|
||||
|
||||
@@ -21,4 +21,9 @@ internal static partial class NativeImports
|
||||
[LibraryImport("user32.dll", SetLastError = true), DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
internal static partial void SetWindowPos(nint hWnd, nint hWndInsertAfter, int x, int y, int cx, int cy,
|
||||
uint uFlags);
|
||||
|
||||
// Windows theming (scrollbars / dark mode for some controls)
|
||||
[LibraryImport("uxtheme.dll", SetLastError = true)]
|
||||
internal static partial int SetWindowTheme(nint hWnd, [MarshalAs(UnmanagedType.LPWStr)] string pszSubAppName,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string pszSubIdList);
|
||||
}
|
||||
@@ -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(() =>
|
||||
{
|
||||
|
||||
@@ -0,0 +1,607 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace CreamInstaller.Utility;
|
||||
|
||||
internal static class ThemeManager
|
||||
{
|
||||
// -----------------------------------------------------------------
|
||||
// Color definitions (do not change values)
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// ----------------------------
|
||||
// Dark mode colors
|
||||
// ----------------------------
|
||||
private static readonly Color DarkBack = ColorTranslator.FromHtml("#1E1E1E");
|
||||
private static readonly Color DarkBackAlt = ColorTranslator.FromHtml("#252525");
|
||||
private static readonly Color DarkBorder = ColorTranslator.FromHtml("#3F3F46");
|
||||
private static readonly Color DarkFore = ColorTranslator.FromHtml("#D4D4D4");
|
||||
private static readonly Color DarkForeDim = ColorTranslator.FromHtml("#9CA3AF");
|
||||
private static readonly Color Accent = ColorTranslator.FromHtml("#0E639C");
|
||||
private static readonly Color DarkLink = ColorTranslator.FromHtml("#64B5F6");
|
||||
|
||||
// CustomTreeView dark-mode specific colors
|
||||
private static readonly Color DarkPlatform = ColorTranslator.FromHtml("#FFFF99");
|
||||
private static readonly Color DarkId = ColorTranslator.FromHtml("#99FFFF");
|
||||
private static readonly Color DarkProxy = ColorTranslator.FromHtml("#99FF99");
|
||||
private static readonly Color DarkSelectionBack = ColorTranslator.FromHtml("#2A2D2E");
|
||||
private static readonly Color DarkComboBack = DarkBackAlt; // #252525
|
||||
private static readonly Color DarkComboBorder = DarkBorder; // #3F3F46
|
||||
private static readonly Color DarkComboText = DarkFore; // #D4D4D4
|
||||
|
||||
// ----------------------------
|
||||
// Light mode colors (system defaults)
|
||||
// ----------------------------
|
||||
private static readonly Color LightBack = SystemColors.Control;
|
||||
private static readonly Color LightBackAlt = SystemColors.ControlLightLight;
|
||||
private static readonly Color LightFore = SystemColors.ControlText;
|
||||
private static readonly Color LightBorder = SystemColors.ControlDark;
|
||||
|
||||
// CustomTreeView light-mode specific colors
|
||||
private static readonly Color LightPlatform = ColorTranslator.FromHtml("#696900");
|
||||
private static readonly Color LightId = ColorTranslator.FromHtml("#006969");
|
||||
private static readonly Color LightProxy = ColorTranslator.FromHtml("#006900");
|
||||
private static readonly Color LightSelectionBack = SystemColors.Highlight;
|
||||
private static readonly Color LightComboBack = SystemColors.Control;
|
||||
private static readonly Color LightComboBorder = SystemColors.ControlDark;
|
||||
private static readonly Color LightComboText = SystemColors.ControlText;
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Theme-aware properties used by other components (CustomTreeView etc.)
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
internal static bool IsDark => Program.DarkModeEnabled;
|
||||
|
||||
internal static Color CustomTreeViewPlatformColor => IsDark ? DarkPlatform : LightPlatform;
|
||||
|
||||
internal static Color CustomTreeViewIdColor => IsDark ? DarkId : LightId;
|
||||
|
||||
internal static Color CustomTreeViewProxyColor => IsDark ? DarkProxy : LightProxy;
|
||||
|
||||
internal static Color CustomTreeViewHighlightPlatformColor => DarkPlatform; // C1 (uses same color for highlight)
|
||||
internal static Color CustomTreeViewDisabledPlatformColor => ColorTranslator.FromHtml("#AAAA69"); // C3
|
||||
internal static Color CustomTreeViewHighlightIdColor => DarkId; // C4
|
||||
internal static Color CustomTreeViewDisabledIdColor => ColorTranslator.FromHtml("#69AAAA"); // C6
|
||||
internal static Color CustomTreeViewDisabledProxyColor => ColorTranslator.FromHtml("#69AA69"); // C8
|
||||
|
||||
// Background color used when a tree node is selected.
|
||||
// Keeps light-mode behavior using the system highlight, but supplies a custom dark color for dark mode
|
||||
internal static Color CustomTreeViewSelectionBackColor => IsDark ? DarkSelectionBack : LightSelectionBack;
|
||||
|
||||
internal static Color CustomTreeViewComboBackColor => IsDark ? DarkComboBack : LightComboBack;
|
||||
|
||||
internal static Color CustomTreeViewComboBorderColor => IsDark ? DarkComboBorder : LightComboBorder;
|
||||
|
||||
internal static Color CustomTreeViewComboTextColor => IsDark ? DarkComboText : LightComboText;
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Public / Internal API
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Toggle dark mode and re-apply theming to all open forms.
|
||||
/// </summary>
|
||||
internal static void ToggleDarkMode(Form anyForm)
|
||||
{
|
||||
Program.DarkModeEnabled = !Program.DarkModeEnabled;
|
||||
ApplyToAllOpenForms();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply current theme to a single form and its child controls.
|
||||
/// </summary>
|
||||
internal static void Apply(Form form)
|
||||
{
|
||||
if (form is null) return;
|
||||
if (!IsDark)
|
||||
{
|
||||
Reset(form);
|
||||
return;
|
||||
}
|
||||
|
||||
form.SuspendLayout();
|
||||
form.BackColor = DarkBack;
|
||||
form.ForeColor = DarkFore;
|
||||
ApplyTitleBar(form);
|
||||
|
||||
foreach (Control c in form.Controls)
|
||||
ApplyControlTheme(c, true);
|
||||
|
||||
form.ResumeLayout(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply the theme to all currently open forms.
|
||||
/// </summary>
|
||||
internal static void ApplyToAllOpenForms()
|
||||
{
|
||||
foreach (Form openForm in Application.OpenForms.Cast<Form>())
|
||||
Apply(openForm);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Control theming helpers
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Apply theming to a control tree. Entry point which recurses children
|
||||
/// then applies either the dark or light styling logic.
|
||||
/// </summary>
|
||||
private static void ApplyControlTheme(Control control, bool dark)
|
||||
{
|
||||
if (control is null) return;
|
||||
|
||||
// Recurse first so parent layering still works correctly
|
||||
foreach (Control child in control.Controls)
|
||||
ApplyControlTheme(child, dark);
|
||||
|
||||
if (dark)
|
||||
ApplyDarkControl(control);
|
||||
else
|
||||
ApplyLightControl(control);
|
||||
|
||||
// Try to apply themed scrollbars where applicable
|
||||
TryApplyScrollbarTheme(control, dark);
|
||||
}
|
||||
|
||||
// Separated dark/light cases to make the intent clearer and reduce duplication
|
||||
private static void ApplyDarkControl(Control control)
|
||||
{
|
||||
switch (control)
|
||||
{
|
||||
// Group box background/foreground
|
||||
case GroupBox gb:
|
||||
gb.ForeColor = DarkFore;
|
||||
gb.BackColor = DarkBackAlt;
|
||||
break;
|
||||
|
||||
// Buttons: flat appearance, border and foreground
|
||||
case Button b:
|
||||
b.FlatStyle = FlatStyle.Flat;
|
||||
b.FlatAppearance.BorderColor = DarkBorder;
|
||||
b.BackColor = DarkBackAlt;
|
||||
b.ForeColor = DarkFore;
|
||||
break;
|
||||
|
||||
// Checkboxes: match form background and foreground
|
||||
case CheckBox cb:
|
||||
cb.BackColor = DarkBack;
|
||||
cb.ForeColor = DarkFore;
|
||||
break;
|
||||
|
||||
// LinkLabel: color and active/visited styling
|
||||
case LinkLabel ll:
|
||||
ll.BackColor = DarkBack;
|
||||
ll.ForeColor = DarkFore;
|
||||
ll.LinkColor = DarkLink;
|
||||
ll.ActiveLinkColor = Color.White;
|
||||
ll.VisitedLinkColor = DarkLink;
|
||||
break;
|
||||
|
||||
// Labels: dark background, light foreground
|
||||
case Label lbl:
|
||||
lbl.BackColor = DarkBack;
|
||||
lbl.ForeColor = DarkFore;
|
||||
break;
|
||||
|
||||
// ProgressBar uses accent color for foreground
|
||||
case ProgressBar pb:
|
||||
pb.ForeColor = Accent;
|
||||
pb.BackColor = DarkBackAlt;
|
||||
break;
|
||||
|
||||
// TreeView: darker alternate background, light text, darker lines
|
||||
case TreeView tv:
|
||||
tv.BackColor = DarkBackAlt;
|
||||
tv.ForeColor = DarkFore;
|
||||
tv.LineColor = DarkBorder;
|
||||
tv.Invalidate(); // Forces a redraw
|
||||
break;
|
||||
|
||||
// RichTextBox follows alternate dark background
|
||||
case RichTextBox rtb:
|
||||
rtb.BackColor = DarkBackAlt;
|
||||
rtb.ForeColor = DarkFore;
|
||||
break;
|
||||
|
||||
// TextBox follows alternate dark background
|
||||
case TextBox tb:
|
||||
tb.BackColor = DarkBackAlt;
|
||||
tb.ForeColor = DarkFore;
|
||||
tb.BorderStyle = BorderStyle.FixedSingle;
|
||||
NativeMethods.RefreshCueBanner(tb);
|
||||
break;
|
||||
|
||||
// Layout panels set a consistent background
|
||||
case TableLayoutPanel tlp:
|
||||
tlp.BackColor = DarkBack;
|
||||
break;
|
||||
case FlowLayoutPanel flp:
|
||||
flp.BackColor = DarkBack;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyLightControl(Control control)
|
||||
{
|
||||
switch (control)
|
||||
{
|
||||
case GroupBox gb:
|
||||
gb.BackColor = LightBack;
|
||||
gb.ForeColor = LightFore;
|
||||
break;
|
||||
case Button b:
|
||||
b.FlatStyle = FlatStyle.Standard;
|
||||
b.BackColor = LightBack;
|
||||
b.ForeColor = LightFore;
|
||||
break;
|
||||
case CheckBox cb:
|
||||
cb.BackColor = LightBack;
|
||||
cb.ForeColor = LightFore;
|
||||
break;
|
||||
case LinkLabel ll:
|
||||
ll.BackColor = LightBack;
|
||||
ll.ForeColor = LightFore;
|
||||
ll.LinkColor = SystemColors.HotTrack;
|
||||
ll.ActiveLinkColor = SystemColors.Highlight;
|
||||
ll.VisitedLinkColor = SystemColors.HotTrack;
|
||||
break;
|
||||
case Label lbl:
|
||||
lbl.BackColor = LightBack;
|
||||
lbl.ForeColor = LightFore;
|
||||
break;
|
||||
case ProgressBar pb:
|
||||
pb.BackColor = LightBack;
|
||||
pb.ForeColor = LightFore;
|
||||
break;
|
||||
case TreeView tv:
|
||||
tv.BackColor = LightBack;
|
||||
tv.ForeColor = LightFore;
|
||||
tv.LineColor = LightBorder;
|
||||
tv.Invalidate(); // Forces a redraw
|
||||
break;
|
||||
case RichTextBox rtb:
|
||||
rtb.BackColor = LightBack;
|
||||
rtb.ForeColor = LightFore;
|
||||
break;
|
||||
case TextBox tb:
|
||||
tb.BackColor = LightBackAlt;
|
||||
tb.ForeColor = LightFore;
|
||||
tb.BorderStyle = BorderStyle.Fixed3D;
|
||||
NativeMethods.RefreshCueBanner(tb);
|
||||
break;
|
||||
case TableLayoutPanel tlp:
|
||||
tlp.BackColor = LightBack;
|
||||
break;
|
||||
case FlowLayoutPanel flp:
|
||||
flp.BackColor = LightBack;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Reset(Form form)
|
||||
{
|
||||
form.SuspendLayout();
|
||||
form.BackColor = LightBack;
|
||||
form.ForeColor = LightFore;
|
||||
ApplyTitleBar(form);
|
||||
foreach (Control c in form.Controls)
|
||||
ApplyControlTheme(c, false);
|
||||
form.ResumeLayout(true);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Titlebar / platform-specific helpers
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
private static void ApplyTitleBar(Form form)
|
||||
{
|
||||
try
|
||||
{
|
||||
int useDark = IsDark ? 1 : 0;
|
||||
NativeMethods.EnableDarkTitleBar(form.Handle, useDark);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private static void TryApplyScrollbarTheme(Control control, bool dark)
|
||||
{
|
||||
try
|
||||
{
|
||||
string theme = dark ? "DarkMode_Explorer" : null;
|
||||
NativeImports.SetWindowTheme(control.Handle, theme, null);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Context menu / ToolStrip theming
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Apply theme to a context menu (ContextMenuStrip).
|
||||
/// </summary>
|
||||
internal static void ApplyContextMenu(ContextMenuStrip contextMenu)
|
||||
{
|
||||
if (contextMenu is null) return;
|
||||
|
||||
bool dark = IsDark;
|
||||
|
||||
contextMenu.BackColor = dark ? DarkBackAlt : SystemColors.Menu;
|
||||
contextMenu.ForeColor = dark ? DarkFore : SystemColors.MenuText;
|
||||
contextMenu.Renderer = dark ? new DarkContextMenuRenderer() : new ToolStripProfessionalRenderer();
|
||||
|
||||
foreach (ToolStripItem item in contextMenu.Items)
|
||||
ApplyContextMenuItem(item, dark);
|
||||
}
|
||||
|
||||
private static void ApplyContextMenuItem(ToolStripItem item, bool dark)
|
||||
{
|
||||
if (item is null) return;
|
||||
|
||||
item.BackColor = dark ? DarkBackAlt : SystemColors.Menu;
|
||||
item.ForeColor = dark ? DarkFore : SystemColors.MenuText;
|
||||
|
||||
if (item is ToolStripMenuItem menuItem)
|
||||
foreach (ToolStripItem subItem in menuItem.DropDownItems)
|
||||
ApplyContextMenuItem(subItem, dark);
|
||||
}
|
||||
|
||||
internal static void ApplyToolStripDropDown(ToolStripDropDown dropDown)
|
||||
{
|
||||
if (dropDown is null) return;
|
||||
|
||||
bool dark = IsDark;
|
||||
|
||||
dropDown.BackColor = dark ? DarkBackAlt : SystemColors.Menu;
|
||||
dropDown.ForeColor = dark ? DarkFore : SystemColors.MenuText;
|
||||
dropDown.Renderer = dark ? new DarkDropDownRenderer() : new ToolStripProfessionalRenderer();
|
||||
|
||||
foreach (ToolStripItem item in dropDown.Items)
|
||||
ApplyToolStripItem(item, dark);
|
||||
}
|
||||
|
||||
private static void ApplyToolStripItem(ToolStripItem item, bool dark)
|
||||
{
|
||||
if (item is null) return;
|
||||
|
||||
item.BackColor = dark ? DarkBackAlt : SystemColors.Menu;
|
||||
item.ForeColor = dark ? DarkFore : SystemColors.MenuText;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Themed renderers for menus
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
private class DarkContextMenuRenderer : ToolStripProfessionalRenderer
|
||||
{
|
||||
public DarkContextMenuRenderer() : base(new DarkMenuColorTable()) { }
|
||||
|
||||
protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e)
|
||||
{
|
||||
if (e.Item.Selected)
|
||||
e.TextColor = DarkFore;
|
||||
base.OnRenderItemText(e);
|
||||
}
|
||||
}
|
||||
|
||||
private class DarkDropDownRenderer : ToolStripProfessionalRenderer
|
||||
{
|
||||
public DarkDropDownRenderer() : base(new DarkMenuColorTable()) { }
|
||||
|
||||
protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e)
|
||||
{
|
||||
// Force text color to stay light even when selected
|
||||
e.TextColor = DarkFore;
|
||||
base.OnRenderItemText(e);
|
||||
}
|
||||
}
|
||||
|
||||
private class DarkMenuColorTable : ProfessionalColorTable
|
||||
{
|
||||
public override Color MenuItemSelected => ColorTranslator.FromHtml("#2A2D2E");
|
||||
public override Color MenuItemSelectedGradientBegin => ColorTranslator.FromHtml("#2A2D2E");
|
||||
public override Color MenuItemSelectedGradientEnd => ColorTranslator.FromHtml("#2A2D2E");
|
||||
public override Color MenuItemBorder => ColorTranslator.FromHtml("#3F3F46");
|
||||
public override Color MenuBorder => ColorTranslator.FromHtml("#3F3F46");
|
||||
public override Color MenuItemPressedGradientBegin => ColorTranslator.FromHtml("#252525");
|
||||
public override Color MenuItemPressedGradientEnd => ColorTranslator.FromHtml("#252525");
|
||||
public override Color ImageMarginGradientBegin => ColorTranslator.FromHtml("#1E1E1E");
|
||||
public override Color ImageMarginGradientMiddle => ColorTranslator.FromHtml("#1E1E1E");
|
||||
public override Color ImageMarginGradientEnd => ColorTranslator.FromHtml("#1E1E1E");
|
||||
public override Color ToolStripDropDownBackground => ColorTranslator.FromHtml("#252525");
|
||||
public override Color SeparatorDark => ColorTranslator.FromHtml("#3F3F46");
|
||||
public override Color SeparatorLight => ColorTranslator.FromHtml("#3F3F46");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Theming helpers for CustomTreeView
|
||||
// All rendering logic for the CustomTreeView's proxy combo box and dropdown
|
||||
// button is centralized here so theming resides in ThemeManager.
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// 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");
|
||||
|
||||
/// <summary>
|
||||
/// Draws a checkbox glyph in pure GDI that matches the appearance of a dark-themed
|
||||
/// WinForms CheckBox control (same background, border, tick colors, and rounded corners).
|
||||
/// Use this in owner-draw contexts where CheckBoxRenderer always paints a white background.
|
||||
/// </summary>
|
||||
internal static void DrawDarkCheckBox(Graphics g, Point point, Size glyphSize, bool isChecked, bool enabled = true)
|
||||
{
|
||||
if (g is null) return;
|
||||
int w = glyphSize.Width;
|
||||
int h = glyphSize.Height;
|
||||
Rectangle box = new(point.X, point.Y, w - 1, h - 1);
|
||||
int radius = Math.Max(2, w / 5);
|
||||
|
||||
using System.Drawing.Drawing2D.GraphicsPath path = RoundedRect(box, radius);
|
||||
|
||||
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
|
||||
|
||||
if (isChecked && enabled)
|
||||
{
|
||||
// Checked + enabled: accent fill, no border, white tick — matches Windows 11 dark CheckBox
|
||||
using SolidBrush fillBrush = new(Accent);
|
||||
g.FillPath(fillBrush, path);
|
||||
|
||||
using Pen tickPen = new(Color.White, 1.7f)
|
||||
{
|
||||
StartCap = System.Drawing.Drawing2D.LineCap.Round,
|
||||
EndCap = System.Drawing.Drawing2D.LineCap.Round,
|
||||
LineJoin = System.Drawing.Drawing2D.LineJoin.Round,
|
||||
};
|
||||
float scaleX = w / 13f;
|
||||
float scaleY = h / 13f;
|
||||
g.DrawLines(tickPen, new PointF[]
|
||||
{
|
||||
new(point.X + 2 * scaleX, point.Y + 6 * scaleY),
|
||||
new(point.X + 5 * scaleX, point.Y + 9 * scaleY),
|
||||
new(point.X + 10 * scaleX, point.Y + 3 * scaleY),
|
||||
});
|
||||
}
|
||||
else if (isChecked)
|
||||
{
|
||||
// Checked + disabled: dimmed accent fill, dimmed tick
|
||||
Color dimAccent = Color.FromArgb(120, Accent);
|
||||
using SolidBrush fillBrush = new(dimAccent);
|
||||
g.FillPath(fillBrush, path);
|
||||
|
||||
using Pen tickPen = new(DarkForeDim, 1.7f)
|
||||
{
|
||||
StartCap = System.Drawing.Drawing2D.LineCap.Round,
|
||||
EndCap = System.Drawing.Drawing2D.LineCap.Round,
|
||||
LineJoin = System.Drawing.Drawing2D.LineJoin.Round,
|
||||
};
|
||||
float scaleX = w / 13f;
|
||||
float scaleY = h / 13f;
|
||||
g.DrawLines(tickPen, new PointF[]
|
||||
{
|
||||
new(point.X + 2 * scaleX, point.Y + 6 * scaleY),
|
||||
new(point.X + 5 * scaleX, point.Y + 9 * scaleY),
|
||||
new(point.X + 10 * scaleX, point.Y + 3 * scaleY),
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unchecked: dark fill, gray border, no tick
|
||||
using SolidBrush fillBrush = new(DarkBackAlt);
|
||||
g.FillPath(fillBrush, path);
|
||||
|
||||
using Pen borderPen = new(enabled ? DarkCbBorder : DarkCbDisabledBorder);
|
||||
g.DrawPath(borderPen, path);
|
||||
}
|
||||
|
||||
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.Default;
|
||||
}
|
||||
|
||||
private static System.Drawing.Drawing2D.GraphicsPath RoundedRect(Rectangle r, int radius)
|
||||
{
|
||||
int d = radius * 2;
|
||||
System.Drawing.Drawing2D.GraphicsPath path = new();
|
||||
path.AddArc(r.Left, r.Top, d, d, 180, 90);
|
||||
path.AddArc(r.Right - d, r.Top, d, d, 270, 90);
|
||||
path.AddArc(r.Right - d, r.Bottom - d, d, d, 0, 90);
|
||||
path.AddArc(r.Left, r.Bottom - d, d, d, 90, 90);
|
||||
path.CloseFigure();
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the themed combobox area (background, border and text) used in CustomTreeView.
|
||||
/// This centralizes colors and rendering for light/dark modes.
|
||||
/// </summary>
|
||||
internal static void DrawCustomComboBox(Graphics graphics, Rectangle rect, Font font, string text)
|
||||
{
|
||||
if (graphics is null) return;
|
||||
using SolidBrush comboBrush = new(CustomTreeViewComboBackColor);
|
||||
using Pen borderPen = new(CustomTreeViewComboBorderColor);
|
||||
graphics.FillRectangle(comboBrush, rect);
|
||||
graphics.DrawRectangle(borderPen, rect);
|
||||
// Draw text inside the combobox
|
||||
Size textSize = TextRenderer.MeasureText(graphics, text, font);
|
||||
Point textPoint = new(rect.Left +3, rect.Top + rect.Height /2 - textSize.Height /2);
|
||||
TextRenderer.DrawText(graphics, text, font, textPoint, CustomTreeViewComboTextColor, TextFormatFlags.Default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the themed dropdown button (right-side arrow) used in CustomTreeView comboboxes.
|
||||
/// </summary>
|
||||
internal static void DrawCustomComboBoxButton(Graphics graphics, Rectangle rect)
|
||||
{
|
||||
if (graphics is null) return;
|
||||
using SolidBrush comboBrush = new(CustomTreeViewComboBackColor);
|
||||
using Pen borderPen = new(CustomTreeViewComboBorderColor);
|
||||
graphics.FillRectangle(comboBrush, rect);
|
||||
graphics.DrawRectangle(borderPen, rect);
|
||||
|
||||
// Draw the arrow glyph centered in the rect
|
||||
int arrowSize =3;
|
||||
Point arrowTop = new(rect.X + rect.Width /2, rect.Y + rect.Height /2 -1);
|
||||
Point[] arrowPoints = new[]
|
||||
{
|
||||
arrowTop,
|
||||
new Point(arrowTop.X - arrowSize, arrowTop.Y - arrowSize),
|
||||
new Point(arrowTop.X + arrowSize, arrowTop.Y - arrowSize)
|
||||
};
|
||||
using SolidBrush arrowBrush = new(CustomTreeViewComboTextColor);
|
||||
graphics.FillPolygon(arrowBrush, arrowPoints);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps Win32 API calls that have no managed equivalent in WinForms.
|
||||
/// These P/Invoke declarations are required because .NET does not expose
|
||||
/// the underlying Windows messages or DWM attributes through its own APIs.
|
||||
/// </summary>
|
||||
internal static class NativeMethods
|
||||
{
|
||||
// DWM attribute index for enabling/disabling the immersive dark title bar.
|
||||
// Documented in dwmapi.h; value 20 corresponds to DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||
// (Windows 10 build 19041+ / Windows 11).
|
||||
private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
|
||||
|
||||
// DwmSetWindowAttribute allows setting per-window Desktop Window Manager attributes.
|
||||
// We use it here to flip the title bar to dark or light depending on the active theme,
|
||||
// since WinForms has no built-in API to control title bar coloring.
|
||||
[System.Runtime.InteropServices.DllImport("dwmapi.dll")]
|
||||
private static extern int DwmSetWindowAttribute(System.IntPtr hwnd, int attr, ref int attrValue, int attrSize);
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the dark/light title bar chrome for the given window handle.
|
||||
/// Pass <c>1</c> for dark mode, <c>0</c> for light mode.
|
||||
/// </summary>
|
||||
internal static void EnableDarkTitleBar(System.IntPtr handle, int useDark)
|
||||
{
|
||||
_ = DwmSetWindowAttribute(handle, DWMWA_USE_IMMERSIVE_DARK_MODE, ref useDark, sizeof(int));
|
||||
}
|
||||
|
||||
// Win32 Edit control message that sets or updates the cue (placeholder) banner text.
|
||||
// WinForms sets PlaceholderText once at creation time via this same message internally,
|
||||
// but does not re-send it when the control's colors change. When we restyle a TextBox
|
||||
// for dark/light mode the cue banner can disappear, so we must re-send the message
|
||||
// manually to make the placeholder visible again.
|
||||
private const int EM_SETCUEBANNER = 0x1501;
|
||||
|
||||
// SendMessage is the standard Win32 mechanism for posting messages directly to a
|
||||
// window/control handle. We use the Unicode variant so the placeholder string is
|
||||
// transmitted without any ANSI conversion.
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
|
||||
private static extern System.IntPtr SendMessage(System.IntPtr hWnd, int msg, System.IntPtr wParam, string lParam);
|
||||
|
||||
/// <summary>
|
||||
/// Re-sends <c>EM_SETCUEBANNER</c> to the given TextBox so its placeholder text
|
||||
/// is redrawn after a theme change has altered the control's background or foreground colors.
|
||||
/// Does nothing if the control handle has not yet been created or the placeholder is empty.
|
||||
/// </summary>
|
||||
internal static void RefreshCueBanner(System.Windows.Forms.TextBox textBox)
|
||||
{
|
||||
if (textBox?.IsHandleCreated == true && textBox.PlaceholderText is { Length: > 0 })
|
||||
SendMessage(textBox.Handle, EM_SETCUEBANNER, (System.IntPtr)1, textBox.PlaceholderText);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ $Array64 = [System.Text.StringBuilder]::new().AppendLine('[')
|
||||
function Write-Hash([System.IO.FileInfo] $File, [string] $Version) {
|
||||
$Hash = (Get-FileHash $File -Algorithm MD5).Hash
|
||||
$Value = "`t`"$Hash`", // CreamAPI $Version"
|
||||
$Value = "`t`"$Hash`", // SmokeAPI $Version"
|
||||
if ($File.Name.Contains('64')) {
|
||||
$Array64.AppendLine($Value) | Out-Null
|
||||
} else {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
### CreamInstaller: Automatic DLC Unlocker Installer & Configuration Generator
|
||||
### [Revived] CreamInstaller: Automatic DLC Unlocker Installer & Configuration Generator
|
||||
[](https://github.com/FroggMaster/CreamInstaller/releases/latest) [](https://github.com/FroggMaster/CreamInstaller/actions/workflows/ci-builds.yml)
|
||||
|
||||

|
||||

|
||||
|
||||
###### **NOTE:** This is simply a preview image; this is not a list of supported games nor configurations!
|
||||
|
||||
##### The program utilizes the latest version of [CreamAPI](https://cs.rin.ru/forum/viewtopic.php?f=29&t=70576) by [deadmau5](https://cs.rin.ru/forum/viewtopic.php?f=29&t=70576). It also utilizes the latest versions of [Koaloader](https://github.com/acidicoala/Koaloader), ~~[SmokeAPI](https://github.com/acidicoala/SmokeAPI)~~, [ScreamAPI](https://github.com/acidicoala/ScreamAPI), [Uplay R1 Unlocker](https://github.com/acidicoala/UplayR1Unlocker) and [Uplay R2 Unlocker](https://github.com/acidicoala/UplayR2Unlocker), all by [acidicoala](https://github.com/acidicoala). All unlockers are downloaded and embedded into the program itself; no further downloads necessary on your part!
|
||||
##### The program utilizes the latest version of [CreamAPI](https://cs.rin.ru/forum/viewtopic.php?f=29&t=70576) by [deadmau5](https://cs.rin.ru/forum/viewtopic.php?f=29&t=70576). It also utilizes the latest versions of [SmokeAPI](https://github.com/acidicoala/SmokeAPI), [Koaloader](https://github.com/acidicoala/Koaloader), [ScreamAPI](https://github.com/acidicoala/ScreamAPI), [Uplay R1 Unlocker](https://github.com/acidicoala/UplayR1Unlocker) and [Uplay R2 Unlocker](https://github.com/acidicoala/UplayR2Unlocker), all by [acidicoala](https://github.com/acidicoala). All unlockers are downloaded and embedded into the program itself; no further downloads necessary on your part!
|
||||
---
|
||||
#### Description:
|
||||
Automatically finds all installed Steam, Epic and Ubisoft games with their respective DLC-related DLL locations on the user's computer,
|
||||
@@ -26,11 +27,18 @@ games and DLCs the user selects; however, through the use of **right-click conte
|
||||
* Automatic DLL installation and configuration generation for CreamAPI, Koaloader, ScreamAPI, Uplay R1 Unlocker and Uplay R2 Unlocker.
|
||||
* Automatic uninstallation of DLLs and configurations for CreamAPI, Koaloader, SmokeAPI, ScreamAPI, Uplay R1 Unlocker and Uplay R2 Unlocker.
|
||||
* Automatic reparation of the Paradox Launcher (and manually via the right-click context menu "Repair" option). *For when the launcher updates whilst you have CreamAPI, SmokeAPI or ScreamAPI installed to it.*
|
||||
---
|
||||
<details>
|
||||
<summary><strong>Continuous Integration (CI) Builds</strong></summary>
|
||||
|
||||
- CreamInstaller is automatically built and tested using GitHub Actions on every push to the **main** branch. You can view all recent CI build runs by clicking the status badge at the top or here: [](https://github.com/FroggMaster/CreamInstaller/actions/workflows/ci-builds.yml)
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
#### Installation:
|
||||
1. Click [here](https://github.com/pointfeev/CreamInstaller/releases/latest/download/CreamInstaller.zip) to download the latest release from [GitHub](https://github.com/pointfeev/CreamInstaller).
|
||||
2. Extract the executable from the ZIP file to anywhere on your computer you want. *It's completely self-contained.*
|
||||
1. Click [here](https://github.com/FroggMaster/CreamInstaller/releases/latest/download/CreamInstaller.exe) to download the latest release from [GitHub](https://github.com/FroggMaster/CreamInstaller).
|
||||
2. Move the executable to anywhere on your computer you want. *It's completely self-contained.*
|
||||
|
||||
If the program doesn't seem to launch, try downloading and installing [.NET Desktop Runtime 8.0.7](https://download.visualstudio.microsoft.com/download/pr/bb581716-4cca-466e-9857-512e2371734b/5fe261422a7305171866fd7812d0976f/windowsdesktop-runtime-8.0.7-win-x64.exe) and restarting your computer. Note that the program currently only supports Windows 10+ 64-bit machines as seen [here](https://github.com/dotnet/core/blob/main/release-notes/8.0/supported-os.md).
|
||||
|
||||
@@ -49,26 +57,91 @@ If the program doesn't seem to launch, try downloading and installing [.NET Desk
|
||||
##### **NOTE:** This program does not automatically download nor install actual DLC files for you; as the title of the program states, this program is only a *DLC Unlocker* installer. Should the game you wish to unlock DLC for not already come with the DLCs installed, as is the case with a good majority of games, you must find, download and install those to the game yourself. This process includes manually installing new DLCs and manually updating the previously manually installed DLCs after game updates.
|
||||
|
||||
---
|
||||
#### FAQ / Common Issues:
|
||||
# FAQ / Common Issues
|
||||
|
||||
**Q:** The program is not launching.
|
||||
**A:** First and foremost, note that the program currently only supports Windows 10+ 64-bit machines as seen [here](https://github.com/dotnet/core/blob/main/release-notes/8.0/supported-os.md). If that does not apply to you, then make sure you've extracted the executable from the ZIP file before you've launched it, resolved your anti-virus, and have tried downloading the .NET Desktop Runtime mentioned under [installation instructions](https://github.com/pointfeev/CreamInstaller#installation) above and restarting your computer. If none of the above work, then I simply cannot do anything about it, I do not control .NET. Either your system is not supported by the current version of .NET, or something is wrong/corrupted with your system.
|
||||
### The program won't launch
|
||||
|
||||
**Q:** The game I installed the unlocker(s) to is not working/the DLCs are not unlocked.
|
||||
**A:** Make sure you've read the note under [Usage](https://github.com/pointfeev/CreamInstaller#usage) above! Assuming the program functioned as it was supposed to by properly installing DLC unlockers to your chosen games, this is not an issue I can do anything about and it's entirely up to you to seek the appropriate resources to fix it yourself (hint: https://cs.rin.ru/forum/viewforum.php?f=10).
|
||||
Check the following in order:
|
||||
|
||||
1. **System requirements**: Windows 10+ 64-bit only ([.NET 8 Supported OS List](https://github.com/dotnet/core/blob/main/release-notes/8.0/supported-os.md))
|
||||
2. **Extract before running**: Ensure you've extracted the executable from the ZIP file
|
||||
3. **Antivirus**: Add an exception for CreamInstaller (see [False Positives](#false-positive-antivirus-detections) below)
|
||||
4. **Runtime**: Install [.NET 8 Desktop Runtime](https://github.com/FroggMaster/CreamInstaller#installation) and restart your computer
|
||||
|
||||
If none of these work, your system may not support .NET 8 or may have underlying system issues.
|
||||
|
||||
---
|
||||
|
||||
### DLCs aren't unlocking in my game
|
||||
|
||||
CreamInstaller only installs DLC **unlockers** — it does **not** guarantee they will work for every game.
|
||||
|
||||
If the program successfully installs the unlockers but DLCs still aren’t unlocking, this is **not an issue with CreamInstaller itself** and isn’t something I can directly fix. DLC Unlocker compatibility and behavior vary from game to game.
|
||||
|
||||
**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 you’re 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
|
||||
|
||||
|
||||
---
|
||||
|
||||
### My antivirus detects CreamInstaller as a virus (False Positives)
|
||||
|
||||
**These are false positives.** See the detailed explanation below:
|
||||
<details>
|
||||
<summary>Click to expand for information about false positives</summary>
|
||||
|
||||
## Why Antivirus Software Flags CreamInstaller
|
||||
|
||||
CreamInstaller is **not a virus**, but it's commonly flagged because of its functionality:
|
||||
|
||||
| Reason | Explanation |
|
||||
|--------|-------------|
|
||||
| **DLL modification** | Replaces game DLLs to unlock content — behavior similar to some malware |
|
||||
| **Process hooking** | Embedded DLC unlockers interact with Steam/Epic/Ubisoft/game processes |
|
||||
| **Compressed executable** | Single-file executables are often associated with packed malware |
|
||||
| **Not code-signed** | No Extended Validation certificate ($300–500/year) means lower AV reputation (**I will not be paying for this**) |
|
||||
| **Misc** | Game modding tools frequently trigger heuristic detections regardless of intent |
|
||||
|
||||
## Common False Positive Names
|
||||
|
||||
| Detection Name | What It Usually Means / Why It’s a False Positive |
|
||||
|----------------------------------------|---------------------------------------------------|
|
||||
| Mamson.A!ac | Generic heuristic detection; often triggered by packed or obfuscated executables |
|
||||
| Phonzy.A!ml | Machine-learning detection; flags unusual behavior patterns |
|
||||
| Wacatac.H!ml | Extremely common false positive; triggered by compressed or self-updating programs |
|
||||
| Malgent!MSR | Generic Microsoft label for “suspicious behavior,” not confirmed malware |
|
||||
| Tiggre!rfn | Heuristic runtime detection often seen with tools that hook processes |
|
||||
| UDS:DangerousObject.Multi.Generic | Reputation-based detection for tools that *can* be abused |
|
||||
| Trojan.Win64.Agent | Very broad category; common false positive for unsigned binaries |
|
||||
| Trojan.Win64.Agent.oa!s1 | Cloud/AI heuristic variant of the above |
|
||||
|
||||
**See also:** [Archived issue #40](https://web.archive.org/web/20240604162435/https://github.com/pointfeev/CreamInstaller/issues/40)
|
||||
|
||||
## Verify Safety Yourself
|
||||
|
||||
CreamInstaller is **100% open source**:
|
||||
|
||||
1. **Review the source code** in this repository
|
||||
2. **Build it yourself**
|
||||
3. **Compare hashes** of your build with the official release
|
||||
|
||||
</details>
|
||||
|
||||
**Q:** The program and/or files installed by the program are detected as a virus/trojan/malware.
|
||||
**A:** The "issue" of the program's outputted Koaloader DLLs being detected as false positives such as Mamson.A!ac, Phonzy.A!ml, Wacatac.H!ml, Malgent!MSR, Tiggre!rfn, and many many others, has already been posted and explained dozens of times now in many different manners... please do not post it again, you will just be ignored; instead, refer to the explanations within issue #40 and its linked issues: https://github.com/pointfeev/CreamInstaller/issues/40.
|
||||
|
||||
---
|
||||
##### Bugs/Crashes/Issues:
|
||||
For reliable and quick assistance, all bugs, crashes and other issues should be referred to the [GitHub Issues](https://github.com/pointfeev/CreamInstaller/issues) page!
|
||||
For reliable and quick assistance, all bugs, crashes and other issues should be referred to the [GitHub Issues](https://github.com/FroggMaster/CreamInstaller/issues) page!
|
||||
|
||||
##### **HOWEVER**: Please read the [FAQ entry](https://github.com/pointfeev/CreamInstaller#faq--common-issues) above and/or [template issue](https://github.com/pointfeev/CreamInstaller/issues/new/choose) corresponding to your problem should one exist! Also, note that the [GitHub Issues](https://github.com/pointfeev/CreamInstaller/issues) page is not your personal assistance hotline, rather it is for genuine bugs/crashes/issues with the program itself. If you post an issue which is off-topic or has already been explained within the FAQ, template issues, and/or within this text in general, I will just close it and you will be ignored.
|
||||
##### **HOWEVER**: Please read the [FAQ entry](https://github.com/FroggMaster/CreamInstaller#faq--common-issues) above and/or [template issue](https://github.com/FroggMaster/CreamInstaller/issues/new/choose) corresponding to your problem should one exist! Also, note that the [GitHub Issues](https://github.com/FroggMaster/CreamInstaller/issues) page is not your personal assistance hotline, rather it is for genuine bugs/crashes/issues with the program itself. If you post an issue which is off-topic or has already been explained within the FAQ, template issues, and/or within this text in general, I will just close it and you will be ignored.
|
||||
|
||||
---
|
||||
|
||||
##### More Information:
|
||||
* SteamCMD installation and appinfo cache can be found at **C:\ProgramData\CreamInstaller**.
|
||||
* The program automatically and very quickly updates from [GitHub](https://github.com/pointfeev/CreamInstaller) by choice of the user through a dialog on startup.
|
||||
* The program source and other information can be found on [GitHub](https://github.com/pointfeev/CreamInstaller).
|
||||
* The program automatically and very quickly updates from [GitHub](https://github.com/FroggMaster/CreamInstaller) by choice of the user through a dialog on startup.
|
||||
* The program source and other information can be found on [GitHub](https://github.com/FroggMaster/CreamInstaller).
|
||||
* Credit to [Mattahan](https://www.mattahan.com) for the program icon.
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 36 KiB |
Reference in New Issue
Block a user