281 lines
13 KiB
Plaintext
281 lines
13 KiB
Plaintext
using System.Text;
|
|
|
|
namespace ShrinkNetwork.ServerHost.Framework;
|
|
|
|
public sealed class ServerHostPropertiesLoadResult
|
|
{
|
|
public required ServerHostOptions Options { get; init; }
|
|
public required string FilePath { get; init; }
|
|
public bool CreatedDefaultFile { get; init; }
|
|
public IReadOnlyList<string> Warnings { get; init; } = Array.Empty<string>();
|
|
}
|
|
|
|
public static class ServerHostProperties
|
|
{
|
|
public const string DefaultFileName = "server.properties";
|
|
|
|
public static ServerHostPropertiesLoadResult LoadOrCreate()
|
|
{
|
|
var filePath = ResolveFilePath();
|
|
var options = new ServerHostOptions();
|
|
var warnings = new List<string>();
|
|
var createdDefaultFile = false;
|
|
|
|
if (!File.Exists(filePath))
|
|
{
|
|
var directory = Path.GetDirectoryName(filePath);
|
|
if (!string.IsNullOrWhiteSpace(directory))
|
|
Directory.CreateDirectory(directory);
|
|
|
|
File.WriteAllText(filePath, BuildDefaultFile(filePath, options), Encoding.UTF8);
|
|
createdDefaultFile = true;
|
|
}
|
|
|
|
ApplyFile(filePath, options, warnings);
|
|
options.ApplyEnvironmentOverrides();
|
|
NormalizeOptions(options);
|
|
|
|
return new ServerHostPropertiesLoadResult
|
|
{
|
|
Options = options,
|
|
FilePath = filePath,
|
|
CreatedDefaultFile = createdDefaultFile,
|
|
Warnings = warnings
|
|
};
|
|
}
|
|
|
|
private static string ResolveFilePath()
|
|
{
|
|
var configured = Environment.GetEnvironmentVariable("SHRINK_SERVER_CONFIG_PATH");
|
|
if (!string.IsNullOrWhiteSpace(configured))
|
|
return Path.GetFullPath(configured.Trim());
|
|
|
|
var currentDirectoryCandidate = Path.Combine(Environment.CurrentDirectory, DefaultFileName);
|
|
if (File.Exists(currentDirectoryCandidate))
|
|
return currentDirectoryCandidate;
|
|
|
|
return ServerHostOptions.ResolveDefaultConfigFilePath();
|
|
}
|
|
|
|
private static void ApplyFile(string filePath, ServerHostOptions options, List<string> warnings)
|
|
{
|
|
var directory = Path.GetDirectoryName(filePath) ?? Environment.CurrentDirectory;
|
|
var lines = File.ReadAllLines(filePath);
|
|
for (var index = 0; index < lines.Length; index++)
|
|
{
|
|
var rawLine = lines[index].Trim();
|
|
if (string.IsNullOrWhiteSpace(rawLine) || rawLine.StartsWith("#", StringComparison.Ordinal) ||
|
|
rawLine.StartsWith(";", StringComparison.Ordinal))
|
|
continue;
|
|
|
|
var separatorIndex = rawLine.IndexOf('=');
|
|
if (separatorIndex <= 0)
|
|
{
|
|
warnings.Add($"第 {index + 1} 行不是有效的 key=value,将忽略。");
|
|
continue;
|
|
}
|
|
|
|
var key = rawLine[..separatorIndex].Trim();
|
|
var value = rawLine[(separatorIndex + 1)..].Trim();
|
|
ApplyValue(options, directory, key, value, warnings);
|
|
}
|
|
}
|
|
|
|
private static void ApplyValue(ServerHostOptions options, string directory, string key, string value, List<string> warnings)
|
|
{
|
|
switch (key)
|
|
{
|
|
case "server-port":
|
|
options.Port = ParseInt(value, options.Port, 1, warnings, key);
|
|
break;
|
|
case "unity-assets-path":
|
|
options.UnityAssetsPath = ResolvePath(directory, value, options.UnityAssetsPath);
|
|
break;
|
|
case "scan-output-directory":
|
|
options.ScanOutputDirectory = ResolvePath(directory, value, options.ScanOutputDirectory);
|
|
break;
|
|
case "shared-auth-token":
|
|
options.SharedAuthToken = value;
|
|
break;
|
|
case "allow-anonymous-when-auth-token-missing":
|
|
options.AllowAnonymousWhenAuthTokenMissing = ParseBool(value, options.AllowAnonymousWhenAuthTokenMissing, warnings, key);
|
|
break;
|
|
case "enable-session-tokens":
|
|
options.EnableSessionTokens = ParseBool(value, options.EnableSessionTokens, warnings, key);
|
|
break;
|
|
case "session-token-ttl-seconds":
|
|
options.SessionTokenTtlSeconds = ParseInt(value, options.SessionTokenTtlSeconds, 60, warnings, key);
|
|
break;
|
|
case "session-token-refresh-window-seconds":
|
|
options.SessionTokenRefreshWindowSeconds = ParseInt(value, options.SessionTokenRefreshWindowSeconds, 0, warnings, key);
|
|
break;
|
|
case "disconnect-on-invalid-session-token":
|
|
options.DisconnectOnInvalidSessionToken = ParseBool(value, options.DisconnectOnInvalidSessionToken, warnings, key);
|
|
break;
|
|
case "enable-unity-code-scan":
|
|
options.EnableUnityCodeScan = ParseBool(value, options.EnableUnityCodeScan, warnings, key);
|
|
break;
|
|
case "min-protocol-version":
|
|
options.MinProtocolVersion = ParseInt(value, options.MinProtocolVersion, 1, warnings, key);
|
|
break;
|
|
case "max-protocol-version":
|
|
options.MaxProtocolVersion = ParseInt(value, options.MaxProtocolVersion, 1, warnings, key);
|
|
break;
|
|
case "min-schema-version":
|
|
options.MinSchemaVersion = ParseInt(value, options.MinSchemaVersion, 1, warnings, key);
|
|
break;
|
|
case "max-schema-version":
|
|
options.MaxSchemaVersion = ParseInt(value, options.MaxSchemaVersion, 1, warnings, key);
|
|
break;
|
|
case "disconnect-on-protocol-violation":
|
|
options.DisconnectOnProtocolViolation = ParseBool(value, options.DisconnectOnProtocolViolation, warnings, key);
|
|
break;
|
|
case "diagnostics-log-interval-seconds":
|
|
options.DiagnosticsLogIntervalSeconds = ParseInt(value, options.DiagnosticsLogIntervalSeconds, 0, warnings, key);
|
|
break;
|
|
case "enable-tcp-tls":
|
|
options.EnableTcpTls = ParseBool(value, options.EnableTcpTls, warnings, key);
|
|
break;
|
|
case "tcp-tls-target-host":
|
|
options.TcpTlsTargetHost = NullIfEmpty(value);
|
|
break;
|
|
case "tcp-tls-certificate-path":
|
|
options.TcpTlsCertificatePath = ResolvePath(directory, value, options.TcpTlsCertificatePath);
|
|
break;
|
|
case "tcp-tls-certificate-password":
|
|
options.TcpTlsCertificatePassword = value;
|
|
break;
|
|
case "tcp-tls-check-certificate-revocation":
|
|
options.TcpTlsCheckCertificateRevocation = ParseBool(value, options.TcpTlsCheckCertificateRevocation, warnings, key);
|
|
break;
|
|
default:
|
|
warnings.Add($"未识别的配置键:{key}");
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static void NormalizeOptions(ServerHostOptions options)
|
|
{
|
|
options.SharedAuthToken = NullIfEmpty(options.SharedAuthToken);
|
|
options.TcpTlsTargetHost = NullIfEmpty(options.TcpTlsTargetHost);
|
|
options.TcpTlsCertificatePath = NullIfEmpty(options.TcpTlsCertificatePath);
|
|
options.TcpTlsCertificatePassword = NullIfEmpty(options.TcpTlsCertificatePassword);
|
|
options.SessionTokenRefreshWindowSeconds = Math.Max(0, options.SessionTokenRefreshWindowSeconds);
|
|
options.SessionTokenTtlSeconds = Math.Max(60, options.SessionTokenTtlSeconds);
|
|
options.Port = Math.Clamp(options.Port, 1, 65535);
|
|
options.DiagnosticsLogIntervalSeconds = Math.Max(0, options.DiagnosticsLogIntervalSeconds);
|
|
if (options.MaxProtocolVersion < options.MinProtocolVersion)
|
|
options.MaxProtocolVersion = options.MinProtocolVersion;
|
|
if (options.MaxSchemaVersion < options.MinSchemaVersion)
|
|
options.MaxSchemaVersion = options.MinSchemaVersion;
|
|
if (options.EnableTcpTls && string.IsNullOrWhiteSpace(options.TcpTlsCertificatePath))
|
|
options.EnableTcpTls = false;
|
|
}
|
|
|
|
private static string BuildDefaultFile(string filePath, ServerHostOptions options)
|
|
{
|
|
var directory = Path.GetDirectoryName(filePath) ?? Environment.CurrentDirectory;
|
|
var builder = new StringBuilder();
|
|
builder.AppendLine("# ShrinkNetwork server.properties");
|
|
builder.AppendLine("# 类似 Minecraft 的 key=value 配置文件。");
|
|
builder.AppendLine("# 优先级:代码默认值 < 本文件 < 环境变量。");
|
|
builder.AppendLine("# 修改后需要重启服务器进程。");
|
|
builder.AppendLine();
|
|
builder.AppendLine("# 基础网络");
|
|
builder.AppendLine($"server-port={options.Port}");
|
|
builder.AppendLine();
|
|
builder.AppendLine("# 代码扫描与生成");
|
|
builder.AppendLine($"unity-assets-path={ToRelativePath(directory, options.UnityAssetsPath)}");
|
|
builder.AppendLine($"scan-output-directory={ToRelativePath(directory, options.ScanOutputDirectory)}");
|
|
builder.AppendLine($"enable-unity-code-scan={options.EnableUnityCodeScan.ToString().ToLowerInvariant()}");
|
|
builder.AppendLine();
|
|
builder.AppendLine("# 登录与会话");
|
|
builder.AppendLine("shared-auth-token=");
|
|
builder.AppendLine($"allow-anonymous-when-auth-token-missing={options.AllowAnonymousWhenAuthTokenMissing.ToString().ToLowerInvariant()}");
|
|
builder.AppendLine($"enable-session-tokens={options.EnableSessionTokens.ToString().ToLowerInvariant()}");
|
|
builder.AppendLine($"session-token-ttl-seconds={options.SessionTokenTtlSeconds}");
|
|
builder.AppendLine($"session-token-refresh-window-seconds={options.SessionTokenRefreshWindowSeconds}");
|
|
builder.AppendLine($"disconnect-on-invalid-session-token={options.DisconnectOnInvalidSessionToken.ToString().ToLowerInvariant()}");
|
|
builder.AppendLine();
|
|
builder.AppendLine("# 协议兼容");
|
|
builder.AppendLine($"min-protocol-version={options.MinProtocolVersion}");
|
|
builder.AppendLine($"max-protocol-version={options.MaxProtocolVersion}");
|
|
builder.AppendLine($"min-schema-version={options.MinSchemaVersion}");
|
|
builder.AppendLine($"max-schema-version={options.MaxSchemaVersion}");
|
|
builder.AppendLine($"disconnect-on-protocol-violation={options.DisconnectOnProtocolViolation.ToString().ToLowerInvariant()}");
|
|
builder.AppendLine();
|
|
builder.AppendLine("# 观测");
|
|
builder.AppendLine($"diagnostics-log-interval-seconds={options.DiagnosticsLogIntervalSeconds}");
|
|
builder.AppendLine();
|
|
builder.AppendLine("# TCP TLS");
|
|
builder.AppendLine($"enable-tcp-tls={options.EnableTcpTls.ToString().ToLowerInvariant()}");
|
|
builder.AppendLine("tcp-tls-target-host=");
|
|
builder.AppendLine("tcp-tls-certificate-path=");
|
|
builder.AppendLine("tcp-tls-certificate-password=");
|
|
builder.AppendLine($"tcp-tls-check-certificate-revocation={options.TcpTlsCheckCertificateRevocation.ToString().ToLowerInvariant()}");
|
|
return builder.ToString();
|
|
}
|
|
|
|
private static int ParseInt(string raw, int fallback, int minValue, List<string> warnings, string key)
|
|
{
|
|
if (!int.TryParse(raw, out var value))
|
|
{
|
|
warnings.Add($"配置 {key} 不是有效整数,将回退为 {fallback}。");
|
|
return fallback;
|
|
}
|
|
|
|
return Math.Max(minValue, value);
|
|
}
|
|
|
|
private static bool ParseBool(string raw, bool fallback, List<string> warnings, string key)
|
|
{
|
|
if (TryParseBool(raw, out var parsed))
|
|
return parsed;
|
|
|
|
warnings.Add($"配置 {key} 不是有效布尔值,将回退为 {fallback}。");
|
|
return fallback;
|
|
}
|
|
|
|
private static bool TryParseBool(string raw, out bool value)
|
|
{
|
|
if (bool.TryParse(raw, out value))
|
|
return true;
|
|
|
|
switch (raw.Trim().ToLowerInvariant())
|
|
{
|
|
case "1":
|
|
case "yes":
|
|
case "on":
|
|
value = true;
|
|
return true;
|
|
case "0":
|
|
case "no":
|
|
case "off":
|
|
value = false;
|
|
return true;
|
|
default:
|
|
value = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static string ResolvePath(string baseDirectory, string raw, string? fallback)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(raw))
|
|
return fallback ?? string.Empty;
|
|
|
|
return Path.GetFullPath(Path.IsPathRooted(raw) ? raw : Path.Combine(baseDirectory, raw));
|
|
}
|
|
|
|
private static string ToRelativePath(string baseDirectory, string targetPath)
|
|
{
|
|
var relative = Path.GetRelativePath(baseDirectory, targetPath);
|
|
return relative.Replace('\\', '/');
|
|
}
|
|
|
|
private static string? NullIfEmpty(string? value)
|
|
{
|
|
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
|
}
|
|
}
|