Two launcher behaviors vPOD only simulated are now available for real, matching the pod exactly: - "Actually set system volume": set_VolumeLevel drives this machine's master volume through the launcher's own chain — nircmd.exe in the games root, else Core Audio (Vista+), else winmm. The chain moved out of TeslaLauncher.cs into Launcher/VolumeControl.cs and is compiled into both apps as linked source (the MiniZip pattern); launcher behavior is unchanged. Off by default: the value is stored/echoed only, as before. - pre-uninstall.bat now runs before the product directory is deleted on UninstallApp (working dir, hidden window, 120 s wait, exit code logged — mirrors CleanupProductDirectory). Gated behind the renamed "Run package install/uninstall scripts" checkbox (was "Run postinstall.bat after install"; RunPostInstall -> RunPackageScripts), closing the asymmetry where install scripts had an opt-in but uninstall scripts silently never ran. Verified: 106/106 diff tests; live master-volume set/restore through vPOD's build of VolumeControl; functional probe of UninstallApp against an isolated games root with the flag off (script skipped, dir removed) and on (script ran, then dir removed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
143 lines
5.8 KiB
C#
143 lines
5.8 KiB
C#
// =============================================================================
|
||
// TeslaLauncher — system master volume (shared with vPOD)
|
||
// =============================================================================
|
||
// The Console's set_VolumeLevel ends here. Setter chain:
|
||
// nircmd.exe (legacy, works everywhere incl. XP, if present in the games dir)
|
||
// → Core Audio (Vista+) → winmm waveOutSetVolume (XP fallback).
|
||
//
|
||
// Like MiniZip.cs, this file is compiled into BOTH the launcher and vPOD
|
||
// (linked source): vPOD's "Actually set system volume" mode applies the
|
||
// console's volume commands through the exact code the real pod runs.
|
||
// =============================================================================
|
||
|
||
using System;
|
||
using System.Diagnostics;
|
||
using System.IO;
|
||
using System.Runtime.InteropServices;
|
||
|
||
namespace Tesla.Launcher
|
||
{
|
||
internal static class VolumeControl
|
||
{
|
||
/// <summary>Sets the system master volume to <paramref name="scalar"/>
|
||
/// (clamped to 0.0–1.0). <paramref name="nircmdDir"/> is probed for
|
||
/// nircmd.exe first (the legacy path used on the original pods); the
|
||
/// Windows APIs are the fallback. Never throws.</summary>
|
||
public static void SetMasterScalar(float scalar, string nircmdDir)
|
||
{
|
||
scalar = Math.Max(0f, Math.Min(1f, scalar));
|
||
|
||
if (!string.IsNullOrEmpty(nircmdDir))
|
||
{
|
||
var nircmd = Path.Combine(nircmdDir, "nircmd.exe");
|
||
if (File.Exists(nircmd))
|
||
{
|
||
try
|
||
{
|
||
var p = Process.Start(nircmd, "setsysvolume " + (int)(scalar * 65535));
|
||
if (p != null) p.Dispose();
|
||
return;
|
||
}
|
||
catch { /* fall through to API */ }
|
||
}
|
||
}
|
||
|
||
if (Environment.OSVersion.Version.Major >= 6)
|
||
{
|
||
try { CoreAudio.SetMasterScalar(scalar); return; }
|
||
catch { /* fall through */ }
|
||
}
|
||
|
||
WinMmVolume.SetMasterScalar(scalar);
|
||
}
|
||
}
|
||
|
||
// ── Windows Core Audio API — minimal COM interop (Vista+) ─────────────────
|
||
// No external dependencies. Vtable slot order matches the SDK headers exactly.
|
||
// Methods we don't call are declared as void stubs to preserve vtable offsets.
|
||
// NOT available on XP — callers must gate on OS version.
|
||
|
||
internal static class CoreAudio
|
||
{
|
||
internal static void SetMasterScalar(float scalar)
|
||
{
|
||
var ep = GetEndpointVolume();
|
||
try { var ctx = Guid.Empty; ep.SetMasterVolumeLevelScalar(scalar, ref ctx); }
|
||
finally { Marshal.ReleaseComObject(ep); }
|
||
}
|
||
|
||
private static IAudioEndpointVolume GetEndpointVolume()
|
||
{
|
||
var enumerator = (IMMDeviceEnumerator)new MMAudioEnumeratorComClass();
|
||
try
|
||
{
|
||
IMMDevice device;
|
||
enumerator.GetDefaultAudioEndpoint(0 /*eRender*/, 1 /*eMultimedia*/, out device);
|
||
try
|
||
{
|
||
var iid = typeof(IAudioEndpointVolume).GUID;
|
||
object obj;
|
||
device.Activate(ref iid, 23 /*CLSCTX_ALL*/, IntPtr.Zero, out obj);
|
||
return (IAudioEndpointVolume)obj;
|
||
}
|
||
finally { Marshal.ReleaseComObject(device); }
|
||
}
|
||
finally { Marshal.ReleaseComObject(enumerator); }
|
||
}
|
||
}
|
||
|
||
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
|
||
internal class MMAudioEnumeratorComClass { }
|
||
|
||
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"),
|
||
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||
internal interface IMMDeviceEnumerator
|
||
{
|
||
void _unused_EnumAudioEndpoints(); // slot 0 — not used
|
||
[PreserveSig]
|
||
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice endpoint);
|
||
}
|
||
|
||
[Guid("D666063F-1587-4E43-81F1-B948E807363F"),
|
||
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||
internal interface IMMDevice
|
||
{
|
||
[PreserveSig]
|
||
int Activate(ref Guid iid, int clsCtx, IntPtr pActivationParams,
|
||
[MarshalAs(UnmanagedType.IUnknown)] out object interfacePointer);
|
||
}
|
||
|
||
// Vtable order (after IUnknown): RegisterControlChangeNotify(0),
|
||
// UnregisterControlChangeNotify(1), GetChannelCount(2),
|
||
// SetMasterVolumeLevel(3), SetMasterVolumeLevelScalar(4),
|
||
// GetMasterVolumeLevel(5), GetMasterVolumeLevelScalar(6)
|
||
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"),
|
||
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||
internal interface IAudioEndpointVolume
|
||
{
|
||
void _unused_RegisterControlChangeNotify(); // slot 0
|
||
void _unused_UnregisterControlChangeNotify(); // slot 1
|
||
void _unused_GetChannelCount(); // slot 2
|
||
[PreserveSig] int SetMasterVolumeLevel(float levelDB, ref Guid ctx);
|
||
[PreserveSig] int SetMasterVolumeLevelScalar(float level, ref Guid ctx);
|
||
[PreserveSig] int GetMasterVolumeLevel(out float levelDB);
|
||
[PreserveSig] int GetMasterVolumeLevelScalar(out float level);
|
||
}
|
||
|
||
// ── winmm wave-out volume (XP fallback) ───────────────────────────────────
|
||
// waveOutSetVolume with device -1 sets the wave mixer level of the default
|
||
// device: low 16 bits = left channel, high 16 bits = right channel.
|
||
|
||
internal static class WinMmVolume
|
||
{
|
||
[DllImport("winmm.dll")]
|
||
private static extern int waveOutSetVolume(IntPtr hwo, uint dwVolume);
|
||
|
||
internal static void SetMasterScalar(float scalar)
|
||
{
|
||
uint level = (uint)(Math.Max(0f, Math.Min(1f, scalar)) * 0xFFFF);
|
||
try { waveOutSetVolume(IntPtr.Zero, (level << 16) | level); } catch { }
|
||
}
|
||
}
|
||
}
|