初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.IO.Net;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Core.Utils.Secret;
|
||||
using static PCL.Core.Link.EasyTier.ETInfoProvider;
|
||||
using static PCL.Core.Link.Lobby.LobbyInfoProvider;
|
||||
using static PCL.Core.Link.Natayark.NatayarkProfileManager;
|
||||
|
||||
namespace PCL.Core.Link.EasyTier;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
public enum ETState
|
||||
{
|
||||
Stopped,
|
||||
Running,
|
||||
Ready
|
||||
}
|
||||
|
||||
public static class ETController
|
||||
{
|
||||
public static Process? ETProcess { get; private set; }
|
||||
public static int ETRpcPort { get; private set; }
|
||||
public static ETState Status { get; internal set; }
|
||||
|
||||
public static int Precheck()
|
||||
{
|
||||
var existedET = Process.GetProcessesByName("easytier-core");
|
||||
foreach (var p in existedET)
|
||||
{
|
||||
LogWrapper.Warn("Link", $"发现已有的 EasyTier 实例,可能影响与启动器所用的实例通信: {p.Id}");
|
||||
}
|
||||
|
||||
// 检查文件
|
||||
LogWrapper.Info("Link", "EasyTier 路径: " + ETPath);
|
||||
if (!(File.Exists(ETPath + "\\easytier-core.exe") && File.Exists(ETPath + "\\easytier-cli.exe") &&
|
||||
File.Exists(ETPath + "\\Packet.dll")))
|
||||
{
|
||||
LogWrapper.Error("Link", "EasyTier 不存在或不完整");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int Launch(bool isHost, string? hostname = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (TargetLobby is null || Precheck() != 0)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
ETProcess = new Process
|
||||
{
|
||||
EnableRaisingEvents = true,
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = $"{ETPath}\\easytier-core.exe", WorkingDirectory = ETPath,
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
}
|
||||
};
|
||||
|
||||
var arguments = new ArgumentsBuilder();
|
||||
|
||||
// 大厅信息
|
||||
var name = TargetLobby.NetworkName;
|
||||
var secret = TargetLobby.NetworkSecret;
|
||||
|
||||
switch (TargetLobby.Type)
|
||||
{
|
||||
case LobbyType.PCLCE:
|
||||
name = ETNetworkNamePrefix + name;
|
||||
secret = ETNetworkSecretPrefix + secret;
|
||||
break;
|
||||
case LobbyType.Terracotta:
|
||||
name = "terracotta-mc-" + name;
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(Lang.Text("Link.Lobby.UnsupportedType", TargetLobby.Type));
|
||||
}
|
||||
|
||||
arguments.AddFlag("no-tun");
|
||||
arguments.Add("network-name", name);
|
||||
arguments.Add("network-secret", secret);
|
||||
arguments.Add("relay-network-whitelist", name);
|
||||
arguments.Add("private-mode", "true");
|
||||
// 网络参数
|
||||
if (isHost)
|
||||
{
|
||||
LogWrapper.Info("Link", $"本机作为创建者创建大厅,EasyTier 网络名称: {name}");
|
||||
arguments.Add("i", "10.114.51.41");
|
||||
arguments.Add("tcp-whitelist", TargetLobby.Port.ToString());
|
||||
arguments.Add("udp-whitelist", TargetLobby.Port.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
LogWrapper.Info("Link", $"本机作为加入者加入大厅,EasyTier 网络名称: {name}");
|
||||
arguments.AddFlag("d");
|
||||
arguments.Add("tcp-whitelist", "0");
|
||||
arguments.Add("udp-whitelist", "0");
|
||||
|
||||
JoinerLocalPort = NetworkHelper.NewTcpPort();
|
||||
LogWrapper.Info("Link", $"ET 端口转发: 远程 {TargetLobby.Port} -> 本地 {JoinerLocalPort}");
|
||||
arguments.Add("port-forward", $"tcp://127.0.0.1:{JoinerLocalPort}/{TargetLobby.Ip}:{TargetLobby.Port}");
|
||||
arguments.Add("port-forward", $"udp://127.0.0.1:{JoinerLocalPort}/{TargetLobby.Ip}:{TargetLobby.Port}");
|
||||
}
|
||||
|
||||
// 节点设置
|
||||
var relays = ETRelay.RelayList;
|
||||
var customNodes = Config.Link.CustomRelayServer;
|
||||
foreach (var node in customNodes.Split([';'], StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (node.Contains("tcp://") || node.Contains("udp://"))
|
||||
{
|
||||
relays.Add(new ETRelay
|
||||
{
|
||||
Url = node,
|
||||
Name = "Custom",
|
||||
Type = ETRelayType.Custom
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
LogWrapper.Warn("Link", $"无效的自定义节点 URL: {node}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var relay in
|
||||
from relay in relays
|
||||
let serverType = Config.Link.ServerType
|
||||
where (relay.Type == ETRelayType.Selfhosted && serverType != 2) ||
|
||||
(relay.Type == ETRelayType.Community && serverType == 1) || relay.Type == ETRelayType.Custom
|
||||
select relay)
|
||||
{
|
||||
arguments.Add("p", relay.Url);
|
||||
}
|
||||
|
||||
// 中继行为设置
|
||||
if (Config.Link.RelayType == LinkRelayBehavior.ForceRelay)
|
||||
{
|
||||
arguments.AddFlag("disable-p2p");
|
||||
}
|
||||
|
||||
// 数据流代理设置
|
||||
arguments.AddFlag("enable-quic-proxy");
|
||||
arguments.AddFlag("enable-kcp-proxy");
|
||||
arguments.AddFlag("use-smoltcp");
|
||||
arguments.Add("encryption-algorithm", "chacha20");
|
||||
arguments.Add("default-protocol", Config.Link.ProtocolPreference.ToString().ToLower());
|
||||
arguments.AddFlagIf(!Config.Link.TryPunchSym, "disable-sym-hole-punching");
|
||||
arguments.AddFlagIf(!Config.Link.EnableIPv6, "disable-ipv6");
|
||||
|
||||
// 用户名与其他参数
|
||||
arguments.AddFlagIf(Config.Link.UseLatencyFirstMode, "latency-first");
|
||||
arguments.Add("compression", "zstd");
|
||||
arguments.AddFlag("multi-thread");
|
||||
arguments.Add("machine-id", Identify.LauncherId);
|
||||
|
||||
// TODO: 等待玩家档案迁移以获取正在使用的档案名称
|
||||
var showName = "default";
|
||||
if (AllowCustomName && !string.IsNullOrWhiteSpace(Config.Link.Username))
|
||||
{
|
||||
showName = Config.Link.Username;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(NaidProfile.Username))
|
||||
{
|
||||
showName = NaidProfile.Username;
|
||||
}
|
||||
|
||||
arguments.Add("hostname",
|
||||
(isHost ? "H|" : "J|") + showName + (!string.IsNullOrWhiteSpace(hostname) ? "|" + hostname : ""));
|
||||
|
||||
// 指定 RPC 端口以避免与其他 ET 实例冲突
|
||||
ETRpcPort = NetworkHelper.NewTcpPort();
|
||||
arguments.Add("rpc-portal", $"127.0.0.1:{ETRpcPort}");
|
||||
|
||||
// 启动
|
||||
ETProcess.StartInfo.Arguments = arguments.GetResult();
|
||||
LogWrapper.Info("Link", "启动 EasyTier");
|
||||
// 操作 UI 显示大厅编号(可能写到 XAML 下面 UI 控制那部分去?)
|
||||
ETProcess.Start();
|
||||
Status = ETState.Running;
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Link", "尝试启动 EasyTier 时遇到问题");
|
||||
Status = ETState.Stopped;
|
||||
ETProcess = null;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Exit()
|
||||
{
|
||||
if (Status == ETState.Stopped || ETProcess is null) return;
|
||||
try
|
||||
{
|
||||
LogWrapper.Info("Link", $"关闭 EasyTier (PID: {ETProcess.Id})");
|
||||
ETProcess.Kill();
|
||||
ETProcess.WaitForExit(200);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
LogWrapper.Warn("Link", "EasyTier 进程不存在,可能已退出");
|
||||
}
|
||||
catch (NullReferenceException)
|
||||
{
|
||||
LogWrapper.Warn("Link", "EasyTier 进程不存在,可能已退出");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Link", "关闭 EasyTier 时遇到问题");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Status = ETState.Stopped;
|
||||
ETProcess = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL.Core.Link.EasyTier;
|
||||
// ReSharper disable InconsistentNaming, CompareOfFloatsByEqualityOperator
|
||||
|
||||
public enum ETConnectionType
|
||||
{
|
||||
Local,
|
||||
P2P,
|
||||
Relay,
|
||||
Unknown
|
||||
}
|
||||
|
||||
public class ETPlayerInfo
|
||||
{
|
||||
public required bool IsHost { get; init; }
|
||||
/// <summary>
|
||||
/// EasyTier 设置的原始主机名
|
||||
/// </summary>
|
||||
public required string Hostname { get; init; }
|
||||
/// <summary>
|
||||
/// 显示的用户名,依次可能为自定义用户名、NAID 用户名
|
||||
/// </summary>
|
||||
public string? Username { get; init; }
|
||||
public string? McName { get; init; }
|
||||
/// <summary>
|
||||
/// 连接方式
|
||||
/// </summary>
|
||||
public ETConnectionType Cost { get; init; } = ETConnectionType.Unknown;
|
||||
/// <summary>
|
||||
/// 延迟 (ms)
|
||||
/// </summary>
|
||||
public double Ping { get; set; }
|
||||
/// <summary>
|
||||
/// 丢包率 (%)
|
||||
/// </summary>
|
||||
public double Loss { get; init; }
|
||||
public string? NatType { get; init; }
|
||||
/// <summary>
|
||||
/// 节点的 EasyTier 版本
|
||||
/// </summary>
|
||||
public string? ETVersion { get; init; }
|
||||
}
|
||||
|
||||
public static class ETInfoProvider
|
||||
{
|
||||
public const string ETNetworkNamePrefix = "PCLCELobby";
|
||||
public const string ETNetworkSecretPrefix = "PCLCEETLOBBY2025";
|
||||
public const string ETVersion = Scaffolding.EasyTier.EasyTierMetadata.CurrentEasyTierVer;
|
||||
public static readonly string ETPath = Path.Combine(Paths.SharedLocalData, "EasyTier", ETVersion,
|
||||
"easytier-windows-" + (RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm64" : "x86_64"));
|
||||
|
||||
private static ETConnectionType _GetConnectionType(string cost)
|
||||
{
|
||||
if (IsContains("p2p")) return ETConnectionType.P2P;
|
||||
if (IsContains("relay")) return ETConnectionType.Relay;
|
||||
if (IsContains("local")) return ETConnectionType.Local;
|
||||
return ETConnectionType.Unknown;
|
||||
bool IsContains(string str) => cost.Contains(str, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
private static readonly Process _CliProcess = new() {
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = $"{ETPath}\\easytier-cli.exe",
|
||||
WorkingDirectory = ETPath,
|
||||
Arguments= $"--rpc-portal 127.0.0.1:{ETController.ETRpcPort} -o json peer",
|
||||
ErrorDialog = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
StandardErrorEncoding = Encoding.UTF8
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 检查 EasyTier 状态,若状态正常则返回 0。
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static async Task<int> CheckETStatusAsync()
|
||||
{
|
||||
var retryCount = 0;
|
||||
var process = ETController.ETProcess;
|
||||
while (process is null && retryCount < 10)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
retryCount++;
|
||||
}
|
||||
if (process is not null)
|
||||
{
|
||||
while (ETController.Status != ETState.Ready)
|
||||
{
|
||||
var info = GetPlayerList().Item1?[0];
|
||||
if (info is null)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
continue;
|
||||
}
|
||||
if (info.Ping != 1000) { ETController.Status = ETState.Ready; }
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 EasyTier 网络的成员列表和本地信息,若获取失败则均返回 null。
|
||||
/// </summary>
|
||||
/// <returns>Tuple(玩家列表, 本地信息)</returns>
|
||||
public static Tuple<List<ETPlayerInfo>?, ETPlayerInfo?> GetPlayerList()
|
||||
{
|
||||
try
|
||||
{
|
||||
_CliProcess.StartInfo.Arguments = $"--rpc-portal 127.0.0.1:{ETController.ETRpcPort} -o json peer";
|
||||
_CliProcess.Start();
|
||||
_CliProcess.WaitForExit(180);
|
||||
|
||||
var output = _CliProcess.StandardOutput.ReadToEnd() + _CliProcess.StandardError.ReadToEnd();
|
||||
if (!_CliProcess.HasExited)
|
||||
{
|
||||
LogWrapper.Warn("Link", "Cli 获取结果超时(180 ms),程序状态可能异常!");
|
||||
LogWrapper.Warn("Link", "获取到 EasyTier Cli 信息: \r\n" + output);
|
||||
}
|
||||
|
||||
var playerList = new List<ETPlayerInfo>();
|
||||
ETPlayerInfo? localInfo = null;
|
||||
if (JsonCompat.ParseNode(output) is not JsonArray json)
|
||||
return new Tuple<List<ETPlayerInfo>?, ETPlayerInfo?>(null, null);
|
||||
foreach (var p in json)
|
||||
{
|
||||
var info = p.Deserialize<ETPeerInfo>(JsonCompat.SerializerOptions);
|
||||
if (info is null) { continue; }
|
||||
if (info.Hostname.StartsWith("PublicServer")) { continue; } // 服务器
|
||||
var hostnameSplit = info.Hostname.Split('|');
|
||||
var playerInfo = new ETPlayerInfo
|
||||
{
|
||||
IsHost = info.Hostname.StartsWith("H|") || info.Ipv4 == "10.144.144.1",
|
||||
Hostname = info.Hostname,
|
||||
Username = hostnameSplit.Length >= 2 ? hostnameSplit[1] : null,
|
||||
McName = hostnameSplit.Length == 3 ? hostnameSplit[2] : null,
|
||||
Cost = _GetConnectionType(info.Cost),
|
||||
Ping = Math.Round(Convert.ToDouble(info.Ping != "-" ? info.Ping : "0")),
|
||||
Loss = Math.Round(Convert.ToDouble(info.Loss != "-" ? info.Loss.Replace("%", "") : "0")),
|
||||
NatType = info.NatType,
|
||||
ETVersion = info.ETVersion
|
||||
};
|
||||
|
||||
if (playerInfo.IsHost)
|
||||
{
|
||||
playerList.Insert(0, playerInfo); // 主机信息放在列表首位
|
||||
}
|
||||
else
|
||||
{
|
||||
playerList.Add(playerInfo);
|
||||
}
|
||||
if (playerInfo.Cost == ETConnectionType.Local)
|
||||
{
|
||||
localInfo = playerInfo;
|
||||
}
|
||||
}
|
||||
return new Tuple<List<ETPlayerInfo>?, ETPlayerInfo?>(playerList, localInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex,"Link", "获取 EasyTier 网络成员列表失败");
|
||||
return new Tuple<List<ETPlayerInfo>?, ETPlayerInfo?>(null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PCL.Core.Link.EasyTier;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
public record ETPeerInfo
|
||||
{
|
||||
[JsonPropertyName("hostname")] public required string Hostname { get; init; }
|
||||
[JsonPropertyName("ipv4")] public required string Ipv4 { get; init; }
|
||||
[JsonPropertyName("cost")] public required string Cost { get; init; }
|
||||
[JsonPropertyName("lat_ms")] public required string Ping { get; init; }
|
||||
[JsonPropertyName("loss_rate")] public required string Loss { get; init; }
|
||||
[JsonPropertyName("nat_type")] public required string NatType { get; init; }
|
||||
[JsonPropertyName("version")] public required string ETVersion { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PCL.Core.Link.EasyTier;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
public class ETRelay
|
||||
{
|
||||
public static List<ETRelay> RelayList { get; set; } = [];
|
||||
|
||||
public required string Url { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public ETRelayType Type { get; init; }
|
||||
}
|
||||
|
||||
public enum ETRelayType
|
||||
{
|
||||
Community,
|
||||
Selfhosted,
|
||||
Custom
|
||||
}
|
||||
Reference in New Issue
Block a user