Files
MrMahoandClaude Opus 5.5 5fd162fecb Setup-Assistent und Update-Abfrage bei jedem Start
Die heruntergeladene Datei ist ein Setup: Sprache, Ordner (nur ohne Admin-Rechte beschreibbar),
Verknuepfungen, Fortschritt, Fertig-Seite; Ordner steht in der Registry. Deinstallieren loescht im
gewaehlten Ordner nur die eigenen Dateien. Beim Start fragt der Launcher nach neuen Updates
(Launcher und Mod).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:01:27 +01:00

231 lines
12 KiB
C#

using System.Diagnostics;
using System.Reflection;
using Microsoft.Win32;
namespace BSO.Launcher;
/// <summary>
/// Installation: die heruntergeladene .exe (BSO-Launcher-Setup.exe) ist zugleich das Setup (SetupForm). Sie kopiert sich
/// ohne Admin-Rechte in den gewaehlten Ordner (Standard %LocalAppData%\Programs\BSO Launcher), legt die gewaehlten
/// Verknuepfungen an, traegt sich unter "Apps &amp; Features" ein (Deinstallieren) und startet dann den installierten Launcher.
/// Der Ordner steht in der Registry (InstallLocation) - danach aktualisiert sich der Launcher dort selbst.
/// </summary>
public static class Installer
{
public const string AppName = "BSO Launcher";
const string ExeName = "BSO-Launcher.exe";
const string UninstallKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall\BSO-Launcher";
public static string DefaultDir =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", AppName);
/// <summary>Ordner der bestehenden Installation (aus "Apps &amp; Features") oder null.</summary>
public static string? RegisteredDir
{
get
{
try
{
using var key = Registry.CurrentUser.OpenSubKey(UninstallKey);
return key?.GetValue("InstallLocation") is string dir && dir.Length > 0 ? dir : null;
}
catch { return null; }
}
}
public static string InstallDir => RegisteredDir ?? DefaultDir;
public static string InstalledExe => Path.Combine(InstallDir, ExeName);
static string Self => Environment.ProcessPath ?? Application.ExecutablePath;
public static long SelfSize => new FileInfo(Self).Length;
static string StartMenuShortcut =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Programs), AppName + ".lnk");
static string DesktopShortcut =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), AppName + ".lnk");
public static bool HasStartMenuShortcut => File.Exists(StartMenuShortcut);
public static bool HasDesktopShortcut => File.Exists(DesktopShortcut);
/// <summary>Gewaehlter Ordner -> Installationsordner: immer ein eigener Unterordner "BSO Launcher" (beim Deinstallieren
/// wird nur der geloescht, nie z.B. "D:\Spiele").</summary>
public static string NormalizeDir(string chosen)
{
string dir = Path.GetFullPath(Environment.ExpandEnvironmentVariables(chosen.Trim().Trim('"')));
return string.Equals(Path.GetFileName(dir.TrimEnd('\\')), AppName, StringComparison.OrdinalIgnoreCase) ? dir : Path.Combine(dir, AppName);
}
/// <summary>Darf dieser Benutzer dort schreiben? (Programme ohne Admin-Rechte - auch fuer die Selbst-Updates noetig.)</summary>
public static string? CheckWritable(string dir)
{
try
{
Directory.CreateDirectory(dir);
string probe = Path.Combine(dir, ".bso-write-test");
File.WriteAllText(probe, "");
File.Delete(probe);
return null;
}
catch (UnauthorizedAccessException) { return "Keine Schreibrechte in diesem Ordner - bitte einen Ordner waehlen, in dem du ohne Admin-Rechte schreiben darfst"; }
catch (Exception e) { return "Ordner nicht nutzbar: " + e.Message; }
}
/// <summary>Laeuft gerade die installierte Kopie?</summary>
public static bool RunningInstalled =>
string.Equals(Path.GetFullPath(Self), Path.GetFullPath(InstalledExe), StringComparison.OrdinalIgnoreCase);
public static string? InstalledVersion()
{
if (!File.Exists(InstalledExe)) return null;
return FileVersionInfo.GetVersionInfo(InstalledExe).ProductVersion?.Split('+')[0];
}
/// <summary>Installieren (auch Update/Neu-Installation, auch in einen anderen Ordner). progress(0..1, Text).</summary>
public static void Install(string dir, bool desktop, bool startMenu, Action<double, string> progress)
{
string? oldExe = File.Exists(InstalledExe) ? InstalledExe : null;
string exe = Path.Combine(dir, ExeName);
progress(0.1, Lang.T("Laufenden Launcher schliessen …"));
foreach (var p in Process.GetProcessesByName("BSO-Launcher"))
{
try
{
if (p.Id == Environment.ProcessId) continue;
string? file = p.MainModule?.FileName;
if (!string.Equals(file, exe, StringComparison.OrdinalIgnoreCase) && !string.Equals(file, oldExe, StringComparison.OrdinalIgnoreCase)) continue;
p.CloseMainWindow();
if (!p.WaitForExit(3000)) p.Kill(true);
}
catch { }
}
progress(0.3, Lang.T("Dateien kopieren …"));
Directory.CreateDirectory(dir);
string tmp = exe + ".new";
File.Copy(Self, tmp, true);
File.Move(tmp, exe, true);
// Vorher woanders installiert: die alte Kopie wegraeumen (nur unsere Dateien, den Ordner nur wenn leer)
if (oldExe != null && !string.Equals(oldExe, exe, StringComparison.OrdinalIgnoreCase)) RemoveFiles(Path.GetDirectoryName(oldExe)!);
progress(0.6, Lang.T("Verknuepfungen anlegen …"));
Register(dir); // ab hier zeigen InstallDir/InstalledExe auf den neuen Ordner
if (startMenu) CreateShortcut(StartMenuShortcut); else TryDelete(StartMenuShortcut);
if (desktop) CreateShortcut(DesktopShortcut); else TryDelete(DesktopShortcut);
progress(1.0, Lang.T("Fertig"));
}
static void TryDelete(string file) { try { File.Delete(file); } catch { } }
/// <summary>Unsere Dateien im Installationsordner loeschen, den Ordner nur wenn er danach leer ist.</summary>
static void RemoveFiles(string dir)
{
foreach (string name in new[] { ExeName, ExeName + ".old", ExeName + ".new" }) TryDelete(Path.Combine(dir, name));
try { if (!Directory.EnumerateFileSystemEntries(dir).Any()) Directory.Delete(dir); } catch { }
}
/// <summary>Eintrag unter "Apps &amp; Features" (bzw. dessen Version nach einem Selbst-Update).</summary>
public static void Register(string? dir = null)
{
try
{
dir ??= InstallDir;
string exe = Path.Combine(dir, ExeName);
using var key = Registry.CurrentUser.CreateSubKey(UninstallKey);
key.SetValue("DisplayName", AppName);
key.SetValue("DisplayVersion", SelfUpdate.CurrentVersion);
key.SetValue("Publisher", "MBM.wtf");
key.SetValue("DisplayIcon", exe + ",0");
key.SetValue("InstallLocation", dir);
key.SetValue("UninstallString", $"\"{exe}\" --uninstall");
key.SetValue("QuietUninstallString", $"\"{exe}\" --uninstall --quiet");
key.SetValue("URLInfoAbout", Api.BaseUrl);
key.SetValue("NoModify", 1, RegistryValueKind.DWord);
key.SetValue("NoRepair", 1, RegistryValueKind.DWord);
try { key.SetValue("EstimatedSize", (int)(new FileInfo(exe).Length / 1024), RegistryValueKind.DWord); } catch { }
}
catch { /* Eintrag ist nur Komfort */ }
}
static void CreateShortcut(string path)
{
try
{
Type? shellType = Type.GetTypeFromProgID("WScript.Shell");
if (shellType == null) return;
dynamic shell = Activator.CreateInstance(shellType)!;
dynamic link = shell.CreateShortcut(path);
link.TargetPath = InstalledExe;
link.WorkingDirectory = InstallDir;
link.IconLocation = InstalledExe + ",0";
link.Description = "BSO - Bus Simulator Online";
link.Save();
}
catch { /* ohne Verknuepfung geht es auch */ }
}
/// <summary>Installierte Kopie starten - mit -dev, wenn diese hier mit -dev lief.</summary>
public static void StartInstalled()
{
var start = new ProcessStartInfo(InstalledExe) { UseShellExecute = true, WorkingDirectory = InstallDir };
if (Api.Dev) start.ArgumentList.Add(Api.DevArg);
Process.Start(start);
}
/// <summary>Ordner/Dateien loeschen, sobald dieser Prozess beendet ist (seine .exe ist bis dahin gesperrt). Danach
/// emptyDir entfernen, aber nur wenn er leer ist (der Benutzer hat den Ordner gewaehlt - fremde Dateien bleiben).</summary>
static void RemoveAfterExit(string emptyDir, params string[] paths)
{
static string Quote(string p) => "'" + p.Replace("'", "''") + "'";
string list = string.Join(",", paths.Where(p => !string.IsNullOrWhiteSpace(p)).Select(Quote));
string script =
$"Wait-Process -Id {Environment.ProcessId} -ErrorAction SilentlyContinue; " +
$"foreach ($i in 1..30) {{ $left = $false; foreach ($p in @({list})) {{ " +
"if (Test-Path -LiteralPath $p) { Remove-Item -LiteralPath $p -Recurse -Force -ErrorAction SilentlyContinue; " +
"if (Test-Path -LiteralPath $p) { $left = $true } } }; if (-not $left) { break }; Start-Sleep -Seconds 1 }; " +
$"$d = {Quote(emptyDir)}; if ((Test-Path -LiteralPath $d) -and -not (Get-ChildItem -LiteralPath $d -Force)) {{ Remove-Item -LiteralPath $d -Force }}";
var psi = new ProcessStartInfo("powershell.exe") { CreateNoWindow = true, UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden };
foreach (string a in new[] { "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", script })
psi.ArgumentList.Add(a);
try { Process.Start(psi); } catch { }
}
/// <summary>Deinstallieren (aus "Apps &amp; Features" oder den Einstellungen). false = abgebrochen.</summary>
public static bool Uninstall(bool quiet)
{
if (!quiet)
{
var answer = MessageBox.Show(
Lang.T("BSO komplett deinstallieren?\n\nDas entfernt den Launcher, BSO aus dem Bus-Simulator-2012-Ordner (dinput8.dll, BSO-Ordner) " +
"und deine Anmeldung im Launcher."),
AppName, MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
if (answer != DialogResult.Yes) return false;
while (Game.IsRunning())
{
if (MessageBox.Show(Lang.T("Erst Bus-Simulator 2012 schliessen - solange es laeuft, sind die Dateien in Benutzung."), AppName,
MessageBoxButtons.RetryCancel, MessageBoxIcon.Warning) != DialogResult.Retry) return false;
}
}
string dir = InstallDir;
foreach (var p in Process.GetProcessesByName("BSO-Launcher"))
try { if (p.Id != Environment.ProcessId) p.Kill(true); } catch { }
List<string> problems = Game.RemoveBso(Settings.Load());
try { File.Delete(StartMenuShortcut); } catch { }
try { File.Delete(DesktopShortcut); } catch { }
try { Registry.CurrentUser.DeleteSubKeyTree(UninstallKey, false); } catch { }
// Die laufende .exe laesst sich nicht selbst loeschen: aufraeumen, sobald dieser Prozess beendet ist
RemoveAfterExit(dir, Path.Combine(dir, ExeName), Path.Combine(dir, ExeName + ".old"), Path.Combine(dir, ExeName + ".new"), Settings.Folder);
if (!quiet)
MessageBox.Show(Lang.T(problems.Count == 0
? "BSO wurde komplett deinstalliert."
: "BSO wurde deinstalliert. Einiges liess sich nicht entfernen:\n\n" + string.Join("\n", problems)),
AppName, MessageBoxButtons.OK, problems.Count == 0 ? MessageBoxIcon.Information : MessageBoxIcon.Warning);
return true;
}
public static Image? Logo()
{
var s = Assembly.GetExecutingAssembly().GetManifestResourceStream("ui/logo.png");
return s == null ? null : Image.FromStream(s);
}
}