; ===================================================== ; ArtLightControl - GitHub Release Installer ; WinUI 3 (Windows App SDK 2.4) unpackaged deployment ; ===================================================== #define MyAppName "ArtLight Control" #define MyAppVersion "1.6.1" #define MyAppPublisher "onaiaku" #define MyAppExeName "ArtLightControl.exe" #define MyAppURL "https://github.com/onaiaku/ArtLight" #define ServiceName "ArtLightControlService" #define ServiceExe "ArtLightControlService.exe" #include "CodeDependencies.iss" [Setup] AppId={{D37D0ED6-5E8D-4131-B2C1-30A5840AC97B} AppName={#MyAppName} AppVersion={#MyAppVersion} UninstallDisplayName={#MyAppName} AppPublisher={#MyAppPublisher} DefaultDirName={autopf}\ArtLight\{#MyAppName} DefaultGroupName={#MyAppName} InfoBeforeFile=changelog.txt SetupIconFile=ArtLightControl\Resources\artlightcontrol.ico ; Wizard artwork lives under installer\ (not ArtLightControl\Resources\): the WinUI ; targets glob images in the app's Resources folder into the build output, which the ; [Files] sweep would then ship into {app} — installer-only assets have no business ; in the install directory. Same layout as ArtMoon. WizardSmallImageFile=installer\resources\artlightcontrol.png WizardImageFile=installer\resources\artlightcontrol-installer.png UninstallDisplayIcon={app}\Resources\artlightcontrol.ico AllowNoIcons=yes DirExistsWarning=no CloseApplications=yes Compression=lzma2 SolidCompression=yes OutputDir=Output OutputBaseFilename=ArtLightControl_{#MyAppVersion}_Installer ; WinUI 3 + Windows App SDK 2.4 require Windows 10 1903+ (build 18362) — the 2.x line ; raised this from the 1809 (17763) floor of the 1.x line. ; ArtLightControl targets 19041 (20H1), which is stricter than both, so nothing changes here. MinVersion=10.0.19041 PrivilegesRequired=admin ; 64-bit Setup binary (Inno Setup 7+). The app is x64-only, so a 32-bit installer ; bought nothing; this also gets high-entropy ASLR by default. SetupArchitecture=x64 ; x64os (not the deprecated "x64", and not "x64compatible"): ArtLightControl is the HOST ; tool — it drives the NIC via CIM, reads GPU sensors via D3DKMT/NVML and hosts the ; streaming server. Running that emulated on ARM64 is not a scenario worth supporting. ; ArtMoon, the client, deliberately uses x64compatible instead. ArchitecturesAllowed=x64os ArchitecturesInstallIn64BitMode=x64os WizardStyle=modern DisableWelcomePage=no [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" [Tasks] ; Start-with-Windows is ON by default. UsePreviousTasks (Inno default) remembers ; the user's choice on upgrades, so a user who unticked it stays unticked. ; The value written here is exactly what the in-app toggle writes ; (SettingsViewModel.StartWithWindows): "" --minimized — the app reads the ; Run key directly, so installer and app stay in sync. --minimized launches the ; app into the tray (App.xaml.cs skips window activation on that flag). Name: "autostart"; Description: "Start ArtLight Control when Windows starts (runs minimized in the tray)"; GroupDescription: "Startup:" [Messages] WelcomeLabel1=Welcome to the ArtLight Control Setup Wizard WelcomeLabel2= [Files] ; ── Main WinUI 3 application ───────────────────────────────────────────────── ; dotnet build output. ArtLightControl.Core.dll is included automatically (ProjectReference). ; Excludes: ; *.pdb — debug symbols, not needed at runtime ; ref\* — compiler-only reference assemblies ; ArtLightControl.exe.WebView2\* — WebView2 user-data folder. The app stopped creating it ; in 7.2.0 (Store tab removed), but a stale one left over ; from the 6.2.x era can still sit in bin and would be ; swept in by recursesubdirs — it holds browsing cache ; and cookies and must never ship. Safety net only. Source: "ArtLightControl\bin\x64\Release\net8.0-windows10.0.19041.0\win-x64\*"; DestDir: "{app}"; Excludes: "*.pdb,ref\*,ArtLightControl.exe.WebView2\*"; Flags: ignoreversion recursesubdirs createallsubdirs ; ── Background service (LocalSystem account, manages NIC speed via CIM) ───── Source: "ArtLightControlService\bin\x64\Release\net8.0-windows\win-x64\*"; DestDir: "{app}"; Excludes: "*.pdb,ref\*"; Flags: ignoreversion recursesubdirs createallsubdirs ; ── Release notes ──────────────────────────────────────────────────────────── Source: "changelog.txt"; DestDir: "{app}"; Flags: ignoreversion ; ── Installer wizard logo (extracted to temp for the welcome page) ──────────── Source: "installer\resources\artlightcontrol.png"; Flags: dontcopy [Icons] Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" ; Autostart on by default (task "autostart", checked). Same value the in-app ; toggle writes, so the Settings page reflects ON after install with no drift. [Registry] Root: HKCU; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; ValueType: string; ValueName: "ArtLightControl"; \ ValueData: """{app}\{#MyAppExeName}"" --minimized"; Flags: uninsdeletevalue; Tasks: autostart [Run] Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#MyAppName}}"; Flags: postinstall skipifsilent nowait [Code] // ── Stop Windows from maximising the wizard ────────────────────────────────── // Mirror of the same block in ArtMoon's installer — keep the two identical. // // On a handheld the wizard opens filling the whole screen with the layout still // drawn for a small window: artwork at natural size in the top-left, a large empty // area around it. That is a window MAXIMISED after its layout was computed, not a // wizard computed too large (which would stretch the artwork to full height). It is // not the Inno Setup version — it did the same under Inno Setup 6. // // Windows only auto-maximises windows that can be maximised, so the fix is to say // this one cannot: take the sizing frame and the maximise box off it. Setup's wizard // is not meant to be resized anyway — Inno Setup 7 dropped WizardResizable for // exactly that reason, which makes this a no-op there and a fix everywhere else. // // ⚠️ SetWindowLongW, not SetWindowLongPtrW. Both are exported by user32 on 64-bit, // but the Ptr variant takes a LONG_PTR (8 bytes) and we build a 64-bit installer // (SetupArchitecture=x64os), so handing it a 32-bit value is the argument-size // mismatch Inno Setup 7's release notes warn about. Window styles are 32-bit, so the // plain variant is the correct one for GWL_STYLE on any architecture. const GWL_STYLE = -16; WS_MAXIMIZEBOX = $00010000; WS_THICKFRAME = $00040000; SWP_NOSIZE = $0001; SWP_NOMOVE = $0002; SWP_NOZORDER = $0004; SWP_NOACTIVATE = $0010; SWP_FRAMECHANGED = $0020; function GetWindowLong(hWnd: HWND; nIndex: Integer): LongInt; external 'GetWindowLongW@user32.dll stdcall'; function SetWindowLong(hWnd: HWND; nIndex: Integer; dwNewLong: LongInt): LongInt; external 'SetWindowLongW@user32.dll stdcall'; function SetWindowPos(hWnd: HWND; hWndInsertAfter: HWND; X, Y, cx, cy: Integer; uFlags: Cardinal): LongInt; external 'SetWindowPos@user32.dll stdcall'; procedure MakeWizardFixedSize; begin SetWindowLong(WizardForm.Handle, GWL_STYLE, GetWindowLong(WizardForm.Handle, GWL_STYLE) and not (WS_MAXIMIZEBOX or WS_THICKFRAME)); // Required after any style change: SetWindowLong alters the style bits but leaves the // cached non-client frame alone, so the window keeps the client area computed for the old // styles until Windows is asked to recompute it. SetWindowLong's own documentation // prescribes this call. // // ⚠️ It is NOT the fix for the check boxes being clipped on their left edge — that was my // first theory and hardware disproved it. That symptom appears only on the Ally and is // unexplained; see §28. This call stays because it is correct on its own terms. SetWindowPos(WizardForm.Handle, 0, 0, 0, 0, 0, SWP_NOMOVE or SWP_NOSIZE or SWP_NOZORDER or SWP_NOACTIVATE or SWP_FRAMECHANGED); end; var LogoImage: TBitmapImage; DevelopedByLabel: TNewStaticText; GitHubLinkLabel: TNewStaticText; ArtMoonPage: TWizardPage; ArtMoonIntroLabel: TNewStaticText; ArtMoonBulletsLabel: TNewStaticText; ArtMoonOutroLabel: TNewStaticText; ArtMoonLearnMoreLabel: TNewStaticText; ArtMoonLinkLabel: TNewStaticText; procedure GitHubLinkClick(Sender: TObject); var ErrorCode: Integer; begin ShellExec('open', '{#MyAppURL}', '', '', SW_SHOWNORMAL, ewNoWait, ErrorCode); end; procedure ArtMoonLinkClick(Sender: TObject); var ErrorCode: Integer; begin ShellExec('open', 'https://github.com/onaiaku/ArtMoon', '', '', SW_SHOWNORMAL, ewNoWait, ErrorCode); end; procedure InitializeWizard; var TmpFileName: String; begin // Before anything is laid out: the window exists by now, but has not been shown. MakeWizardFixedSize; ExtractTemporaryFile('artlightcontrol.png'); TmpFileName := ExpandConstant('{tmp}\artlightcontrol.png'); LogoImage := TBitmapImage.Create(WizardForm); LogoImage.Parent := WizardForm.WelcomePage; // PngImage (not Bitmap) is the loader for .png — see Inno Setup's CodeClasses example. LogoImage.PngImage.LoadFromFile(TmpFileName); LogoImage.Left := WizardForm.WelcomeLabel1.Left; LogoImage.Top := WizardForm.WelcomeLabel1.Top + WizardForm.WelcomeLabel1.Height + ScaleY(25); // Sized explicitly, NOT with AutoSize. // // AutoSize draws the PNG at its native pixel size, so the artwork's own resolution silently // becomes the layout. That held while the file happened to be 96x96; the moment it was // replaced with a 672x672 master the logo rendered seven times too big and bled across the // welcome page. Stretch scales whatever it is given into the box below, so the asset can be // any resolution — and a higher one is now the better choice, since it downscales cleanly. // // ScaleX/ScaleY where AutoSize gave raw pixels: the rest of this layout is already // DPI-scaled, so the logo was the one element that shrank on a high-DPI display. LogoImage.AutoSize := False; LogoImage.Stretch := True; LogoImage.Width := ScaleX(96); LogoImage.Height := ScaleY(96); DevelopedByLabel := TNewStaticText.Create(WizardForm); DevelopedByLabel.Parent := WizardForm.WelcomePage; DevelopedByLabel.Left := LogoImage.Left; DevelopedByLabel.Top := LogoImage.Top + LogoImage.Height + ScaleY(30); DevelopedByLabel.Caption := 'Developed by onaiaku © 2026'; DevelopedByLabel.Font.Size := 10; DevelopedByLabel.AutoSize := True; GitHubLinkLabel := TNewStaticText.Create(WizardForm); GitHubLinkLabel.Parent := WizardForm.WelcomePage; GitHubLinkLabel.Left := DevelopedByLabel.Left; GitHubLinkLabel.Top := DevelopedByLabel.Top + DevelopedByLabel.Height + ScaleY(15); GitHubLinkLabel.Caption := '{#MyAppURL}'; GitHubLinkLabel.Cursor := crHand; GitHubLinkLabel.Font.Color := clHighlight; GitHubLinkLabel.Font.Style := [fsUnderline]; GitHubLinkLabel.OnClick := @GitHubLinkClick; // Companion page: points at ArtMoon, our client, since neither app is much use alone. // the other, since neither is much use to a streaming setup on its own. Same layout, // same full inner-page width — the Welcome page's right panel is too narrow for a // bullet list. ArtMoonPage := CreateCustomPage(wpWelcome, 'ArtMoon — recommended companion app', #13#10 + 'Install ArtMoon on the device you play from to unlock ArtLight Control''s features.'); ArtMoonIntroLabel := TNewStaticText.Create(ArtMoonPage); ArtMoonIntroLabel.Parent := ArtMoonPage.Surface; ArtMoonIntroLabel.Left := 0; ArtMoonIntroLabel.Top := 0; ArtMoonIntroLabel.Width := ArtMoonPage.SurfaceWidth; ArtMoonIntroLabel.WordWrap := True; ArtMoonIntroLabel.AutoSize := True; ArtMoonIntroLabel.Caption := 'ArtLight Control tunes this host for any Moonlight-compatible client. Paired with ' + 'ArtMoon — a free open-source Moonlight fork for the client device, from the ' + 'same team — the two work as one:'; ArtMoonBulletsLabel := TNewStaticText.Create(ArtMoonPage); ArtMoonBulletsLabel.Parent := ArtMoonPage.Surface; ArtMoonBulletsLabel.Left := ScaleX(16); ArtMoonBulletsLabel.Top := ArtMoonIntroLabel.Top + ArtMoonIntroLabel.Height + ScaleY(14); ArtMoonBulletsLabel.AutoSize := True; ArtMoonBulletsLabel.Caption := // NB: this label has no WordWrap, so every bullet must stay on one line — // keep them at or under ~76 characters or they get clipped on the right. '• Link-speed matching — this host follows the client, before connecting' + #13#10 + '• Seamless launch — the client can wait for your game to appear' + #13#10 + '• Wake this host and type its PIN from the client, with the controller' + #13#10 + '• This host''s GPU, encoder, VRAM, temperature and CPU in the game overlay' + #13#10 + '• Store badges on your synced game covers (Steam, Epic, GOG, Xbox, …)' + #13#10 + '• Per-session quality grading, charts and delivered-vs-target bitrate' + #13#10 + '• This host''s last session shown on the client, cover art included' + #13#10 + '• Sleep, restart or power this host off, or update it, from the client' + #13#10 + '• Gamepad-first interface: every action reachable from the pad' + #13#10 + '• Per-game and per-host profiles, custom resolutions, live stream settings' + #13#10 + '• Tailscale presence for streaming from outside your network'; ArtMoonOutroLabel := TNewStaticText.Create(ArtMoonPage); ArtMoonOutroLabel.Parent := ArtMoonPage.Surface; ArtMoonOutroLabel.Left := 0; ArtMoonOutroLabel.Top := ArtMoonBulletsLabel.Top + ArtMoonBulletsLabel.Height + ScaleY(18); ArtMoonOutroLabel.Width := ArtMoonPage.SurfaceWidth; ArtMoonOutroLabel.WordWrap := True; ArtMoonOutroLabel.AutoSize := True; ArtMoonOutroLabel.Caption := 'ArtMoon is optional — ArtLight Control works with any Moonlight-compatible client, ' + 'and you can install it on the client device at any time. Click Next to continue ' + 'installing ArtLight Control.'; ArtMoonLearnMoreLabel := TNewStaticText.Create(ArtMoonPage); ArtMoonLearnMoreLabel.Parent := ArtMoonPage.Surface; ArtMoonLearnMoreLabel.Left := 0; ArtMoonLearnMoreLabel.Top := ArtMoonOutroLabel.Top + ArtMoonOutroLabel.Height + ScaleY(16); ArtMoonLearnMoreLabel.Caption := 'Learn more:'; ArtMoonLearnMoreLabel.AutoSize := True; ArtMoonLinkLabel := TNewStaticText.Create(ArtMoonPage); ArtMoonLinkLabel.Parent := ArtMoonPage.Surface; ArtMoonLinkLabel.Left := ArtMoonLearnMoreLabel.Left + ArtMoonLearnMoreLabel.Width + ScaleX(4); ArtMoonLinkLabel.Top := ArtMoonLearnMoreLabel.Top; ArtMoonLinkLabel.Caption := 'https://github.com/onaiaku/ArtMoon'; ArtMoonLinkLabel.Cursor := crHand; ArtMoonLinkLabel.Font.Color := clHighlight; ArtMoonLinkLabel.Font.Style := [fsUnderline]; ArtMoonLinkLabel.OnClick := @ArtMoonLinkClick; ArtMoonLinkLabel.AutoSize := True; end; function PrepareToInstall(var NeedsRestart: Boolean): String; var ResultCode: Integer; begin // Stop the service BEFORE [Files] copies anything. On an upgrade over a running // install, ArtLightControlService.exe holds a lock on its own binary, so overwriting it // would otherwise depend on Restart Manager (CloseApplications=yes) noticing the // service and shutting it down — which is implicit, not guaranteed, and can race // with the sc delete/create that runs later in ssPostInstall. // A failure here is ignored on purpose: on a first install the service doesn't exist. Exec('sc.exe', 'stop ' + '{#ServiceName}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); // sc.exe returns as soon as the STOP is accepted, not once the service has actually // stopped, so give it a moment to release the file handle before the copy starts. Sleep(1500); Result := ''; end; procedure CurStepChanged(CurStep: TSetupStep); var ResultCode: Integer; AppDir: String; begin if CurStep = ssPostInstall then begin AppDir := ExpandConstant('{app}'); // Stop and remove any existing service instance before (re)creating Exec('sc.exe', 'stop ' + '{#ServiceName}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); Exec('sc.exe', 'delete ' + '{#ServiceName}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); // Create service with automatic start, running as LocalSystem Exec('sc.exe', 'create ' + '{#ServiceName}' + ' binPath= "' + AppDir + '\{#ServiceExe}"' + ' DisplayName= "ArtLight Control Speed Service"' + ' start= auto', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); // Set service description Exec('sc.exe', 'description ' + '{#ServiceName}' + ' "Applies network adapter speed changes for ArtLight Control without UAC prompts."', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); // Start the service immediately Exec('sc.exe', 'start ' + '{#ServiceName}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); end; end; procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); var ResultCode: Integer; begin if CurUninstallStep = usUninstall then begin Exec('sc.exe', 'stop ' + '{#ServiceName}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); Exec('sc.exe', 'delete ' + '{#ServiceName}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); // Remove autostart registry entry if the user had it enabled in-app RegDeleteValue(HKCU, 'SOFTWARE\Microsoft\Windows\CurrentVersion\Run', 'ArtLightControl'); // ...and the logon task, if the user chose Priority instead. Without this the task // survives the uninstall and fails at every logon, forever, launching an exe that is gone. Exec('schtasks.exe', '/Delete /TN "ArtLightControl Startup" /F', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); end; end; function InitializeSetup: Boolean; begin // .NET 8 base runtime (Microsoft.NETCore.App) — WinUI 3 does not need Desktop runtime Dependency_AddDotNet80; // Windows App SDK 2.4 runtime — provides the WinUI 3 XAML framework (DDLM package) Dependency_AddWindowsAppRuntime24; Result := True; end;