Files
CydandClaude Fable 5 106fa610c0 vPOD: real system volume + pre-uninstall.bat, behind the existing opt-ins
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>
2026-07-11 21:26:38 -05:00

143 lines
5.8 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// =============================================================================
// 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.01.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 { }
}
}
}