Files

196 lines
9.5 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Microsoft.Win32.SafeHandles;
internal static class ServerLauncher
{
private static volatile bool stopping;
private static string stopFile;
private static int Main(string[] args)
{
Console.OutputEncoding = Encoding.UTF8;
if (Array.IndexOf(args, "--help") >= 0 || Array.IndexOf(args, "-h") >= 0)
{
Console.WriteLine("BLOCKLINE Dedicated Server - Windows x64\n" +
"Start-Server.exe [--config=C:\\Pfad\\server.json] [Optionen]\n" +
"Optionen ueberschreiben die JSON-Datei fuer diesen Start:\n" +
" --name=MeinServer --bind=0.0.0.0 --port=27840\n" +
" --mode=TDM|FFA --max-players=8 --bots=8 --difficulty=0..3\n" +
" --score-limit=50 --time-limit=600 --round-delay=10\n" +
" --discovery=true|false --run-for=Sekunden (optionaler Testlauf)\n" +
"Bots fuellen bis zur angegebenen Gesamtspielerzahl auf.\n" +
"Zeitangaben in Sekunden. Stoppen: Ctrl+C oder stop + Enter.\n" +
"Ohne Optionen wird server.json neben der EXE verwendet.");
return 0;
}
string root = AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar);
string logs = Path.Combine(root, "logs");
try
{
Directory.CreateDirectory(logs);
File.WriteAllText(Path.Combine(logs, ".gdignore"), "");
string engine = Path.Combine(root, "tools", "godot", "Godot_v4.5-stable_win64.exe");
if (!File.Exists(engine) || !File.Exists(Path.Combine(root, "project.godot")))
throw new IOException("Spielprojekt oder Engine fehlt. Bitte das gesamte Server-ZIP entpacken.");
string config = Path.Combine(root, "server.json");
var forwarded = new List<string>();
foreach (string arg in args)
{
if (arg.StartsWith("--config=")) config = Path.GetFullPath(arg.Substring(9));
else if (arg.StartsWith("--stop-file=") || arg == "--dedicated")
throw new ArgumentException("Reservierte Option: " + arg);
else forwarded.Add(arg);
}
if (!File.Exists(config)) throw new FileNotFoundException("server.json fehlt: " + config);
string session = DateTime.Now.ToString("yyyyMMdd-HHmmss") + "-" + Process.GetCurrentProcess().Id;
stopFile = Path.Combine(logs, "server-" + session + ".stop");
Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e) { e.Cancel = true; RequestStop(); };
Console.WriteLine("BLOCKLINE Dedicated Server\nKonfiguration: " + config + "\nProtokolle: " + logs);
using (var job = new ChildJob())
{
Console.WriteLine("Pruefe Spielressourcen ...");
string importLog = Path.Combine(logs, "server-import-" + session + ".log");
int imported = Run(engine, "--headless --editor --import --quit --path " + Quote(root) +
" --log-file " + Quote(importLog), root, job, 180000, false);
if (imported != 0 || stopping) return imported == 0 ? 0 : 1;
string content = File.Exists(importLog) ? File.ReadAllText(importLog) : "SCRIPT ERROR: missing log";
if (content.Contains("SCRIPT ERROR:") || content.Contains("Failed to load"))
throw new IOException("Ressourcenimport fehlgeschlagen. Details: " + importLog);
string command = "--headless --max-fps 60 --path " + Quote(root) + " --log-file " +
Quote(Path.Combine(logs, "server-" + session + ".log")) + " -- --dedicated " +
Quote("--config=" + config) + " " + Quote("--stop-file=" + stopFile);
foreach (string arg in forwarded) command += " " + Quote(arg);
var input = new Thread(delegate()
{
try
{
string line;
while ((line = Console.ReadLine()) != null)
{
if (line.Trim().Equals("stop", StringComparison.OrdinalIgnoreCase)) { RequestStop(); break; }
Console.WriteLine("Befehl: stop + Enter. Status wird alle 10 Sekunden ausgegeben.");
}
}
catch (IOException) { }
});
input.IsBackground = true;
input.Start();
return Run(engine, command, root, job, 0, true);
}
}
catch (Exception error)
{
Console.Error.WriteLine("SERVER START ERROR: " + error.Message);
return 1;
}
finally
{
if (stopFile != null) { try { File.Delete(stopFile); } catch { } }
}
}
private static void RequestStop()
{
if (stopping) return;
stopping = true;
Console.WriteLine("Server wird beendet ...");
try { if (stopFile != null) File.WriteAllText(stopFile, "stop"); } catch { }
}
private static int Run(string executable, string arguments, string root, ChildJob job, int timeout, bool server)
{
var info = new ProcessStartInfo(executable, arguments) { WorkingDirectory = root,
UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true };
using (var child = new Process { StartInfo = info })
{
child.OutputDataReceived += delegate(object sender, DataReceivedEventArgs e) { if (server && e.Data != null) Console.WriteLine(e.Data); };
child.ErrorDataReceived += delegate(object sender, DataReceivedEventArgs e) { if (e.Data != null) Console.Error.WriteLine(e.Data); };
child.Start();
try { job.Assign(child); } catch { child.Kill(); throw; }
child.BeginOutputReadLine();
child.BeginErrorReadLine();
var timer = Stopwatch.StartNew();
Stopwatch shutdown = null;
while (!child.WaitForExit(100))
{
if (stopping && shutdown == null) shutdown = Stopwatch.StartNew();
if ((timeout > 0 && timer.ElapsedMilliseconds > timeout) ||
(shutdown != null && (!server || shutdown.ElapsedMilliseconds > 10000)))
{
child.Kill();
child.WaitForExit();
if (stopping) return 0;
throw new TimeoutException("Ressourcenimport hat das Zeitlimit ueberschritten.");
}
}
child.WaitForExit(); // Drain redirected output callbacks.
return child.ExitCode;
}
}
// Windows command-line escaping; no shell ever interprets user values.
private static string Quote(string value)
{
var result = new StringBuilder("\"");
int slashes = 0;
foreach (char c in value)
{
if (c == '\\') { slashes++; continue; }
result.Append('\\', c == '"' ? slashes * 2 + 1 : slashes);
result.Append(c);
slashes = 0;
}
result.Append('\\', slashes * 2);
return result.Append('"').ToString();
}
// Closing the console or terminating the launcher also stops its engine.
private sealed class ChildJob : IDisposable
{
private SafeFileHandle handle;
[StructLayout(LayoutKind.Sequential)] private struct BasicLimits
{
public long ProcessTime, JobTime;
public uint Flags;
public UIntPtr MinWorking, MaxWorking;
public uint ActiveProcesses;
public UIntPtr Affinity;
public uint Priority, Scheduling;
}
[StructLayout(LayoutKind.Sequential)] private struct IoCounters
{ public ulong ReadOps, WriteOps, OtherOps, ReadBytes, WriteBytes, OtherBytes; }
[StructLayout(LayoutKind.Sequential)] private struct ExtendedLimits
{
public BasicLimits Basic;
public IoCounters Io;
public UIntPtr ProcessMemory, JobMemory, PeakProcessMemory, PeakJobMemory;
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern SafeFileHandle CreateJobObject(IntPtr attributes, string name);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetInformationJobObject(SafeFileHandle job, int infoClass, ref ExtendedLimits info, uint size);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AssignProcessToJobObject(SafeFileHandle job, IntPtr process);
internal ChildJob()
{
handle = CreateJobObject(IntPtr.Zero, null);
var limits = new ExtendedLimits();
limits.Basic.Flags = 0x2000; // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
if (handle.IsInvalid || !SetInformationJobObject(handle, 9, ref limits, (uint)Marshal.SizeOf(limits)))
{ handle.Dispose(); throw new IOException("Windows-Prozessverwaltung konnte nicht initialisiert werden."); }
}
internal void Assign(Process child)
{
if (!AssignProcessToJobObject(handle, child.Handle))
throw new IOException("Engine konnte nicht an den Serverstarter gebunden werden.");
}
public void Dispose() { handle.Dispose(); }
}
}