Erste Version

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 17:26:25 +01:00
co-authored by Claude Opus 5.5
commit b69b9638ff
20 changed files with 1863 additions and 0 deletions
+244
View File
@@ -0,0 +1,244 @@
using System.Diagnostics;
using System.Reflection;
using Microsoft.Win32;
namespace BSO.Launcher;
/// <summary>
/// Installation wie bei Discord (wie beim MSCO Launcher): die heruntergeladene .exe installiert sich selbst ohne
/// Admin-Rechte nach %LocalAppData%\Programs\BSO Launcher, legt Verknuepfungen (Desktop, Startmenue) an, traegt sich unter
/// "Apps &amp; Features" ein (Deinstallieren) und startet dann den installierten Launcher.
/// </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 InstallDir =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", AppName);
public static string InstalledExe => Path.Combine(InstallDir, ExeName);
static string Self => Environment.ProcessPath ?? Application.ExecutablePath;
static string StartMenuShortcut =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Programs), AppName + ".lnk");
static string DesktopShortcut =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), AppName + ".lnk");
/// <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>Muss installiert (oder aktualisiert) werden? false = installierte Version ist gleich/neuer.</summary>
public static bool NeedsInstall()
{
string? installed = InstalledVersion();
return installed == null || SelfUpdate.IsNewer(SelfUpdate.CurrentVersion, installed);
}
/// <summary>Installieren. progress(0..1, Text).</summary>
public static void Install(Action<double, string> progress)
{
progress(0.1, "Laufenden Launcher schliessen …");
foreach (var p in Process.GetProcessesByName("BSO-Launcher"))
{
try
{
if (p.Id == Environment.ProcessId) continue;
if (!string.Equals(p.MainModule?.FileName, InstalledExe, StringComparison.OrdinalIgnoreCase)) continue;
p.CloseMainWindow();
if (!p.WaitForExit(3000)) p.Kill(true);
}
catch { }
}
progress(0.3, "Dateien kopieren …");
Directory.CreateDirectory(InstallDir);
string tmp = InstalledExe + ".new";
File.Copy(Self, tmp, true);
File.Move(tmp, InstalledExe, true);
progress(0.6, "Verknuepfungen anlegen …");
CreateShortcut(StartMenuShortcut);
CreateShortcut(DesktopShortcut);
progress(0.8, "Registrieren …");
Register();
progress(1.0, "Fertig");
}
/// <summary>Eintrag unter "Apps &amp; Features" (bzw. dessen Version nach einem Selbst-Update).</summary>
public static void Register()
{
try
{
using var key = Registry.CurrentUser.CreateSubKey(UninstallKey);
key.SetValue("DisplayName", AppName);
key.SetValue("DisplayVersion", SelfUpdate.CurrentVersion);
key.SetValue("Publisher", "MBM.wtf");
key.SetValue("DisplayIcon", InstalledExe + ",0");
key.SetValue("InstallLocation", InstallDir);
key.SetValue("UninstallString", $"\"{InstalledExe}\" --uninstall");
key.SetValue("QuietUninstallString", $"\"{InstalledExe}\" --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(InstalledExe).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 */ }
}
public static void StartInstalled()
{
Process.Start(new ProcessStartInfo(InstalledExe) { UseShellExecute = true, WorkingDirectory = InstallDir });
}
/// <summary>Ordner/Dateien loeschen, sobald dieser Prozess beendet ist (seine .exe ist bis dahin gesperrt).</summary>
static void RemoveAfterExit(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 }";
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(
"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("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, Settings.Folder);
if (!quiet)
MessageBox.Show(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);
}
}
/// <summary>Kleines Installationsfenster (wie der Discord-Installer): Logo, Status, Fortschrittsbalken.</summary>
public sealed class InstallForm : Form
{
readonly Label status = new();
double progress;
string? error;
public InstallForm()
{
FormBorderStyle = FormBorderStyle.None;
StartPosition = FormStartPosition.CenterScreen;
ClientSize = new Size(420, 260);
BackColor = Color.FromArgb(0x15, 0x19, 0x22);
ShowInTaskbar = true;
Text = "BSO Launcher wird installiert";
DoubleBuffered = true;
try { Icon = Icon.ExtractAssociatedIcon(Environment.ProcessPath ?? Application.ExecutablePath); } catch { }
status.ForeColor = Color.FromArgb(0x8b, 0x95, 0xa5);
status.Font = new Font("Segoe UI", 9.5f);
status.TextAlign = ContentAlignment.MiddleCenter;
status.SetBounds(20, 178, 380, 24);
status.Text = "Vorbereiten …";
Controls.Add(status);
Shown += async (_, _) => await Run();
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
using (var border = new Pen(Color.FromArgb(0x26, 0x2c, 0x38))) g.DrawRectangle(border, 0, 0, Width - 1, Height - 1);
var logo = Installer.Logo();
if (logo != null) g.DrawImage(logo, (Width - 72) / 2, 34, 72, 72);
using var title = new Font("Segoe UI", 15f, FontStyle.Bold);
TextRenderer.DrawText(g, "BSO Launcher", title, new Rectangle(0, 116, Width, 34), Color.White, TextFormatFlags.HorizontalCenter);
using var sub = new Font("Segoe UI", 9f);
TextRenderer.DrawText(g, "Bus Simulator Online", sub, new Rectangle(0, 148, Width, 22), Color.FromArgb(0x8b, 0x95, 0xa5), TextFormatFlags.HorizontalCenter);
// Fortschrittsbalken
var bar = new Rectangle(40, 214, Width - 80, 8);
using (var back = new SolidBrush(Color.FromArgb(0x23, 0x2a, 0x36))) g.FillRectangle(back, bar);
if (progress > 0)
{
var fill = new Rectangle(bar.X, bar.Y, (int)(bar.Width * progress), bar.Height);
using var brush = new System.Drawing.Drawing2D.LinearGradientBrush(bar, Color.FromArgb(0x1f, 0x6f, 0xe5), Color.FromArgb(0x5e, 0xe4, 0xff), 0f);
g.FillRectangle(error != null ? Brushes.IndianRed : brush, fill);
}
}
async Task Run()
{
try
{
await Task.Run(() => Installer.Install((p, text) => BeginInvoke(() => { progress = p; status.Text = text; Invalidate(); })));
await Task.Delay(400);
Installer.StartInstalled();
Close();
}
catch (Exception e)
{
error = e.Message;
status.ForeColor = Color.FromArgb(0xff, 0x8a, 0x8d);
status.Text = "Installation fehlgeschlagen: " + e.Message;
Invalidate();
await Task.Delay(6000);
Close();
}
}
}