diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7d571e4..aa5bad5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,15 +29,18 @@ jobs:
with:
dotnet-version: "10.0.x"
- # Debug + implicit restore. -o collects the output in a known, flat path for the upload step.
- - run: dotnet build --configuration Debug -o artifacts
+ - run: dotnet build --configuration Debug
- - run: dotnet format --verify-no-changes
+ - run: dotnet build tools/PreviewSettings/PreviewSettings.csproj --configuration Debug
+
+ - run: dotnet format --verify-no-changes --no-restore
+
+ - run: dotnet format tools/PreviewSettings/PreviewSettings.csproj --verify-no-changes --no-restore
- uses: actions/upload-artifact@v4
with:
name: LiveSplit.AutoSplitIntegration
path: |
- artifacts/LiveSplit.AutoSplitIntegration.dll
- artifacts/LiveSplit.AutoSplitIntegration.pdb
+ bin/Debug/*/LiveSplit.AutoSplitIntegration.dll
+ bin/Debug/*/LiveSplit.AutoSplitIntegration.pdb
if-no-files-found: error
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 02de93c..5f5529f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -98,8 +98,7 @@ jobs:
with:
dotnet-version: "10.0.x"
- # Release build with implicit restore.
- - run: dotnet build --configuration Release -o artifacts -p:Version=$env:VERSION
+ - run: dotnet build --configuration Release -p:Version=$env:VERSION
if: steps.exists.outputs.already != 'true'
- name: Release notes from manifest
@@ -122,6 +121,6 @@ jobs:
name: v${{ needs.validate.outputs.version }}
body_path: release-notes.md
files: |
- artifacts/LiveSplit.AutoSplitIntegration.dll
- artifacts/LiveSplit.AutoSplitIntegration.pdb
+ bin/Release/*/LiveSplit.AutoSplitIntegration.dll
+ bin/Release/*/LiveSplit.AutoSplitIntegration.pdb
fail_on_unmatched_files: true
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 27a651f..025bd24 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -24,7 +24,13 @@ Then either:
- Run `dotnet build --configuration Release`, or
- Open `LiveSplit.AutoSplitIntegration.csproj` in Visual Studio 2022 and build.
-The LiveSplit assemblies needed to compile (`LiveSplit.exe`, `LiveSplit.Core.dll`, `UpdateManager.dll`) are vendored under [`lib/`](lib/). See [`lib/README.md`](lib/README.md) to refresh them.
+The LiveSplit assemblies needed to compile (`LiveSplit.exe`, and dlls) are vendored under [`lib/`](lib/). See [`lib/README.md`](lib/README.md) to refresh them.
+
+## Previewing the settings UI
+
+The component's settings panel is a WinForms `UserControl`. To preview visual changes without installing the component into a real LiveSplit, run `scripts/preview-settings.ps1`. It builds [`tools/PreviewSettings`](tools/PreviewSettings/) (a throwaway .NET WinExe that reads the component's `Settings` control by reflection and feeds it a minimal `LiveSplitState`) and launches it. Buttons are inert since there is no host.
+
+The tool works on Windows directly. On Linux it runs under [wine](https://www.winehq.org/), whose bundled Mono renders WinForms through wine's own graphics stack, so text and layout match Windows and the real LiveSplit host. Native [Mono](https://www.mono-project.com/) is intentionally not used: it renders quite differently via `libgdiplus`, on top of having known [rendering bug](https://github.com/TASEmulators/BizHawk/issues/4469), which defeats the purpose of a visual preview.
## Changelog
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..12444eb
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,41 @@
+
+
+
+ latest
+ enable
+
+
+ true
+
+
+ true
+
+
+ true
+ moderate
+
+ $(WarningsAsErrors);NU1903;NU1904
+
+ all
+
+
+
+
+
+ false
+
+ net481
+ true
+
+
diff --git a/LiveSplit.AutoSplitIntegration.csproj b/LiveSplit.AutoSplitIntegration.csproj
index 04bd226..228063d 100644
--- a/LiveSplit.AutoSplitIntegration.csproj
+++ b/LiveSplit.AutoSplitIntegration.csproj
@@ -1,47 +1,10 @@
-
-
- latest
- enable
-
-
- true
-
-
- true
-
-
- true
- moderate
-
- $(WarningsAsErrors);NU1903;NU1904
-
- all
-
-
-
- net481
- true
Library
LiveSplit
LiveSplit.AutoSplitIntegration
true
-
- false
-
true
@@ -56,6 +19,16 @@
0.0.0-dev
+
+
+
+
+
+
diff --git a/scripts/preview-settings.ps1 b/scripts/preview-settings.ps1
new file mode 100644
index 0000000..e843438
--- /dev/null
+++ b/scripts/preview-settings.ps1
@@ -0,0 +1,27 @@
+#!/usr/bin/env pwsh
+# Builds and launches the settings-UI preview project
+# so visual changes can be checked without a running LiveSplit.
+# See CONTRIBUTING.md for why Linux native Mono is not used in favor of Wine.
+$ErrorActionPreference = 'Stop'
+
+$repoRoot = Split-Path -Parent $PSScriptRoot
+$project = Join-Path $repoRoot 'tools/PreviewSettings/PreviewSettings.csproj'
+$outDir = Join-Path $repoRoot 'tools/PreviewSettings/bin/Release/net481'
+$exe = Join-Path $outDir 'PreviewSettings.exe'
+
+# Check run prerequisites before building, so a missing display or wine fails fast.
+if (-not $IsWindows) {
+ if (-not $env:DISPLAY) {
+ throw 'No X DISPLAY set. Run from a graphical session (or set DISPLAY).'
+ }
+
+ # wine only: its bundled Mono renders WinForms via wine's graphics stack, matching the real host.
+ # Native Mono (libgdiplus) drops button/label text (Mono bug, not ours: TASEmulators/BizHawk#4469).
+ if (-not (Get-Command wine -ErrorAction SilentlyContinue)) {
+ throw 'wine not found. Install wine to preview this Windows-only UI on Linux.'
+ }
+}
+
+dotnet build $project --configuration Release
+
+if ($IsWindows) { & $exe } else { & wine $exe }
diff --git a/tools/PreviewSettings/PreviewSettings.csproj b/tools/PreviewSettings/PreviewSettings.csproj
new file mode 100644
index 0000000..0bdefae
--- /dev/null
+++ b/tools/PreviewSettings/PreviewSettings.csproj
@@ -0,0 +1,37 @@
+
+
+
+ Exe
+
+ PreviewSettings
+ LiveSplit.PreviewTool
+
+
+
+
+
+
+
+
+
+ ..\..\lib\LiveSplit.exe
+ true
+
+
+ ..\..\lib\LiveSplit.Core.dll
+ true
+
+
+ ..\..\lib\UpdateManager.dll
+ true
+
+
+
diff --git a/tools/PreviewSettings/Program.cs b/tools/PreviewSettings/Program.cs
new file mode 100644
index 0000000..d8b71a5
--- /dev/null
+++ b/tools/PreviewSettings/Program.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Drawing;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Windows.Forms;
+using LiveSplit.Model;
+using LiveSplit.UI.Components;
+
+namespace LiveSplit.PreviewTool
+{
+ // Renders AutoSplitIntegrationComponentSettings without a live LiveSplit host.
+ // The component subscribes to LiveSplitState events in its ctor, so a real state is
+ // built; the settings ctor only reads state.CurrentPhase (defaults to NotRunning).
+ internal static class Program
+ {
+ [STAThread]
+ private static void Main()
+ {
+ Application.EnableVisualStyles();
+ Application.SetCompatibleTextRenderingDefault(false);
+
+ LiveSplitState state = MakeState();
+ var component = new AutoSplitIntegrationComponent(state);
+
+ // Settings is internal; the tool is a separate assembly, so reach it by reflection.
+ var settings = (Control)typeof(AutoSplitIntegrationComponent)
+ .GetProperty("Settings", BindingFlags.Instance | BindingFlags.NonPublic)!
+ .GetValue(component)!;
+
+ using var form = new Form
+ {
+ ClientSize = settings.Size,
+ Text = "AutoSplit Integration Settings (preview)",
+ StartPosition = FormStartPosition.CenterScreen,
+ Icon = TryLoadLiveSplitIcon() ?? SystemIcons.Application,
+ };
+ form.Controls.Add(settings);
+ Application.Run(form);
+ }
+
+ // LiveSplit.exe is copied next to this exe (see the vendored Reference's Private=true), so the
+ // window icon can be pulled from it at runtime instead of vendoring a separate .ico. Use shell32's
+ // ExtractIconEx rather than Icon.ExtractAssociatedIcon: the latter returns a generic placeholder
+ // under wine's Mono, while ExtractIconEx reads the real embedded icon on both Windows and wine.
+ // On wine the transparent corners render black (the HICON's 32-bit alpha is dropped); Windows is
+ // unaffected. Accepted as cosmetic for this preview-only tool.
+ private static Icon? TryLoadLiveSplitIcon()
+ {
+ var liveSplitExePath = Path.Combine(AppContext.BaseDirectory, "LiveSplit.exe");
+ var largeIcons = new IntPtr[1];
+ if (ExtractIconEx(liveSplitExePath, 0, largeIcons, null, 1) == 0 || largeIcons[0] == IntPtr.Zero)
+ return null;
+
+ try
+ {
+ // Icon.FromHandle does not own the handle, so clone to a managed icon before freeing it.
+ return (Icon)Icon.FromHandle(largeIcons[0]).Clone();
+ }
+ finally
+ {
+ DestroyIcon(largeIcons[0]);
+ }
+ }
+
+ [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
+ private static extern uint ExtractIconEx(string fileName, int iconIndex, IntPtr[] largeIcons, IntPtr[]? smallIcons, uint iconCount);
+
+ [DllImport("user32.dll")]
+ private static extern bool DestroyIcon(IntPtr handle);
+
+ private static LiveSplitState MakeState()
+ {
+ // LiveSplitState(run, form, layout, layoutSettings, settings). Only CurrentPhase is
+ // read here (enum default NotRunning), so nulls are fine; fall back to an
+ // uninitialized instance if a future ctor change starts dereferencing them.
+ object?[] args = [null, null, null, null, null];
+ return (LiveSplitState)Activator.CreateInstance(typeof(LiveSplitState), args);
+
+ }
+ }
+}