using System; using System.IO; using System.IO.Compression; using System.Drawing; using System.Diagnostics; using System.Reflection; using System.Threading.Tasks; using System.Windows.Forms; internal sealed class Setup : Form { Label status; TextBox destination; Button install; ProgressBar progress; CheckBox shortcut; bool busy; string installed; static readonly Color Mint = Color.FromArgb(119, 223, 199); [STAThread] static int Main(string[] args) { try { Application.EnableVisualStyles(); if (args.Length >= 2 && args[0] == "--test") { Install(args[1], Console.WriteLine, args.Length > 2 ? args[2] : null); return 0; } using (var form = new Setup()) { if (args.Length == 2 && args[0] == "--preview") { form.Show(); Application.DoEvents(); using (var bmp = new Bitmap(form.Width, form.Height)) { form.DrawToBitmap(bmp, new Rectangle(0, 0, form.Width, form.Height)); bmp.Save(args[1]); } return 0; } Application.Run(form); } return 0; } catch (Exception ex) { Console.Error.WriteLine(ex); return 1; } } Setup() { Text = "BLOCKLINE / Setup"; ClientSize = new Size(780, 540); BackColor = Color.FromArgb(10, 23, 31); ForeColor = Color.White; Font = new Font("Segoe UI", 10); StartPosition = FormStartPosition.CenterScreen; FormBorderStyle = FormBorderStyle.FixedSingle; MaximizeBox = false; AddLabel("B / L • LOCAL OPERATIONS", 35, 28, 12, Mint); AddLabel("DEIN NÄCHSTES MATCH.\nEINEN KLICK ENTFERNT.", 35, 72, 27, Color.White, 680, 95); AddLabel("BLOCKLINE / RELAY DISTRICT", 35, 184, 13, Mint); AddLabel("01 SPIELDATEIEN → 02 ENGINE → 03 SPIELBEREIT", 35, 222, 10, Color.Silver); AddLabel("INSTALLATIONSORDNER", 35, 268, 9, Mint); destination = new TextBox { Left = 35, Top = 293, Width = 600, Text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "BLOCKLINE"), BackColor = Color.FromArgb(31, 50, 59), ForeColor = Color.White, BorderStyle = BorderStyle.FixedSingle }; Controls.Add(destination); var browse = new Button { Left = 645, Top = 290, Width = 100, Height = 30, Text = "Ändern" }; browse.Click += delegate { using (var dialog = new FolderBrowserDialog()) { if (dialog.ShowDialog() == DialogResult.OK) destination.Text = Path.Combine(dialog.SelectedPath, "BLOCKLINE"); } }; Controls.Add(browse); shortcut = new CheckBox { Left = 35, Top = 333, Width = 500, Text = "Desktop-Verknüpfung erstellen", Checked = true }; Controls.Add(shortcut); progress = new ProgressBar { Left = 35, Top = 383, Width = 710, Height = 5 }; Controls.Add(progress); status = AddLabel("Godot wird automatisch geladen. Danach auch offline spielbar.", 35, 404, 10, Color.Silver); install = new Button { Left = 475, Top = 455, Width = 270, Height = 48, Text = "INSTALLIEREN →", FlatStyle = FlatStyle.Flat, BackColor = Mint, ForeColor = Color.FromArgb(10, 23, 31) }; install.Click += async delegate { if (installed != null) { Process.Start(new ProcessStartInfo(Path.Combine(installed, "Start-Game.exe"), "--local") { WorkingDirectory = installed }); Close(); return; } busy = true; install.Enabled = false; destination.Enabled = false; browse.Enabled = false; shortcut.Enabled = false; progress.Style = ProgressBarStyle.Marquee; try { string target = Path.GetFullPath(destination.Text); await Task.Run(() => Install(target, message => BeginInvoke((Action)(() => status.Text = message)), null)); installed = target; status.Text = "Bereit für Relay District. Installation erfolgreich."; if (shortcut.Checked) { try { CreateShortcut(target); } catch { status.Text = "Spiel installiert. Desktop-Link konnte nicht erstellt werden."; } } install.Text = "JETZT SPIELEN →"; progress.Style = ProgressBarStyle.Blocks; progress.Value = 100; } catch (Exception ex) { status.Text = "Installation fehlgeschlagen. Erneut versuchen."; MessageBox.Show(ex.Message, "BLOCKLINE Setup", MessageBoxButtons.OK, MessageBoxIcon.Error); destination.Enabled = true; browse.Enabled = true; shortcut.Enabled = true; } finally { busy = false; install.Enabled = true; } }; Controls.Add(install); AddLabel("LAN / 2–8 SPIELER\nKeine Anmeldung erforderlich", 35, 457, 10, Color.Silver, 390, 55); FormClosing += delegate(object sender, FormClosingEventArgs e) { if (busy) e.Cancel = true; }; } Label AddLabel(string text, int x, int y, int size, Color color, int width = 710, int height = 32) { var label = new Label { Text = text, Left = x, Top = y, Width = width, Height = height, Font = new Font("Segoe UI", size), ForeColor = color }; Controls.Add(label); return label; } static void CreateShortcut(string target) { dynamic shell = Activator.CreateInstance(Type.GetTypeFromProgID("WScript.Shell")); dynamic link = shell.CreateShortcut(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "BLOCKLINE.lnk")); link.TargetPath = Path.Combine(target, "Start-Game.exe"); link.Arguments = "--local"; link.WorkingDirectory = target; link.Save(); } internal static void Install(string target, Action status, string archive) { target = Path.GetFullPath(target).TrimEnd(Path.DirectorySeparatorChar); if (Directory.Exists(target) || File.Exists(target)) throw new IOException("Ziel existiert bereits. Bitte einen neuen Installationsordner wählen. Bestehende Dateien bleiben erhalten."); string parent = Path.GetDirectoryName(target); Directory.CreateDirectory(parent); string stage = Path.Combine(parent, ".blockline-setup-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(stage); try { status("Spieldateien werden installiert …"); using (var resource = Assembly.GetExecutingAssembly().GetManifestResourceStream("payload.zip")) using (var zip = new ZipArchive(resource, ZipArchiveMode.Read)) foreach (var entry in zip.Entries) { string path = Path.GetFullPath(Path.Combine(stage, entry.FullName)); if (!path.StartsWith(stage + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("Ungültiger Paketpfad."); if (entry.FullName.EndsWith("/")) continue; Directory.CreateDirectory(Path.GetDirectoryName(path)); entry.ExtractToFile(path); } EngineBootstrap.Install(stage, status, archive); status("Texturen, Modelle und Spielcode werden vorbereitet …"); string logs = Path.Combine(stage, "logs"); Directory.CreateDirectory(logs); string log = Path.Combine(logs, "setup-import.log"); var info = new ProcessStartInfo(Path.Combine(stage, "tools/godot/Godot_v4.5-stable_win64_console.exe"), "--headless --editor --import --quit --path \"" + stage + "\" --log-file \"" + log + "\""); info.UseShellExecute = false; info.CreateNoWindow = true; info.WorkingDirectory = stage; using (var process = Process.Start(info)) { if (!process.WaitForExit(180000)) { process.Kill(); throw new TimeoutException("Import dauert zu lange."); } if (process.ExitCode != 0 || !File.Exists(log) || File.ReadAllText(log).Contains("SCRIPT ERROR")) throw new IOException("Spielimport fehlgeschlagen. Installation wurde nicht aktiviert."); } Directory.Move(stage, target); status("Installation abgeschlossen."); } finally { // Only remove the freshly created staging folder, never an existing install. if (Path.GetDirectoryName(stage) == parent && Path.GetFileName(stage).StartsWith(".blockline-setup-") && Directory.Exists(stage)) Directory.Delete(stage, true); } } }