From 3e4c7dda3c5dd09848d94e6bdb882ebfc8c2de18 Mon Sep 17 00:00:00 2001 From: JAVCIF <81954917+JAVCIF@users.noreply.github.com> Date: Thu, 27 Aug 2026 23:38:01 -0500 Subject: [PATCH 1/2] Fix taskbar icon collisions with explicit launcher and game AppUserModelIDs --- .github/workflows/release.yml | 20 ++- CHANGELOG.md | 9 ++ LocaleGameHub/App.xaml.cs | 7 +- LocaleGameHub/LocaleGameHub.csproj | 2 +- LocaleGameHub/Services/ShortcutService.cs | 1 + .../Services/TaskbarIdentityService.cs | 103 ++++++++++++ README.es.md | 20 ++- README.md | 24 ++- installer/VNAR.iss | 6 +- tests/VNAR.ShellTests/Program.cs | 150 ++++++++++++++++++ tests/VNAR.ShellTests/VNAR.ShellTests.csproj | 15 ++ 11 files changed, 340 insertions(+), 17 deletions(-) create mode 100644 LocaleGameHub/Services/TaskbarIdentityService.cs create mode 100644 tests/VNAR.ShellTests/Program.cs create mode 100644 tests/VNAR.ShellTests/VNAR.ShellTests.csproj diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 47444e8..68b2e83 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ concurrency: cancel-in-progress: false env: - RELEASE_TAG: v1.0.0-beta.1.1 + RELEASE_TAG: v1.0.0-beta.1.2 jobs: build-release: @@ -35,6 +35,9 @@ jobs: - name: Restore run: dotnet restore .\LocaleGameHub\LocaleGameHub.csproj + - name: Test Windows taskbar and shortcut identities + run: dotnet run --project .\tests\VNAR.ShellTests\VNAR.ShellTests.csproj -c Release + - name: Publish self-contained build shell: pwsh run: | @@ -135,9 +138,18 @@ jobs: run: | $tag = "${{ env.RELEASE_TAG }}" $notes = @" - ## VNAR Beta 1.1 + ## VNAR Beta 1.2 + + Fixes the launcher taskbar icon being replaced by a previously opened game shortcut. + + - Stable, separate taskbar identity for the VNAR library window. + - Unique persistent identity for each game shortcut, independent of the admin setting. + - Installer shortcuts use the same launcher identity. + - Windows regression tests check process identities and saved shortcut properties. - First public GitHub beta package. + Recreate old game shortcuts to add their explicit identity. If an old pinned VNAR + icon stays cached, unpin it and pin the running Beta 1.2 launcher again. + Existing library data and game launch profiles are unchanged. ### Downloads - **VNAR-Setup.exe** — self-contained Windows installer. @@ -156,7 +168,7 @@ jobs: .\dist\VNAR-Setup.exe ` .\dist\VNAR-Portable-win-x64.zip ` .\dist\SHA256SUMS.txt ` - --title "VNAR Beta 1.1" ` + --title "VNAR Beta 1.2" ` --notes $notes ` --prerelease --draft ` --target "${{ github.sha }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 32a1d21..47db5f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 1.0.0-beta.1.2 + +- Fix taskbar icon collisions between the VNAR launcher and game shortcuts targeting the same executable. +- Assign a stable launcher AppUserModelID and a distinct ID per game, before creating windows. +- Persist game IDs in shortcut properties without changing their icons, targets, or arguments. +- Set the launcher ID on installer-created shortcuts. +- Add Windows integration checks for process identity and shortcut persistence, with normal/admin game settings. + + ## 1.0.0-beta.1.1 First public GitHub beta package. diff --git a/LocaleGameHub/App.xaml.cs b/LocaleGameHub/App.xaml.cs index 46b8bae..0cfed31 100644 --- a/LocaleGameHub/App.xaml.cs +++ b/LocaleGameHub/App.xaml.cs @@ -16,10 +16,15 @@ protected override void OnStartup(StartupEventArgs e) try { + var isGameLaunch = TryGetLaunchGameId(e.Args, out var gameId); + // Set the identity before creating any windows, including error dialogs. + // Old shortcuts without an explicit AppID cannot claim the launcher UI. + TaskbarIdentityService.SetCurrentProcessIdentity(isGameLaunch ? gameId : null); + var preferences = new LibraryService(); LocalizationService.SetLanguage(preferences.Settings.Language); - if (TryGetLaunchGameId(e.Args, out var gameId)) + if (isGameLaunch) { LaunchFromShortcut(preferences, gameId); Shutdown(0); diff --git a/LocaleGameHub/LocaleGameHub.csproj b/LocaleGameHub/LocaleGameHub.csproj index 787878b..7a78c84 100644 --- a/LocaleGameHub/LocaleGameHub.csproj +++ b/LocaleGameHub/LocaleGameHub.csproj @@ -7,7 +7,7 @@ enable VNAR LocaleGameHub - 1.0.0-beta.1.1 + 1.0.0-beta.1.2 app.manifest Resources\VNAR.ico diff --git a/LocaleGameHub/Services/ShortcutService.cs b/LocaleGameHub/Services/ShortcutService.cs index 4918d8e..843d9d3 100644 --- a/LocaleGameHub/Services/ShortcutService.cs +++ b/LocaleGameHub/Services/ShortcutService.cs @@ -54,6 +54,7 @@ public static string CreateShortcut(GameEntry game, string shortcutName, string ReleaseCom(shellObject); } + TaskbarIdentityService.SetShortcutIdentity(shortcutPath, game.Id); return shortcutPath; } diff --git a/LocaleGameHub/Services/TaskbarIdentityService.cs b/LocaleGameHub/Services/TaskbarIdentityService.cs new file mode 100644 index 0000000..f4b6e49 --- /dev/null +++ b/LocaleGameHub/Services/TaskbarIdentityService.cs @@ -0,0 +1,103 @@ +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; + +namespace LocaleGameHub.Services; + +/// +/// Prevents Explorer from inferring one shared taskbar identity from VNAR.exe +/// for the library UI and every "--launch" shortcut. +/// +public static class TaskbarIdentityService +{ + // Keep this stable across versions, and in sync with installer/VNAR.iss. + public const string LauncherAppId = "JAVCIF.VNAR.Launcher"; + + public static string GetGameAppId(Guid gameId) => $"JAVCIF.VNAR.Game.{gameId:N}"; + + public static void SetCurrentProcessIdentity(Guid? gameId = null) + { + var appId = gameId.HasValue ? GetGameAppId(gameId.Value) : LauncherAppId; + Marshal.ThrowExceptionForHR(SetCurrentProcessExplicitAppUserModelID(appId)); + } + + public static void SetShortcutIdentity(string shortcutPath, Guid gameId) + { + // WScript.Shell exposes the icon/arguments but not System.AppUserModel.ID. + // Reopen the saved link and change only that property, preserving its icon, + // target, arguments and any existing Shell Link flags. + var linkType = Type.GetTypeFromCLSID(new Guid("00021401-0000-0000-C000-000000000046"), true)!; + var link = Activator.CreateInstance(linkType) + ?? throw new InvalidOperationException("Could not open the Shell Link property store."); + + try + { + var file = (IPersistFile)link; + file.Load(shortcutPath, 2); // STGM_READWRITE + var store = (IPropertyStore)link; + var key = new PropertyKey + { + FormatId = new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"), + PropertyId = 5 // PKEY_AppUserModel_ID + }; + var value = new PropVariant + { + ValueType = 31, // VT_LPWSTR + StringValue = Marshal.StringToCoTaskMemUni(GetGameAppId(gameId)) + }; + + try + { + Marshal.ThrowExceptionForHR(store.SetValue(ref key, ref value)); + Marshal.ThrowExceptionForHR(store.Commit()); + file.Save(shortcutPath, true); + } + finally + { + Marshal.FreeCoTaskMem(value.StringValue); + } + } + finally + { + Marshal.FinalReleaseComObject(link); + } + } + + [DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] + private static extern int SetCurrentProcessExplicitAppUserModelID(string appId); + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct PropertyKey + { + public Guid FormatId; + public uint PropertyId; + } + + // PROPVARIANT's union contains a counted pointer: 16 bytes on x86, + // 24 on x64. Include that member so the native layout is correct on both. + [StructLayout(LayoutKind.Sequential)] + private struct CountedPointer + { + public uint Count; + public IntPtr Pointer; + } + + [StructLayout(LayoutKind.Explicit)] + private struct PropVariant + { + [FieldOffset(0)] public ushort ValueType; + [FieldOffset(8)] public IntPtr StringValue; + [FieldOffset(8)] public CountedPointer ArrayValue; + } + + [ComImport] + [Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IPropertyStore + { + [PreserveSig] int GetCount(out uint count); + [PreserveSig] int GetAt(uint index, out PropertyKey key); + [PreserveSig] int GetValue(ref PropertyKey key, out PropVariant value); + [PreserveSig] int SetValue(ref PropertyKey key, ref PropVariant value); + [PreserveSig] int Commit(); + } +} diff --git a/README.es.md b/README.es.md index 61225bb..c9d4265 100644 --- a/README.es.md +++ b/README.es.md @@ -7,7 +7,7 @@ Un launcher ligero para Windows enfocado en visual novels, con integración con Locale Emulator, metadatos de VNDB, edición de portadas, navegación por desarrolladores, favoritos, soporte WebP y accesos directos inteligentes. -[English](README.md) · [Descargar Beta 1.1](https://github.com/JAVCIF/vnar/releases/tag/v1.0.0-beta.1.1) +[English](README.md) · [Descargar Beta 1.2](https://github.com/JAVCIF/vnar/releases/tag/v1.0.0-beta.1.2) ## Descargas @@ -16,8 +16,8 @@ Un launcher ligero para Windows enfocado en visual novels, con integración con | Paquete | Descripción | Descarga | | --- | --- | --- | -| Instalador | Instalación autocontenida, acceso en el menú Inicio y acceso de escritorio opcional | [VNAR-Setup.exe](https://github.com/JAVCIF/vnar/releases/download/v1.0.0-beta.1.1/VNAR-Setup.exe) | -| Portable | Versión autocontenida; descomprime y ejecuta `VNAR.exe` | [VNAR-Portable-win-x64.zip](https://github.com/JAVCIF/vnar/releases/download/v1.0.0-beta.1.1/VNAR-Portable-win-x64.zip) | +| Instalador | Instalación autocontenida, acceso en el menú Inicio y acceso de escritorio opcional | [VNAR-Setup.exe](https://github.com/JAVCIF/vnar/releases/download/v1.0.0-beta.1.2/VNAR-Setup.exe) | +| Portable | Versión autocontenida; descomprime y ejecuta `VNAR.exe` | [VNAR-Portable-win-x64.zip](https://github.com/JAVCIF/vnar/releases/download/v1.0.0-beta.1.2/VNAR-Portable-win-x64.zip) | Locale Emulator **no viene incluido** dentro de VNAR. En el primer arranque puedes seleccionar un `LEProc.exe` existente o dejar que VNAR descargue la versión oficial de Locale Emulator. @@ -81,9 +81,21 @@ dotnet run --project .\LocaleGameHub\LocaleGameHub.csproj También se incluyen `build_portable.bat` y `build_small.bat` para compilaciones locales. +## Iconos de la barra de tareas (Beta 1.2) + +VNAR ahora declara su propio AppUserModelID de Windows. Cada acceso de juego tiene +una identidad distinta y estable para que su icono no se asocie a la ventana del +launcher. La opción de administrador sigue cambiando únicamente el perfil de +Locale Emulator. + +Los accesos antiguos siguen funcionando. Vuelve a crearlos desde VNAR para añadir +la nueva identidad. Si Windows conserva el icono de un VNAR anclado anteriormente, +desancla ese acceso, abre el `VNAR.exe` actualizado y ancla la ventana nueva. +No necesitas borrar la caché global de iconos ni tus datos. + ## Estado del proyecto -Versión pública actual: **Beta 1.1 (`1.0.0-beta.1.1`)**. +Versión pública actual: **Beta 1.2 (`1.0.0-beta.1.2`)**. Puedes usar GitHub Issues para reportar errores o proponer mejoras. diff --git a/README.md b/README.md index 025a0cc..537c69b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A lightweight Windows launcher for visual novels with Locale Emulator integration, VNDB metadata, cover editing, developer browsing, favorites, WebP support, and smart desktop shortcuts. -[Español](README.es.md) · [Download Beta 1.1](https://github.com/JAVCIF/vnar/releases/tag/v1.0.0-beta.1.1) +[Español](README.es.md) · [Download Beta 1.2](https://github.com/JAVCIF/vnar/releases/tag/v1.0.0-beta.1.2) ## Downloads @@ -16,8 +16,8 @@ A lightweight Windows launcher for visual novels with Locale Emulator integratio | Package | Description | Download | | --- | --- | --- | -| Installer | Self-contained installation with Start Menu integration and optional desktop shortcut | [VNAR-Setup.exe](https://github.com/JAVCIF/vnar/releases/download/v1.0.0-beta.1.1/VNAR-Setup.exe) | -| Portable | Self-contained portable build; extract and run `VNAR.exe` | [VNAR-Portable-win-x64.zip](https://github.com/JAVCIF/vnar/releases/download/v1.0.0-beta.1.1/VNAR-Portable-win-x64.zip) | +| Installer | Self-contained installation with Start Menu integration and optional desktop shortcut | [VNAR-Setup.exe](https://github.com/JAVCIF/vnar/releases/download/v1.0.0-beta.1.2/VNAR-Setup.exe) | +| Portable | Self-contained portable build; extract and run `VNAR.exe` | [VNAR-Portable-win-x64.zip](https://github.com/JAVCIF/vnar/releases/download/v1.0.0-beta.1.2/VNAR-Portable-win-x64.zip) | Locale Emulator is **not bundled** with VNAR. On first launch, VNAR can use an existing `LEProc.exe` or download the official Locale Emulator release for you. @@ -87,9 +87,25 @@ dotnet publish .\LocaleGameHub\LocaleGameHub.csproj -c Release -r win-x64 --self The repository also includes `build_portable.bat` and `build_small.bat` for local Windows builds. +## Taskbar icons (Beta 1.2) + +VNAR now declares its own Windows AppUserModelID. Game shortcuts use a separate, +stable ID per game, so opening one cannot supply its icon to the VNAR library window. +The administrator option still only selects the Locale Emulator profile. + +Existing shortcuts keep working. Recreate them in VNAR to add the new shortcut +identity. If an old pinned VNAR icon remains cached, unpin it, open the updated +`VNAR.exe`, and pin that running window again. No icon-cache reset is required. + +Windows shell regression checks: + +```powershell +dotnet run --project .\tests\VNAR.ShellTests\VNAR.ShellTests.csproj -c Release +``` + ## Project status -Current public build: **Beta 1.1 (`1.0.0-beta.1.1`)**. +Current public build: **Beta 1.2 (`1.0.0-beta.1.2`)**. Bug reports and suggestions are welcome through GitHub Issues. diff --git a/installer/VNAR.iss b/installer/VNAR.iss index 57af486..c1b07bc 100644 --- a/installer/VNAR.iss +++ b/installer/VNAR.iss @@ -1,5 +1,5 @@ #define MyAppName "VNAR" -#define MyAppVersion "1.0.0-beta.1.1" +#define MyAppVersion "1.0.0-beta.1.2" #define MyAppPublisher "JAVCIF" #define MyAppExeName "VNAR.exe" @@ -35,8 +35,8 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ Source: "..\dist\portable\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] -Name: "{autoprograms}\VNAR"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}" -Name: "{autodesktop}\VNAR"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; Tasks: desktopicon +Name: "{autoprograms}\VNAR"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; AppUserModelID: "JAVCIF.VNAR.Launcher" +Name: "{autodesktop}\VNAR"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; Tasks: desktopicon; AppUserModelID: "JAVCIF.VNAR.Launcher" [Run] Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,VNAR}"; Flags: nowait postinstall skipifsilent diff --git a/tests/VNAR.ShellTests/Program.cs b/tests/VNAR.ShellTests/Program.cs new file mode 100644 index 0000000..62163c5 --- /dev/null +++ b/tests/VNAR.ShellTests/Program.cs @@ -0,0 +1,150 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using LocaleGameHub.Models; +using LocaleGameHub.Services; + +namespace VNAR.ShellTests; + +internal static class Program +{ + [STAThread] + private static int Main() + { + var root = Path.Combine(Path.GetTempPath(), "VNAR Shell Tests " + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + + try + { + var first = new GameEntry { Name = "Normal game", RunAsAdmin = false }; + var second = new GameEntry { Name = "Admin game", RunAsAdmin = true }; + var icon = Path.Combine(root, "selected icon.ico"); + File.Copy(Path.Combine(AppContext.BaseDirectory, "Fixture.ico"), icon); + + // Exercise real shell32 APIs, including resetting an incoming game ID + // to the launcher ID. This does not launch games or request elevation. + TaskbarIdentityService.SetCurrentProcessIdentity(first.Id); + Equal(TaskbarIdentityService.GetGameAppId(first.Id), ReadProcessIdentity(), "first game process"); + TaskbarIdentityService.SetCurrentProcessIdentity(); + Equal(TaskbarIdentityService.LauncherAppId, ReadProcessIdentity(), "launcher after first game"); + TaskbarIdentityService.SetCurrentProcessIdentity(second.Id); + Equal(TaskbarIdentityService.GetGameAppId(second.Id), ReadProcessIdentity(), "second game process"); + TaskbarIdentityService.SetCurrentProcessIdentity(); + Equal(TaskbarIdentityService.LauncherAppId, ReadProcessIdentity(), "launcher after second game"); + Console.WriteLine("PASS: explicit process identities remain separate."); + + var normalLink = ShortcutService.CreateShortcut(first, "Normal á", root, icon); + var adminLink = ShortcutService.CreateShortcut(second, "Admin かな", root, icon); + VerifyShortcut(normalLink, first, icon); + VerifyShortcut(adminLink, second, icon); + NotEqual(ReadShortcutIdentity(normalLink), ReadShortcutIdentity(adminLink), "different games"); + NotEqual(TaskbarIdentityService.LauncherAppId, ReadShortcutIdentity(normalLink), "game vs launcher"); + Equal(TaskbarIdentityService.LauncherAppId, ReadProcessIdentity(), "creating links cannot change the UI identity"); + Console.WriteLine("PASS: normal/admin game shortcuts persist distinct AppIDs without changing the process identity."); + + // Updating a game's admin preference or display name must not change + // the shortcut's grouping identity or turn it into the launcher. + first.RunAsAdmin = true; + first.Name = "Renamed game"; + var renamedLink = ShortcutService.CreateShortcut(first, "Renamed admin", root, icon); + VerifyShortcut(renamedLink, first, icon); + Equal(ReadShortcutIdentity(normalLink), ReadShortcutIdentity(renamedLink), "same game after rename/admin change"); + Equal(TaskbarIdentityService.LauncherAppId, ReadProcessIdentity(), "identity after later shortcuts"); + Console.WriteLine("PASS: game identity stays stable across rename/admin setting changes."); + + // An older link can be recreated in place, adding the property while + // retaining the intended launch arguments and chosen icon. + var replacement = ShortcutService.CreateShortcut(first, "Normal á", root, icon); + VerifyShortcut(replacement, first, icon); + Console.WriteLine("PASS: shortcut replacement preserves target, arguments, description and icon."); + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + return 1; + } + finally + { + Directory.Delete(root, recursive: true); + } + } + + private static void VerifyShortcut(string path, GameEntry game, string icon) + { + Equal(TaskbarIdentityService.GetGameAppId(game.Id), ReadShortcutIdentity(path), "persisted AppUserModelID"); + + object? shellObject = null; + object? linkObject = null; + try + { + shellObject = Activator.CreateInstance(Type.GetTypeFromProgID("WScript.Shell")!); + dynamic shell = shellObject!; + linkObject = shell.CreateShortcut(path); + dynamic link = linkObject; + Equal(ShortcutService.ResolveVnarExecutable().ToUpperInvariant(), + ((string)link.TargetPath).ToUpperInvariant(), "shortcut target"); + Equal($"--launch {game.Id:D}", (string)link.Arguments, "shortcut arguments"); + Equal(icon.ToUpperInvariant() + ",0", ((string)link.IconLocation).ToUpperInvariant(), "chosen icon"); + Equal($"VNAR · {game.Name}", (string)link.Description, "shortcut description"); + } + finally + { + Release(linkObject); + Release(shellObject); + } + } + + private static string ReadShortcutIdentity(string path) + { + // Read through Explorer's canonical property name, independently of the + // IPropertyStore implementation used to write the link. + object? shellObject = null; + object? folderObject = null; + object? itemObject = null; + try + { + shellObject = Activator.CreateInstance(Type.GetTypeFromProgID("Shell.Application")!); + dynamic shell = shellObject!; + folderObject = shell.NameSpace(Path.GetDirectoryName(path)!); + dynamic folder = folderObject!; + itemObject = folder.ParseName(Path.GetFileName(path)); + dynamic item = itemObject!; + return Convert.ToString(item.ExtendedProperty("System.AppUserModel.ID")) ?? string.Empty; + } + finally + { + Release(itemObject); + Release(folderObject); + Release(shellObject); + } + } + + private static string ReadProcessIdentity() + { + Marshal.ThrowExceptionForHR(GetCurrentProcessExplicitAppUserModelID(out var pointer)); + try { return Marshal.PtrToStringUni(pointer) ?? string.Empty; } + finally { Marshal.FreeCoTaskMem(pointer); } + } + + private static void Equal(string expected, string actual, string label) + { + if (actual != expected) + throw new InvalidOperationException($"{label}: expected '{expected}', got '{actual}'."); + } + + private static void NotEqual(string first, string second, string label) + { + if (first == second) + throw new InvalidOperationException($"{label}: unexpected shared identity '{first}'."); + } + + private static void Release(object? value) + { + if (value is not null && Marshal.IsComObject(value)) + Marshal.FinalReleaseComObject(value); + } + + [DllImport("shell32.dll", ExactSpelling = true)] + private static extern int GetCurrentProcessExplicitAppUserModelID(out IntPtr appId); +} diff --git a/tests/VNAR.ShellTests/VNAR.ShellTests.csproj b/tests/VNAR.ShellTests/VNAR.ShellTests.csproj new file mode 100644 index 0000000..2c48a53 --- /dev/null +++ b/tests/VNAR.ShellTests/VNAR.ShellTests.csproj @@ -0,0 +1,15 @@ + + + Exe + net10.0-windows + true + enable + enable + + + + + PreserveNewest + + + From 20b2ce616593938e683ea147a0354ac286040a0e Mon Sep 17 00:00:00 2001 From: JAVCIF <81954917+JAVCIF@users.noreply.github.com> Date: Thu, 27 Aug 2026 23:40:00 -0500 Subject: [PATCH 2/2] Keep shell identity test filenames independent of WScript code pages --- tests/VNAR.ShellTests/Program.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/VNAR.ShellTests/Program.cs b/tests/VNAR.ShellTests/Program.cs index 62163c5..74f345e 100644 --- a/tests/VNAR.ShellTests/Program.cs +++ b/tests/VNAR.ShellTests/Program.cs @@ -33,8 +33,9 @@ private static int Main() Equal(TaskbarIdentityService.LauncherAppId, ReadProcessIdentity(), "launcher after second game"); Console.WriteLine("PASS: explicit process identities remain separate."); - var normalLink = ShortcutService.CreateShortcut(first, "Normal á", root, icon); - var adminLink = ShortcutService.CreateShortcut(second, "Admin かな", root, icon); + // Keep fixture filenames independent of WScript.Shell ANSI code-page limitations. + var normalLink = ShortcutService.CreateShortcut(first, "Normal shortcut", root, icon); + var adminLink = ShortcutService.CreateShortcut(second, "Admin shortcut", root, icon); VerifyShortcut(normalLink, first, icon); VerifyShortcut(adminLink, second, icon); NotEqual(ReadShortcutIdentity(normalLink), ReadShortcutIdentity(adminLink), "different games"); @@ -54,7 +55,7 @@ private static int Main() // An older link can be recreated in place, adding the property while // retaining the intended launch arguments and chosen icon. - var replacement = ShortcutService.CreateShortcut(first, "Normal á", root, icon); + var replacement = ShortcutService.CreateShortcut(first, "Normal shortcut", root, icon); VerifyShortcut(replacement, first, icon); Console.WriteLine("PASS: shortcut replacement preserves target, arguments, description and icon."); return 0;