Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d186293bf | ||
|
|
6973e7d60c | ||
|
|
85595b8c52 | ||
|
|
106fa610c0 | ||
|
|
8ba428b6a4 |
@@ -8,9 +8,9 @@ using System.Runtime.InteropServices;
|
|||||||
[assembly: AssemblyCopyright("Copyright © 2009")]
|
[assembly: AssemblyCopyright("Copyright © 2009")]
|
||||||
[assembly: AssemblyConfiguration("")]
|
[assembly: AssemblyConfiguration("")]
|
||||||
[assembly: Guid("581ca4b6-a91c-4d24-b9b5-207f3b5da379")]
|
[assembly: Guid("581ca4b6-a91c-4d24-b9b5-207f3b5da379")]
|
||||||
[assembly: AssemblyFileVersion("4.11.4.3")]
|
[assembly: AssemblyFileVersion("4.11.4.4")]
|
||||||
[assembly: AssemblyTrademark("")]
|
[assembly: AssemblyTrademark("")]
|
||||||
[assembly: ComVisible(false)]
|
[assembly: ComVisible(false)]
|
||||||
[assembly: AssemblyTitle("Tesla Console")]
|
[assembly: AssemblyTitle("Tesla Console")]
|
||||||
[assembly: AssemblyDescription("All code and UI property of Virtual World Entertainment.\r\n\r\nDeveloped by Elsewhen Studios, LLC in association with VGCorps, LLC.\r\n\r\nElsewhen Studios and the Elsewhen Wormhole are trademarks of Elsewhen Studios, LLC\r\n\r\nIncludes the WeifenLuo DockingPane library. Copyright © 2007 Weifen Luo (email: weifenluo@yahoo.com). Licensed under the MIT License - details can be found in WeifenLuo.txt included in this installation.")]
|
[assembly: AssemblyDescription("All code and UI property of Virtual World Entertainment.\r\n\r\nDeveloped by Elsewhen Studios, LLC in association with VGCorps, LLC.\r\n\r\nElsewhen Studios and the Elsewhen Wormhole are trademarks of Elsewhen Studios, LLC\r\n\r\nIncludes the WeifenLuo DockingPane library. Copyright © 2007 Weifen Luo (email: weifenluo@yahoo.com). Licensed under the MIT License - details can be found in WeifenLuo.txt included in this installation.")]
|
||||||
[assembly: AssemblyVersion("4.11.4.3")]
|
[assembly: AssemblyVersion("4.11.4.4")]
|
||||||
|
|||||||
@@ -1516,7 +1516,11 @@ internal class SitePanel : DockContent
|
|||||||
for (int i = 0; i < rVolumeItems.Length; i++)
|
for (int i = 0; i < rVolumeItems.Length; i++)
|
||||||
{
|
{
|
||||||
mnuVolume.DropDownItems.Add(rVolumeItems[i]);
|
mnuVolume.DropDownItems.Add(rVolumeItems[i]);
|
||||||
((ToolStripMenuItem)mnuVolume.DropDownItems[i]).Checked = i + 1 == num / 10;
|
// Item i displays i*10 ("mute" at 0), so the reported level maps to
|
||||||
|
// index num/10 directly. The original console checked i+1 here — the
|
||||||
|
// mark sat one step below the actual volume, and mute (0) never got
|
||||||
|
// a mark at all. Original bug (4.11.3), fixed 2026-07-11.
|
||||||
|
((ToolStripMenuItem)mnuVolume.DropDownItems[i]).Checked = i == num / 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ namespace TeslaConsole.DiffTests
|
|||||||
Assert.Contains("TeslaConsole", _fx.Original.AssemblyFullName);
|
Assert.Contains("TeslaConsole", _fx.Original.AssemblyFullName);
|
||||||
Assert.Contains("TeslaConsole", _fx.Recovered.AssemblyFullName);
|
Assert.Contains("TeslaConsole", _fx.Recovered.AssemblyFullName);
|
||||||
Assert.Contains("4.11.3.37076", _fx.Original.AssemblyFullName);
|
Assert.Contains("4.11.3.37076", _fx.Original.AssemblyFullName);
|
||||||
Assert.Contains("4.11.4.3", _fx.Recovered.AssemblyFullName);
|
Assert.Contains("4.11.4.4", _fx.Recovered.AssemblyFullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- RPStrings.GetTimeString: mm:ss formatting with 0.5s rounding ----
|
// ---- RPStrings.GetTimeString: mm:ss formatting with 0.5s rounding ----
|
||||||
|
|||||||
@@ -156,6 +156,12 @@ namespace TeslaSecureConfig
|
|||||||
_port = new System.IO.Ports.SerialPort(comPort, baud,
|
_port = new System.IO.Ports.SerialPort(comPort, baud,
|
||||||
System.IO.Ports.Parity.None, 8, System.IO.Ports.StopBits.One);
|
System.IO.Ports.Parity.None, 8, System.IO.Ports.StopBits.One);
|
||||||
_port.Open();
|
_port.Open();
|
||||||
|
// Hide the text cursor once at startup, exactly as the game and the
|
||||||
|
// controller's own ROM demo do (ESC G 0). ESC @ does not restore it,
|
||||||
|
// so a single hide holds for the session.
|
||||||
|
_port.BaseStream.WriteByte(0x1B); // ESC
|
||||||
|
_port.BaseStream.WriteByte(0x47); // G
|
||||||
|
_port.BaseStream.WriteByte(0x00); // 0 -> cursor hidden
|
||||||
ClearAll();
|
ClearAll();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -166,12 +172,19 @@ namespace TeslaSecureConfig
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearAll: send ESC J (clear display) as per Plasma protocol
|
// Clear + home, per the PD01D221 controller's recovered command set (EPROM
|
||||||
|
// dump -> vrio/PlasmaNew/FIRMWARE.md; the ROM's own demo prefixes every
|
||||||
|
// screen with exactly these two). The previous code sent ESC J, which on
|
||||||
|
// this controller is NOT a clear -- it toggles an orientation/mode bit -- so
|
||||||
|
// the panel never cleared and never homed, and each provisioning cycle wrote
|
||||||
|
// stale text at a stale cursor position (garbled overlap in the field).
|
||||||
public void ClearAll()
|
public void ClearAll()
|
||||||
{
|
{
|
||||||
if (_port == null || !_port.IsOpen) return;
|
if (_port == null || !_port.IsOpen) return;
|
||||||
_port.BaseStream.WriteByte(0x1B); // ESC
|
_port.BaseStream.WriteByte(0x1B); // ESC
|
||||||
_port.BaseStream.WriteByte(0x4A); // J (clear screen)
|
_port.BaseStream.WriteByte(0x40); // @ -> clear active buffer, reset text state
|
||||||
|
_port.BaseStream.WriteByte(0x1B); // ESC
|
||||||
|
_port.BaseStream.WriteByte(0x4C); // L -> home cursor to (0,0)
|
||||||
_port.BaseStream.Flush();
|
_port.BaseStream.Flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+64
-118
@@ -502,6 +502,11 @@ namespace Tesla.Launcher
|
|||||||
private void CmdKillApp(Guid launchKey, int? pid)
|
private void CmdKillApp(Guid launchKey, int? pid)
|
||||||
{
|
{
|
||||||
var key = FindKeyString(launchKey);
|
var key = FindKeyString(launchKey);
|
||||||
|
var victims = new List<Process>();
|
||||||
|
// Untrack under the lock, terminate outside it: KillProcessTree spawns
|
||||||
|
// taskkill and waits, which must not block the RPC lock. Untracking
|
||||||
|
// first also stops our own auto-restart watcher from relaunching (its
|
||||||
|
// stillTracked check fails once the key is gone).
|
||||||
lock (_processLock)
|
lock (_processLock)
|
||||||
{
|
{
|
||||||
List<Process> procs;
|
List<Process> procs;
|
||||||
@@ -513,38 +518,86 @@ namespace Tesla.Launcher
|
|||||||
|
|
||||||
foreach (var p in toKill)
|
foreach (var p in toKill)
|
||||||
{
|
{
|
||||||
try { if (!p.HasExited) p.Kill(); } catch { }
|
victims.Add(p);
|
||||||
procs.Remove(p);
|
procs.Remove(p);
|
||||||
}
|
}
|
||||||
if (procs.Count == 0) _runningProcesses.Remove(key);
|
if (procs.Count == 0) _runningProcesses.Remove(key);
|
||||||
}
|
}
|
||||||
|
KillProcessTrees(victims);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CmdKillAllOfType(Guid launchKey)
|
private void CmdKillAllOfType(Guid launchKey)
|
||||||
{
|
{
|
||||||
var key = FindKeyString(launchKey);
|
var key = FindKeyString(launchKey);
|
||||||
|
List<Process> victims;
|
||||||
lock (_processLock)
|
lock (_processLock)
|
||||||
{
|
{
|
||||||
List<Process> procs;
|
if (key == null || !_runningProcesses.TryGetValue(key, out victims)) return;
|
||||||
if (key == null || !_runningProcesses.TryGetValue(key, out procs)) return;
|
|
||||||
foreach (var p in procs)
|
|
||||||
try { if (!p.HasExited) p.Kill(); } catch { }
|
|
||||||
_runningProcesses.Remove(key);
|
_runningProcesses.Remove(key);
|
||||||
}
|
}
|
||||||
|
KillProcessTrees(victims);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CmdKillAllApps()
|
private void CmdKillAllApps()
|
||||||
{
|
{
|
||||||
|
var victims = new List<Process>();
|
||||||
lock (_processLock)
|
lock (_processLock)
|
||||||
{
|
{
|
||||||
foreach (var kvp in _runningProcesses)
|
foreach (var kvp in _runningProcesses)
|
||||||
foreach (var p in kvp.Value)
|
victims.AddRange(kvp.Value);
|
||||||
try { if (!p.HasExited) p.Kill(); } catch { }
|
|
||||||
_runningProcesses.Clear();
|
_runningProcesses.Clear();
|
||||||
}
|
}
|
||||||
|
KillProcessTrees(victims);
|
||||||
SetTrayStatus("All apps stopped");
|
SetTrayStatus("All apps stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void KillProcessTrees(IEnumerable<Process> procs)
|
||||||
|
{
|
||||||
|
if (procs == null) return;
|
||||||
|
foreach (var p in procs) KillProcessTree(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every Tesla game runs under a supervisor that stays alive and re-spawns
|
||||||
|
// the game (Firestorm -> launcher.exe, Red Planet -> a looping .bat under
|
||||||
|
// cmd.exe, tesla410revival -> pod-launch.exe -> dosbox). net40's
|
||||||
|
// Process.Kill() terminates ONLY the tracked supervisor PID, so the game
|
||||||
|
// survives (and the supervisor/loop relaunches it). taskkill /T walks the
|
||||||
|
// parent-PID tree and terminates the supervisor AND its children in one
|
||||||
|
// shot; it ships on XP Professional and Win10/11. Process.Kill() remains
|
||||||
|
// the fallback if taskkill is somehow unavailable.
|
||||||
|
private static void KillProcessTree(Process p)
|
||||||
|
{
|
||||||
|
if (p == null) return;
|
||||||
|
int pid;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (p.HasExited) return;
|
||||||
|
pid = p.Id;
|
||||||
|
}
|
||||||
|
catch { return; }
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo("taskkill", "/PID " + pid + " /T /F")
|
||||||
|
{
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
WindowStyle = ProcessWindowStyle.Hidden
|
||||||
|
};
|
||||||
|
using (var tk = Process.Start(psi))
|
||||||
|
{
|
||||||
|
if (tk != null)
|
||||||
|
{
|
||||||
|
tk.WaitForExit(10000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* taskkill missing/failed — fall back to single-process kill */ }
|
||||||
|
|
||||||
|
try { if (!p.HasExited) p.Kill(); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
private void CmdShutdown(bool restart)
|
private void CmdShutdown(bool restart)
|
||||||
{
|
{
|
||||||
CmdKillAllApps();
|
CmdKillAllApps();
|
||||||
@@ -938,10 +991,8 @@ namespace Tesla.Launcher
|
|||||||
// ── Volume control ────────────────────────────────────────────────────
|
// ── Volume control ────────────────────────────────────────────────────
|
||||||
// The Console stores/retrieves volume as a float (0.0–1.0 scalar). We
|
// The Console stores/retrieves volume as a float (0.0–1.0 scalar). We
|
||||||
// cache the exact value the Console sent so get returns the same value
|
// cache the exact value the Console sent so get returns the same value
|
||||||
// without device roundtrip quantization error.
|
// without device roundtrip quantization error. The device chain
|
||||||
//
|
// (nircmd → CoreAudio → winmm) lives in VolumeControl.cs, shared with vPOD.
|
||||||
// Setter chain: nircmd.exe (legacy, works everywhere incl. XP)
|
|
||||||
// → CoreAudio (Vista+) → winmm waveOutSetVolume (XP fallback).
|
|
||||||
|
|
||||||
private float _cachedVolumeScalar = 1.0f;
|
private float _cachedVolumeScalar = 1.0f;
|
||||||
|
|
||||||
@@ -953,26 +1004,7 @@ namespace Tesla.Launcher
|
|||||||
private void SetMasterVolume(float scalar)
|
private void SetMasterVolume(float scalar)
|
||||||
{
|
{
|
||||||
_cachedVolumeScalar = scalar;
|
_cachedVolumeScalar = scalar;
|
||||||
|
VolumeControl.SetMasterScalar(scalar, GAMES_DIR);
|
||||||
var nircmd = Path.Combine(GAMES_DIR, "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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Session key ───────────────────────────────────────────────────────
|
// ── Session key ───────────────────────────────────────────────────────
|
||||||
@@ -1201,91 +1233,5 @@ namespace Tesla.Launcher
|
|||||||
public bool AutoRestart { get; set; }
|
public bool AutoRestart { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Windows Core Audio API — minimal COM interop (Vista+) ─────────────────
|
// (Core Audio / winmm volume interop moved to VolumeControl.cs — shared with vPOD.)
|
||||||
// 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 { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<Nullable>disable</Nullable>
|
<Nullable>disable</Nullable>
|
||||||
<ImplicitUsings>disable</ImplicitUsings>
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<Version>4.11.4.3</Version>
|
<Version>4.11.4.4</Version>
|
||||||
<ApplicationIcon>app.ico</ApplicationIcon>
|
<ApplicationIcon>app.ico</ApplicationIcon>
|
||||||
<AssemblyName>TeslaLauncher</AssemblyName>
|
<AssemblyName>TeslaLauncher</AssemblyName>
|
||||||
<RootNamespace>Tesla.Launcher</RootNamespace>
|
<RootNamespace>Tesla.Launcher</RootNamespace>
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// 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 { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -46,7 +46,7 @@ for %%a in (%*) do (
|
|||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ============================================================
|
echo ============================================================
|
||||||
echo Tesla Launcher v4.11.4.3 - Build ^& Package (net40 single binary)
|
echo Tesla Launcher v4.11.4.4 - Build ^& Package (net40 single binary)
|
||||||
echo Output : %BUILD_DIR%
|
echo Output : %BUILD_DIR%
|
||||||
echo ============================================================
|
echo ============================================================
|
||||||
echo.
|
echo.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
setlocal enabledelayedexpansion
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
echo ============================================================
|
echo ============================================================
|
||||||
echo Tesla Launcher v4.11.4.3 - Installation (single binary)
|
echo Tesla Launcher v4.11.4.4 - Installation (single binary)
|
||||||
echo ============================================================
|
echo ============================================================
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
@@ -142,12 +142,15 @@ if not exist "C:\Games" mkdir "C:\Games"
|
|||||||
|
|
||||||
:: Grant Users modify access to the data and games directories so the launcher
|
:: Grant Users modify access to the data and games directories so the launcher
|
||||||
:: can write files (LaunchApps.xml, session key, game installs) from any account.
|
:: can write files (LaunchApps.xml, session key, game installs) from any account.
|
||||||
|
:: The icacls grant token MUST stay quoted: its (OI)(CI) inheritance parens would
|
||||||
|
:: otherwise be read as the end of the if-block -- "(CI)M was unexpected at this
|
||||||
|
:: time" on Win10. Keep this note ABOVE the block; a stray ) inside ( ) breaks it.
|
||||||
if "%ISXP%"=="1" (
|
if "%ISXP%"=="1" (
|
||||||
cacls "%DATA_DIR%" /T /E /G Users:C >nul 2>&1
|
cacls "%DATA_DIR%" /T /E /G Users:C >nul 2>&1
|
||||||
cacls "C:\Games" /T /E /G Users:C >nul 2>&1
|
cacls "C:\Games" /T /E /G Users:C >nul 2>&1
|
||||||
) else (
|
) else (
|
||||||
icacls "%DATA_DIR%" /grant *S-1-5-32-545:(OI)(CI)M /T >nul 2>&1
|
icacls "%DATA_DIR%" /grant "*S-1-5-32-545:(OI)(CI)M" /T >nul 2>&1
|
||||||
icacls "C:\Games" /grant *S-1-5-32-545:(OI)(CI)M /T >nul 2>&1
|
icacls "C:\Games" /grant "*S-1-5-32-545:(OI)(CI)M" /T >nul 2>&1
|
||||||
)
|
)
|
||||||
echo %INSTALL_DIR%
|
echo %INSTALL_DIR%
|
||||||
echo %DATA_DIR% (Users: modify access)
|
echo %DATA_DIR% (Users: modify access)
|
||||||
@@ -203,12 +206,14 @@ if "%ISXP%"=="0" (
|
|||||||
)
|
)
|
||||||
|
|
||||||
:: Create and share game-data folders (closed network - open to Everyone).
|
:: Create and share game-data folders (closed network - open to Everyone).
|
||||||
|
:: The icacls grant token stays quoted so its (OI)(CI) parens are not read as the
|
||||||
|
:: end of the if-block. Keep this note ABOVE the block, not inside the ( ).
|
||||||
echo Creating network shares...
|
echo Creating network shares...
|
||||||
if not exist "C:\mw4files" mkdir "C:\mw4files"
|
if not exist "C:\mw4files" mkdir "C:\mw4files"
|
||||||
if "%ISXP%"=="1" (
|
if "%ISXP%"=="1" (
|
||||||
cacls "C:\mw4files" /T /E /G Everyone:C >nul 2>&1
|
cacls "C:\mw4files" /T /E /G Everyone:C >nul 2>&1
|
||||||
) else (
|
) else (
|
||||||
icacls "C:\mw4files" /grant *S-1-1-0:(OI)(CI)M /T >nul 2>&1
|
icacls "C:\mw4files" /grant "*S-1-1-0:(OI)(CI)M" /T >nul 2>&1
|
||||||
)
|
)
|
||||||
net share mw4files /delete >nul 2>&1
|
net share mw4files /delete >nul 2>&1
|
||||||
net share mw4files=C:\mw4files /grant:Everyone,FULL >nul 2>&1
|
net share mw4files=C:\mw4files /grant:Everyone,FULL >nul 2>&1
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ The Tesla cockpit-pod software, in one repository:
|
|||||||
|
|
||||||
| Folder | What it is | Target |
|
| Folder | What it is | Target |
|
||||||
|--------|------------|--------|
|
|--------|------------|--------|
|
||||||
| [`Console/`](Console/) | **TeslaConsole** — the operator console (WinForms) that configures and drives the pods. A decompiled reconstruction of the original `TeslaConsole.exe` (now the modernized 4.11.4.x line), with a differential test suite pinning it to the original 4.11.3.37076 baseline. | .NET Framework 4.8 |
|
| [`Console/`](Console/) | **TeslaConsole** — the operator console (WinForms) that configures and drives the pods. A decompiled reconstruction of the original `TeslaConsole.exe` (now the modernized 4.11.4.x line), with a differential test suite pinning it to the original 4.11.3.37076 baseline. | .NET Framework 4.0 |
|
||||||
| [`Launcher/`](Launcher/) | **TeslaLauncher** — the pod-side Service (Session 0 RPC listener) + Agent (user-session app launcher). A clean rewrite of the original launcher. | .NET Framework 4.8 |
|
| [`Launcher/`](Launcher/) | **TeslaLauncher** — the pod-side launcher: ONE userland tray app (RPC listener + app launcher). A clean rewrite of the original; the old Service+Agent split (a Session 0 workaround) is gone. | .NET Framework 4.0 |
|
||||||
| [`Contract/`](Contract/) | **Tesla.Contract** — the shared Console↔Launcher RPC contract: wire types, the client, and the framed-JSON protocol. Emits assembly `TeslaConsoleLaunchLib`. | .NET Framework 4.8 |
|
| [`Contract/`](Contract/) | **Tesla.Contract** — the shared Console↔Launcher RPC contract: wire types, the client, and the framed-JSON protocol. Emits assembly `TeslaConsoleLaunchLib`. | .NET Framework 4.0 |
|
||||||
| [`SecureConfig/`](SecureConfig/) | **Tesla.SecureConfig** — the first-boot pod provisioning protocol (UDP beacons, OFB crypto, RSA key exchange). Emits assembly `TeslaSecureConfiguration`. | .NET Framework 4.8 |
|
| [`SecureConfig/`](SecureConfig/) | **Tesla.SecureConfig** — the first-boot pod provisioning protocol (UDP beacons, OFB crypto, RSA key exchange). Emits assembly `TeslaSecureConfiguration`. | .NET Framework 4.0 |
|
||||||
| [`vPOD/`](vPOD/) | **vPOD** — a virtual pod for testing the consoles without cockpit hardware: impersonates both the game client (Munga, TCP 1501) and the pod's TeslaLauncher service (provisioning + Site Management / Install Product on TCP 53290). | .NET Framework 4.8 |
|
| [`vPOD/`](vPOD/) | **vPOD** — a virtual pod for testing the consoles without cockpit hardware: impersonates both the game client (Munga, TCP 1501) and the pod's TeslaLauncher (provisioning + Site Management / Install Product on TCP 53290). | .NET Framework 4.0 |
|
||||||
|
|
||||||
The console and launcher talk over **TCP 53290** using **length-prefixed
|
Everything targets **net40** on purpose (the XP11 port, v4.11.4.3): it is the newest
|
||||||
`System.Text.Json` frames over an OFB-encrypted stream** ([`Contract/PodRpcProtocol.cs`](Contract/PodRpcProtocol.cs)),
|
.NET Framework that installs on Windows XP SP3, and net40 assemblies run in-place on
|
||||||
|
the 4.8 runtime that ships in Windows 10/11 — so the same binaries cover the original
|
||||||
|
XP-era cockpit PCs and modern hardware. That rules out net45+ APIs
|
||||||
|
(`System.Text.Json`, `ZipFile`, async/await, ...); JSON is Newtonsoft, zip extraction
|
||||||
|
is the launcher's own [`MiniZip.cs`](Launcher/MiniZip.cs).
|
||||||
|
|
||||||
|
The console and launcher talk over **TCP 53290** using **length-prefixed JSON
|
||||||
|
frames over an OFB-encrypted stream** ([`Contract/PodRpcProtocol.cs`](Contract/PodRpcProtocol.cs)),
|
||||||
dispatched by method name. The wire contract lives in one source project
|
dispatched by method name. The wire contract lives in one source project
|
||||||
([`Contract/`](Contract/)) referenced by both sides — a single source of truth, no
|
([`Contract/`](Contract/)) referenced by both sides — a single source of truth, no
|
||||||
duplication or hand-syncing.
|
duplication or hand-syncing.
|
||||||
@@ -28,15 +35,21 @@ dotnet test Console/tests/TeslaConsole.DiffTests # differential + protocol
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Pod deployment:** [`Launcher/build.bat`](Launcher/build.bat) publishes the
|
**Pod deployment:** [`Launcher/build.bat`](Launcher/build.bat) publishes the
|
||||||
framework-dependent net48 package into `Launcher/dist/` (~1.6 MB zipped — no runtime to
|
framework-dependent net40 package into `Launcher/dist/` — the launcher itself is tiny,
|
||||||
install, since .NET Framework 4.8 ships in Windows 10/11), and
|
but the package bundles the pod redists (DirectX June 2010, OpenAL, UltraVNC, and
|
||||||
[`Launcher/install.bat`](Launcher/install.bat) deploys it on a cockpit PC (registers the
|
`dotNetFx40_Full_x86_x64.exe` for XP-era pods that don't have .NET 4.0 yet).
|
||||||
Service, sets up the Agent for auto-login, hardens the box). The operator console packages
|
[`Launcher/install.bat`](Launcher/install.bat) deploys it on a cockpit PC — dual-OS
|
||||||
|
(XP SP3 and Win10/11 code paths): auto-login, Run-key registration for the single
|
||||||
|
launcher binary, firewall + box hardening. The operator console packages
|
||||||
the same way: [`Console/build-package.bat`](Console/build-package.bat) → `Console/dist/`,
|
the same way: [`Console/build-package.bat`](Console/build-package.bat) → `Console/dist/`,
|
||||||
installed with [`Console/install.bat`](Console/install.bat). vPOD packages with
|
installed with [`Console/install.bat`](Console/install.bat). vPOD packages with
|
||||||
[`vPOD/pack.ps1`](vPOD/pack.ps1) → `vPOD/dist/vPOD.zip`, deployable to a pod via the
|
[`vPOD/pack.ps1`](vPOD/pack.ps1) → `vPOD/dist/vPOD.zip`, deployable to a pod via the
|
||||||
console's Install Product (or run directly on any machine).
|
console's Install Product (or run directly on any machine).
|
||||||
|
|
||||||
|
Release packages for all three are attached to the
|
||||||
|
[Gitea releases](https://gitea.mysticmachines.com/VWE/TeslaSuite/releases)
|
||||||
|
(latest: **v4.11.4.3**).
|
||||||
|
|
||||||
## Layout notes
|
## Layout notes
|
||||||
|
|
||||||
- `Console/original/TeslaConsole.exe` — the **4.11.3.37076** reference baseline the
|
- `Console/original/TeslaConsole.exe` — the **4.11.3.37076** reference baseline the
|
||||||
@@ -53,6 +66,11 @@ console's Install Product (or run directly on any machine).
|
|||||||
The system was modernized in 2026: the duplicated wire contract was extracted to a single
|
The system was modernized in 2026: the duplicated wire contract was extracted to a single
|
||||||
source project, `BinaryFormatter` (an RCE sink, and what pinned the launcher to an old
|
source project, `BinaryFormatter` (an RCE sink, and what pinned the launcher to an old
|
||||||
runtime) was replaced with the framed-JSON protocol, and the launcher was rebuilt — briefly
|
runtime) was replaced with the framed-JSON protocol, and the launcher was rebuilt — briefly
|
||||||
on net8/x64, then settled on net48 to match the console and ship a tiny, runtime-free
|
on net8/x64, then on net48, then (the **XP11** port, v4.11.4.3) the whole suite settled on
|
||||||
package. The whole console↔pod path (provisioning, install, launch) is validated on real
|
**net40** so one set of binaries runs on the original Windows XP SP3 cockpit hardware and
|
||||||
pods.
|
on Windows 10/11 alike. XP11 also merged the launcher's Service+Agent pair — a workaround
|
||||||
|
for Vista+ Session 0 isolation that XP never needed — back into a single userland app, and
|
||||||
|
moved the console's `.resx` BinaryFormatter bitmaps to raw embedded images (their runtime
|
||||||
|
reader doesn't exist on net40). The whole console↔pod path (provisioning, install, launch)
|
||||||
|
is validated on real pods; the net40 build is bench-validated on Win11's 4.8 runtime (real
|
||||||
|
XP SP3 hardware still pending).
|
||||||
|
|||||||
@@ -29,9 +29,10 @@ namespace VPod;
|
|||||||
/// - Install completion reports 99% (not 100) — the console's
|
/// - Install completion reports 99% (not 100) — the console's
|
||||||
/// InstallProductWorker breaks its retry loop only on 99.
|
/// InstallProductWorker breaks its retry loop only on 99.
|
||||||
///
|
///
|
||||||
/// All state lives in <see cref="VirtualLauncher" />; a packaged postinstall.bat
|
/// All state lives in <see cref="VirtualLauncher" />; packaged product scripts
|
||||||
/// is logged and removed unrun unless <see cref="VirtualLauncher.RunPostInstall" />
|
/// (postinstall.bat here, pre-uninstall.bat in UninstallApp) are logged and
|
||||||
/// is set from the vPOD window, in which case it is executed like the real service.
|
/// removed unrun unless <see cref="VirtualLauncher.RunPackageScripts" /> is set
|
||||||
|
/// from the vPOD window, in which case they run like on the real pod.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class LauncherRpcServer
|
internal sealed class LauncherRpcServer
|
||||||
{
|
{
|
||||||
@@ -350,13 +351,13 @@ internal sealed class LauncherRpcServer
|
|||||||
Log?.Invoke($"Install {callId:N}: extracted to {gamesRoot}");
|
Log?.Invoke($"Install {callId:N}: extracted to {gamesRoot}");
|
||||||
|
|
||||||
// The real service runs (then deletes) a packaged postinstall.bat here.
|
// The real service runs (then deletes) a packaged postinstall.bat here.
|
||||||
// vPOD only does so when the operator opts in via RunPostInstall;
|
// vPOD only does so when the operator opts in via RunPackageScripts;
|
||||||
// otherwise the script is logged and removed unrun (default), since it
|
// otherwise the script is logged and removed unrun (default), since it
|
||||||
// runs package code on the host machine.
|
// runs package code on the host machine.
|
||||||
string postInstall = Path.Combine(gamesRoot, "postinstall.bat");
|
string postInstall = Path.Combine(gamesRoot, "postinstall.bat");
|
||||||
if (File.Exists(postInstall))
|
if (File.Exists(postInstall))
|
||||||
{
|
{
|
||||||
if (mLauncher.RunPostInstall)
|
if (mLauncher.RunPackageScripts)
|
||||||
{
|
{
|
||||||
mLauncher.UpdateProgress(callId, 96, "Running postinstall...");
|
mLauncher.UpdateProgress(callId, 96, "Running postinstall...");
|
||||||
Log?.Invoke($"Install {callId:N}: running postinstall.bat...");
|
Log?.Invoke($"Install {callId:N}: running postinstall.bat...");
|
||||||
|
|||||||
+33
-12
@@ -55,9 +55,10 @@ internal sealed class VPodForm : Form
|
|||||||
private Label mInstallStatusLabel;
|
private Label mInstallStatusLabel;
|
||||||
private ProgressBar mInstallProgressBar;
|
private ProgressBar mInstallProgressBar;
|
||||||
private Button mReprovisionButton;
|
private Button mReprovisionButton;
|
||||||
private CheckBox mRunPostInstallCheckbox;
|
private CheckBox mRunScriptsCheckbox;
|
||||||
private CheckBox mRealLaunchCheckbox;
|
private CheckBox mRealLaunchCheckbox;
|
||||||
private CheckBox mRealAutoRestartCheckbox;
|
private CheckBox mRealAutoRestartCheckbox;
|
||||||
|
private CheckBox mRealVolumeCheckbox;
|
||||||
private ListView mAppsView;
|
private ListView mAppsView;
|
||||||
|
|
||||||
private bool mPoweredOn;
|
private bool mPoweredOn;
|
||||||
@@ -283,7 +284,7 @@ internal sealed class VPodForm : Form
|
|||||||
Padding = new Padding(8)
|
Padding = new Padding(8)
|
||||||
};
|
};
|
||||||
|
|
||||||
Panel siteTop = new Panel { Dock = DockStyle.Top, Height = 278 };
|
Panel siteTop = new Panel { Dock = DockStyle.Top, Height = 300 };
|
||||||
|
|
||||||
mProvisionStatusLabel = new Label
|
mProvisionStatusLabel = new Label
|
||||||
{
|
{
|
||||||
@@ -354,21 +355,22 @@ internal sealed class VPodForm : Form
|
|||||||
};
|
};
|
||||||
mReprovisionButton.Click += ReprovisionClicked;
|
mReprovisionButton.Click += ReprovisionClicked;
|
||||||
|
|
||||||
// Off (default) = a packaged postinstall.bat is logged and removed unrun;
|
// Off (default) = packaged product scripts (postinstall.bat on install,
|
||||||
// on = it is executed at the end of an install, like the real Agent.
|
// pre-uninstall.bat on uninstall) are logged and removed unrun; on =
|
||||||
mRunPostInstallCheckbox = new CheckBox
|
// they are executed like on the real pod.
|
||||||
|
mRunScriptsCheckbox = new CheckBox
|
||||||
{
|
{
|
||||||
Text = "Run postinstall.bat after install",
|
Text = "Run package install/uninstall scripts",
|
||||||
Location = new Point(4, 206),
|
Location = new Point(4, 206),
|
||||||
AutoSize = true,
|
AutoSize = true,
|
||||||
Checked = false
|
Checked = false
|
||||||
};
|
};
|
||||||
mRunPostInstallCheckbox.CheckedChanged += (s, e) =>
|
mRunScriptsCheckbox.CheckedChanged += (s, e) =>
|
||||||
{
|
{
|
||||||
mLauncher.RunPostInstall = mRunPostInstallCheckbox.Checked;
|
mLauncher.RunPackageScripts = mRunScriptsCheckbox.Checked;
|
||||||
OnSiteLog(mRunPostInstallCheckbox.Checked
|
OnSiteLog(mRunScriptsCheckbox.Checked
|
||||||
? "Installs now EXECUTE a packaged postinstall.bat."
|
? "Packaged postinstall.bat / pre-uninstall.bat now EXECUTE."
|
||||||
: "Installs skip (and remove) a packaged postinstall.bat.");
|
: "Packaged postinstall.bat / pre-uninstall.bat are skipped (and removed).");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Off = LaunchApp records simulated PIDs; on = start/kill real processes
|
// Off = LaunchApp records simulated PIDs; on = start/kill real processes
|
||||||
@@ -408,6 +410,24 @@ internal sealed class VPodForm : Form
|
|||||||
: "Watchdog OFF: exited apps stay down.");
|
: "Watchdog OFF: exited apps stay down.");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Off (default) = set_VolumeLevel only stores/echoes the float; on = it
|
||||||
|
// drives this machine's real master volume through the launcher's own
|
||||||
|
// chain (nircmd in the games root -> Core Audio -> winmm).
|
||||||
|
mRealVolumeCheckbox = new CheckBox
|
||||||
|
{
|
||||||
|
Text = "Actually set system volume",
|
||||||
|
Location = new Point(4, 272),
|
||||||
|
AutoSize = true,
|
||||||
|
Checked = false
|
||||||
|
};
|
||||||
|
mRealVolumeCheckbox.CheckedChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
mLauncher.RealVolume = mRealVolumeCheckbox.Checked;
|
||||||
|
OnSiteLog(mRealVolumeCheckbox.Checked
|
||||||
|
? "set_VolumeLevel now changes the REAL system master volume."
|
||||||
|
: "set_VolumeLevel now only stores the value (no audio change).");
|
||||||
|
};
|
||||||
|
|
||||||
siteTop.Controls.Add(mProvisionStatusLabel);
|
siteTop.Controls.Add(mProvisionStatusLabel);
|
||||||
siteTop.Controls.Add(mPassphrasePanel);
|
siteTop.Controls.Add(mPassphrasePanel);
|
||||||
siteTop.Controls.Add(mNetConfigLabel);
|
siteTop.Controls.Add(mNetConfigLabel);
|
||||||
@@ -415,9 +435,10 @@ internal sealed class VPodForm : Form
|
|||||||
siteTop.Controls.Add(mInstallStatusLabel);
|
siteTop.Controls.Add(mInstallStatusLabel);
|
||||||
siteTop.Controls.Add(mInstallProgressBar);
|
siteTop.Controls.Add(mInstallProgressBar);
|
||||||
siteTop.Controls.Add(mReprovisionButton);
|
siteTop.Controls.Add(mReprovisionButton);
|
||||||
siteTop.Controls.Add(mRunPostInstallCheckbox);
|
siteTop.Controls.Add(mRunScriptsCheckbox);
|
||||||
siteTop.Controls.Add(mRealLaunchCheckbox);
|
siteTop.Controls.Add(mRealLaunchCheckbox);
|
||||||
siteTop.Controls.Add(mRealAutoRestartCheckbox);
|
siteTop.Controls.Add(mRealAutoRestartCheckbox);
|
||||||
|
siteTop.Controls.Add(mRealVolumeCheckbox);
|
||||||
|
|
||||||
mAppsView = new ListView
|
mAppsView = new ListView
|
||||||
{
|
{
|
||||||
|
|||||||
+72
-6
@@ -6,6 +6,7 @@ using System.Threading;
|
|||||||
// No System.Text.Json on net40 — persistence goes through Newtonsoft, which
|
// No System.Text.Json on net40 — persistence goes through Newtonsoft, which
|
||||||
// writes the same JSON shape (public fields, PascalCase; see PodRpcProtocol).
|
// writes the same JSON shape (public fields, PascalCase; see PodRpcProtocol).
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Tesla.Launcher;
|
||||||
using Tesla.Net;
|
using Tesla.Net;
|
||||||
|
|
||||||
namespace VPod;
|
namespace VPod;
|
||||||
@@ -53,11 +54,20 @@ internal sealed class VirtualLauncher
|
|||||||
/// exactly like the real pod. Toggled from the vPOD window.</summary>
|
/// exactly like the real pod. Toggled from the vPOD window.</summary>
|
||||||
public bool RealAutoRestart { get; set; } = true;
|
public bool RealAutoRestart { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>When set, a packaged postinstall.bat is executed at the end of a
|
/// <summary>When set, packaged product scripts are executed like on the real
|
||||||
/// product install (like the real Agent) before being deleted, instead of
|
/// pod: postinstall.bat at the end of a product install (before being
|
||||||
/// being logged and removed unrun. Toggled from the vPOD window; off by
|
/// deleted), and pre-uninstall.bat before the product directory is removed
|
||||||
/// default, since it runs package script code on the host machine.</summary>
|
/// on uninstall. Off by default — both run package script code on the host
|
||||||
public bool RunPostInstall { get; set; }
|
/// machine — in which case they are logged and removed unrun. Toggled from
|
||||||
|
/// the vPOD window.</summary>
|
||||||
|
public bool RunPackageScripts { get; set; }
|
||||||
|
|
||||||
|
/// <summary>When set, the console's set_VolumeLevel changes this machine's
|
||||||
|
/// REAL master volume through the launcher's own chain (nircmd.exe in the
|
||||||
|
/// games root → Core Audio → winmm), exactly like the real pod. Off by
|
||||||
|
/// default: the value is only stored and displayed. Toggled from the vPOD
|
||||||
|
/// window.</summary>
|
||||||
|
public bool RealVolume { get; set; }
|
||||||
|
|
||||||
// Bumped to cancel watchdog restarts pending in their 2 s delay — the pod
|
// Bumped to cancel watchdog restarts pending in their 2 s delay — the pod
|
||||||
// "machine" went dark (power off / reboot / reprovision), so nothing may
|
// "machine" went dark (power off / reboot / reprovision), so nothing may
|
||||||
@@ -140,7 +150,17 @@ internal sealed class VirtualLauncher
|
|||||||
set
|
set
|
||||||
{
|
{
|
||||||
lock (mLock) { mVolumeLevel = value; }
|
lock (mLock) { mVolumeLevel = value; }
|
||||||
Log?.Invoke($"Volume set to {value:P0}.");
|
if (RealVolume)
|
||||||
|
{
|
||||||
|
// The launcher's own device chain; probes GamesRoot for
|
||||||
|
// nircmd.exe first, like the real pod's GAMES_DIR.
|
||||||
|
VolumeControl.SetMasterScalar(value, GamesRoot);
|
||||||
|
Log?.Invoke($"Volume set to {value:P0} (applied to system).");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log?.Invoke($"Volume set to {value:P0} (simulated).");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +255,7 @@ internal sealed class VirtualLauncher
|
|||||||
{
|
{
|
||||||
if (Directory.Exists(cleanupDir))
|
if (Directory.Exists(cleanupDir))
|
||||||
{
|
{
|
||||||
|
RunPreUninstallScript(cleanupDir);
|
||||||
Directory.Delete(cleanupDir, recursive: true);
|
Directory.Delete(cleanupDir, recursive: true);
|
||||||
Log?.Invoke($"UninstallApp: removed product directory {cleanupDir}");
|
Log?.Invoke($"UninstallApp: removed product directory {cleanupDir}");
|
||||||
}
|
}
|
||||||
@@ -248,6 +269,51 @@ internal sealed class VirtualLauncher
|
|||||||
AppsChanged?.Invoke();
|
AppsChanged?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>The real Agent's pre-uninstall hook: a product may ship a
|
||||||
|
/// pre-uninstall.bat (e.g. RIOJoy removes its driver) that runs before its
|
||||||
|
/// directory is deleted. Executed only when <see cref="RunPackageScripts" />
|
||||||
|
/// is opted in — otherwise logged and removed unrun (it dies with the
|
||||||
|
/// directory). Mirrors the launcher's CleanupProductDirectory.</summary>
|
||||||
|
private void RunPreUninstallScript(string productDir)
|
||||||
|
{
|
||||||
|
string preUninstall = Path.Combine(productDir, "pre-uninstall.bat");
|
||||||
|
if (!File.Exists(preUninstall))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!RunPackageScripts)
|
||||||
|
{
|
||||||
|
Log?.Invoke("UninstallApp: pre-uninstall.bat present — NOT executed (vPOD), removed with the directory.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log?.Invoke("UninstallApp: running pre-uninstall.bat...");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ProcessStartInfo psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = preUninstall,
|
||||||
|
WorkingDirectory = productDir,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
using (Process proc = Process.Start(psi))
|
||||||
|
{
|
||||||
|
if (proc != null && proc.WaitForExit(120000))
|
||||||
|
{
|
||||||
|
Log?.Invoke($"UninstallApp: pre-uninstall.bat exited with code {proc.ExitCode}.");
|
||||||
|
}
|
||||||
|
else if (proc != null)
|
||||||
|
{
|
||||||
|
Log?.Invoke("UninstallApp: pre-uninstall.bat still running after 2 min — continuing.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log?.Invoke($"UninstallApp: pre-uninstall.bat failed to run: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void RemoveApp(Guid launchKey)
|
public void RemoveApp(Guid launchKey)
|
||||||
{
|
{
|
||||||
lock (mLock)
|
lock (mLock)
|
||||||
|
|||||||
+5
-2
@@ -27,8 +27,8 @@
|
|||||||
<RootNamespace>VPod</RootNamespace>
|
<RootNamespace>VPod</RootNamespace>
|
||||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||||
<!-- Versioned with the suite since v4.11.4.3 (was its own 1.0.0 line). -->
|
<!-- Versioned with the suite since v4.11.4.3 (was its own 1.0.0 line). -->
|
||||||
<AssemblyVersion>4.11.4.3</AssemblyVersion>
|
<AssemblyVersion>4.11.4.4</AssemblyVersion>
|
||||||
<Version>4.11.4.3</Version>
|
<Version>4.11.4.4</Version>
|
||||||
<Product>vPOD</Product>
|
<Product>vPOD</Product>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
@@ -58,6 +58,9 @@
|
|||||||
behavior to the real pod service, and the differential suite's install
|
behavior to the real pod service, and the differential suite's install
|
||||||
round-trip exercises MiniZip against real ZipArchive-built archives. -->
|
round-trip exercises MiniZip against real ZipArchive-built archives. -->
|
||||||
<Compile Include="..\Launcher\MiniZip.cs" Link="MiniZip.cs" />
|
<Compile Include="..\Launcher\MiniZip.cs" Link="MiniZip.cs" />
|
||||||
|
<!-- The real pod's master-volume chain (nircmd -> CoreAudio -> winmm), for
|
||||||
|
the "Actually set system volume" mode. Same linked-source sharing. -->
|
||||||
|
<Compile Include="..\Launcher\VolumeControl.cs" Link="VolumeControl.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user