Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 3 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
8 changes: 7 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
41 changes: 41 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<Project>
<!--
The source skeleton for this configuration can be found at
https://github.com/BesLogic/shared-configs/blob/main/Directory.Build.props
Modifications to this file that are not project-specific should also be done upstream.
-->
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>

<!--
Create lock files. Allows faster restores and reproducibility
without having to pin to exact versions in csproj PackageReference.
-->
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<!-- Ensure we don't accidentally update or bypass the lock files (missing `locked-mode`) -->
<!-- lock files can still be regenerated with: dotnet restore -p:RestoreLockedMode=false -->
<RestoreLockedMode>true</RestoreLockedMode>

<!-- NuGet Security Audit https://learn.microsoft.com/en-us/nuget/concepts/auditing-packages -->
<NuGetAudit>true</NuGetAudit>
<NuGetAuditLevel>moderate</NuGetAuditLevel>
<!--
Only fail on high + critical: https://learn.microsoft.com/en-us/nuget/concepts/auditing-packages#warning-codes
Alternatively this can be made to only fail on CI:
https://learn.microsoft.com/en-us/nuget/concepts/auditing-packages#running-nuget-audit-in-ci
-->
<WarningsAsErrors>$(WarningsAsErrors);NU1903;NU1904</WarningsAsErrors>
<!-- Will be default in .NET 10 -->
<NuGetAuditMode>all</NuGetAuditMode>
</PropertyGroup>

<!-- Properties specific to this project below -->
<PropertyGroup>
<!-- No lock file: the only NuGet package is a version-pinned build-only reference, so it buys nothing. -->
<RestorePackagesWithLockFile>false</RestorePackagesWithLockFile>

<TargetFramework>net481</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
</Project>
47 changes: 10 additions & 37 deletions LiveSplit.AutoSplitIntegration.csproj
Original file line number Diff line number Diff line change
@@ -1,47 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<!--
The source skeleton for this configuration can be found at
https://github.com/BesLogic/shared-configs/blob/main/Directory.Build.props
Modifications to this file that are not project-specific should also be done upstream.
-->
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>

<!--
Create lock files. Allows faster restores and reproducibility
without having to pin to exact versions in csproj PackageReference.
-->
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<!-- Ensure we don't accidentally update or bypass the lock files (missing `locked-mode`) -->
<!-- lock files can still be regenerated with: dotnet restore -p:RestoreLockedMode=false -->
<RestoreLockedMode>true</RestoreLockedMode>

<!-- NuGet Security Audit https://learn.microsoft.com/en-us/nuget/concepts/auditing-packages -->
<NuGetAudit>true</NuGetAudit>
<NuGetAuditLevel>moderate</NuGetAuditLevel>
<!--
Only fail on high + critical: https://learn.microsoft.com/en-us/nuget/concepts/auditing-packages#warning-codes
Alternatively this can be made to only fail on CI:
https://learn.microsoft.com/en-us/nuget/concepts/auditing-packages#running-nuget-audit-in-ci
-->
<WarningsAsErrors>$(WarningsAsErrors);NU1903;NU1904</WarningsAsErrors>
<!-- Will be default in .NET 10 -->
<NuGetAuditMode>all</NuGetAuditMode>
</PropertyGroup>

<!-- Properties specific to this project below -->
<PropertyGroup>
<TargetFramework>net481</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<OutputType>Library</OutputType>
<RootNamespace>LiveSplit</RootNamespace>
<AssemblyName>LiveSplit.AutoSplitIntegration</AssemblyName>
<Deterministic>true</Deterministic>

<!-- No lock file: the only NuGet package is a version-pinned build-only reference, so it buys nothing. -->
<RestorePackagesWithLockFile>false</RestorePackagesWithLockFile>

<!-- Ship a full (Windows) PDB alongside the DLL for readable line numbers in crash reports -->
<DebugSymbols>true</DebugSymbols>
<!-- .NET Framework runtime's stack-trace reader doesn't reliably resolve portable PDBs -->
Expand All @@ -56,6 +19,16 @@
<Version>0.0.0-dev</Version>
</PropertyGroup>

<!--
Local-only dev tools live under tools/ with their own projects. This project sits at the
repo root, so its default `**/*.cs` glob would otherwise compile their sources (and their
generated obj/ attributes), breaking the product build. Keep the tools tree out.
-->
<ItemGroup>
<Compile Remove="tools/**/*.cs" />
<None Remove="tools/**/*" />
</ItemGroup>

<ItemGroup>
<!-- LiveSplit host assemblies, vendored under lib/ (see lib/README.md). -->
<!-- Private=false: provided by the host at runtime, not shipped with the plugin. -->
Expand Down
27 changes: 27 additions & 0 deletions scripts/preview-settings.ps1
Original file line number Diff line number Diff line change
@@ -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 }
37 changes: 37 additions & 0 deletions tools/PreviewSettings/PreviewSettings.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<!--
Local-only dev tool: renders the component's settings UserControl in a bare Form so
visual changes can be previewed without a running LiveSplit host. Not shipped, not
built by CI (`dotnet build` at the repo root only picks up the root project).
Launch via scripts/preview-settings.ps1. See CONTRIBUTING.md > Previewing the settings UI.
-->
<PropertyGroup>
<OutputType>Exe</OutputType>
<!-- Reflection reaches internal types; no InternalsVisibleTo needed on the product. -->
<AssemblyName>PreviewSettings</AssemblyName>
<RootNamespace>LiveSplit.PreviewTool</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\LiveSplit.AutoSplitIntegration.csproj" />
</ItemGroup>

<ItemGroup>
<!--
Product refs are Private=false (host-provided), so they don't flow here.
This tool has no host, so copy the vendored assemblies into its output for runtime resolution.
-->
<Reference Include="LiveSplit">
<HintPath>..\..\lib\LiveSplit.exe</HintPath>
<Private>true</Private>
</Reference>
<Reference Include="LiveSplit.Core">
<HintPath>..\..\lib\LiveSplit.Core.dll</HintPath>
<Private>true</Private>
</Reference>
<Reference Include="UpdateManager">
<HintPath>..\..\lib\UpdateManager.dll</HintPath>
<Private>true</Private>
</Reference>
</ItemGroup>
</Project>
82 changes: 82 additions & 0 deletions tools/PreviewSettings/Program.cs
Original file line number Diff line number Diff line change
@@ -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);

}
}
}
Loading