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); + + } + } +}