using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Ashfall.Core;
using Unity.Collections;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using UnityEngine;
using UnityEngine.InputSystem;
using PlayerInput = Ashfall.Core.PlayerInput;
namespace Ashfall.Runtime
{
public sealed class GameSession : MonoBehaviour
{
private const string InputMessage = "ashfall.input", CommandMessage = "ashfall.command", SnapshotMessage = "ashfall.world", ReplyMessage = "ashfall.reply";
public const ushort Port = 7777;
public ContentCatalog Content { get; private set; }
public GameSimulation Simulation { get; private set; }
public WorldState World => Simulation?.World;
public string PlayerId => identity?.playerId;
public PlayerState LocalPlayer => World?.Player(PlayerId);
public bool Running { get; private set; }
public bool IsAuthority => offline || (network != null && network.IsServer);
public bool IsOffline => offline;
public string Status { get; private set; } = "准备开始远征";
private string saveOverride;
public string SavePath => saveOverride ?? Path.Combine(Application.persistentDataPath, "Worlds", SmokeHarness.Enabled ? "validation.sav" : "frontier.sav");
public void UseTestSave(string absolutePath)
{
if (Running) throw new InvalidOperationException("Stop the session before changing save storage.");
saveOverride = absolutePath; saves = new SaveStore(SavePath, new UnityWorldCodec());
}
public int Revision { get; private set; }
public PlayerState PredictedPlayer { get; private set; }
public VehicleState PredictedVehicle { get; private set; }
public float SnapshotAge { get; private set; }
private JoinIdentity identity;
private NetworkManager network;
private UnityTransport transport;
private SaveStore saves;
private bool offline, stopping, dirty, topologyDirty;
private float accumulator, snapshotTimer, saveTimer, dirtyTimer;
private long commandSequence;
private int inputSequence;
private PlayerInput controls = new PlayerInput();
private readonly Dictionary<ulong, string> clients = new Dictionary<ulong, string>();
private readonly Dictionary<ulong, JoinIdentity> pending = new Dictionary<ulong, JoinIdentity>();
private readonly List<PlayerInput> unacknowledged = new List<PlayerInput>();
private GameHud hud;
private WorldView view;
public event Action<string, bool> Message;
private void Awake()
{
Content = Resources.Load<GameContentAsset>("ContentCatalog")?.catalog ?? ContentCatalog.CreateDefault();
saves = new SaveStore(SavePath, new UnityWorldCodec());
string profile = Argument("--profile", "default");
try { identity = LocalProfile.Load(profile); }
catch (Exception ex) { Status = ex.Message; Debug.LogError(ex); }
}
private static string Argument(string name, string fallback)
{
var args = Environment.GetCommandLineArgs(); int index = Array.IndexOf(args, name);
return index >= 0 && index + 1 < args.Length ? args[index + 1] : fallback;
}
private void Start()
{
hud = GetComponent<GameHud>(); view = GetComponent<WorldView>();
string mode = Argument("--mode", "");
if (mode == "host") StartHost(Species.Human, "房主", !SmokeHarness.Enabled);
if (mode == "client") StartClient(Argument("--address", "127.0.0.1"), Species.Brood, "蠕虫乘员");
}
private bool PrepareWorld(bool load)
{
if (identity == null) { Toast("本地身份未准备好", false); return false; }
try
{
WorldState world = load && saves.Exists ? saves.Load() : WorldFactory.Create();
Simulation = new GameSimulation(world, Content); Simulation.PrepareRestoredWorld();
Simulation.CriticalChange = () => { dirty = topologyDirty = true; Revision++; };
Simulation.Notice = text => Toast(text, true);
commandSequence = world.Player(PlayerId)?.lastCommand ?? 0; inputSequence = 0;
accumulator = 0; saveTimer = 0; dirtyTimer = 0; Revision++;
return true;
}
catch (Exception ex) { Toast("读取失败,原存档保持不变:" + ex.Message, false); return false; }
}
public void StartOffline(Species species, string playerName, bool load)
{
if (Running || !PrepareWorld(load)) return;
identity.species = species; identity.name = playerName; offline = true;
var player = ConnectIdentity(identity);
if (player == null) { offline = false; Toast("此身份无法进入存档", false); return; }
Running = true; Status = "单人 · 本地世界"; Revision++;
}
public void StartFresh(Species species, string playerName)
{
if (Running) return;
try
{
string archive = Path.Combine(Path.GetDirectoryName(SavePath), "Archive", DateTime.UtcNow.ToString("yyyyMMdd-HHmmss-fff"));
for (int i = 0; i <= 3; i++)
{
string source = SavePath + (i == 0 ? "" : ".bak" + i);
if (!File.Exists(source)) continue;
Directory.CreateDirectory(archive); File.Copy(source, Path.Combine(archive, Path.GetFileName(source)));
}
StartOffline(species, playerName, false);
}
catch (Exception ex) { Toast("归档失败,未开始新世界:" + ex.Message, false); }
}
public void StartHost(Species species, string playerName, bool load)
{
if (Running || !PrepareWorld(load)) return;
identity.species = species; identity.name = playerName; offline = false;
SetupNetwork(); transport.SetConnectionData("127.0.0.1", Port, "0.0.0.0");
network.NetworkConfig.ConnectionData = Encoding.UTF8.GetBytes(JsonUtility.ToJson(identity));
try
{
if (!network.StartHost()) { Toast("无法启动房间,请检查端口 7777", false); return; }
RegisterHandlers(); Running = true; Status = "房主 · UDP 7777 · 最多四人";
if (!clients.ContainsKey(NetworkManager.ServerClientId)) OnConnected(NetworkManager.ServerClientId);
}
catch (Exception ex) { Toast(ex.Message, false); Stop(); }
}
public void StartClient(string address, Species species, string playerName)
{
if (Running || identity == null || string.IsNullOrWhiteSpace(address)) return;
identity.species = species; identity.name = playerName; offline = false; Simulation = null;
SetupNetwork(); transport.SetConnectionData(address.Trim(), Port);
network.NetworkConfig.ConnectionData = Encoding.UTF8.GetBytes(JsonUtility.ToJson(identity));
try
{
if (!network.StartClient()) { Toast("无法启动客户端", false); return; }
RegisterHandlers(); Running = true; Status = "正在连接 " + address; inputSequence = 0;
}
catch (Exception ex) { Toast(ex.Message, false); Stop(); }
}
private void SetupNetwork()
{
if (network != null) return;
// NGO requires its manager on a root object, including when it is created at runtime.
var obj = new GameObject("Authoritative session"); DontDestroyOnLoad(obj);
transport = obj.AddComponent<UnityTransport>();
// Joining clients receive a full world snapshot. Four simultaneous joins exceed UTP's default 128 packet queue.
transport.MaxPacketQueueSize = 1024;
transport.MaxSendQueueSize = 2 * 1024 * 1024;
network = obj.AddComponent<NetworkManager>();
network.NetworkConfig = new NetworkConfig { NetworkTransport = transport, TickRate = 30, EnableSceneManagement = false, ConnectionApproval = true };
network.ConnectionApprovalCallback = Approve;
network.OnClientConnectedCallback += OnConnected;
network.OnClientDisconnectCallback += OnDisconnected;
}
private PlayerState ConnectIdentity(JoinIdentity join)
{
var previous = World.Player(join.playerId);
string hash = LocalProfile.TokenHash(join.token);
if (previous != null && previous.reconnectTokenHash != "" && previous.reconnectTokenHash != hash) return null;
var player = Simulation.Connect(join.playerId, join.name, join.species);
if (player != null) player.reconnectTokenHash = hash;
return player;
}
private void Approve(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
{
response.CreatePlayerObject = false; response.Pending = false; response.Approved = false;
try
{
if (request.Payload == null || request.Payload.Length > 1024) { response.Reason = "无效身份数据"; return; }
var join = JsonUtility.FromJson<JoinIdentity>(Encoding.UTF8.GetString(request.Payload));
if (join == null || join.protocol != JoinIdentity.Protocol || !Guid.TryParse(join.playerId, out _) || join.token?.Length != 64
|| !Enum.IsDefined(typeof(Species), join.species)) { response.Reason = "版本或身份不匹配"; return; }
var player = World.Player(join.playerId);
if (player?.connected == true || pending.Values.Any(x => x.playerId == join.playerId)) { response.Reason = "此玩家已经在线"; return; }
if (player != null && player.reconnectTokenHash != "" && player.reconnectTokenHash != LocalProfile.TokenHash(join.token)) { response.Reason = "重连身份校验失败"; return; }
if (World.players.Count(x => x.connected) + pending.Count >= 4) { response.Reason = "房间已满"; return; }
pending[request.ClientNetworkId] = join; response.Approved = true;
}
catch (Exception) { response.Reason = "无法读取连接请求"; }
}
private void OnConnected(ulong clientId)
{
if (!network.IsServer) { Status = "已连接 · 等待世界快照"; return; }
if (clients.ContainsKey(clientId)) return;
JoinIdentity join;
if (clientId == NetworkManager.ServerClientId) join = identity;
else if (!pending.TryGetValue(clientId, out join)) { network.DisconnectClient(clientId); return; }
pending.Remove(clientId);
var player = ConnectIdentity(join);
if (player == null) { network.DisconnectClient(clientId); return; }
clients[clientId] = player.id; Revision++; dirty = topologyDirty = true;
if (clientId != NetworkManager.ServerClientId) SendWorld(clientId, true);
}
private void OnDisconnected(ulong clientId)
{
pending.Remove(clientId);
if (network.IsServer)
{
if (clients.TryGetValue(clientId, out var player)) { Simulation.Disconnect(player); clients.Remove(clientId); dirty = topologyDirty = true; Revision++; }
}
else if (!stopping)
{ string reason = network.DisconnectReason; Stop(); Toast(string.IsNullOrEmpty(reason) ? "连接结束,世界保留在房主电脑" : reason, false); }
}
private void RegisterHandlers()
{
var messages = network.CustomMessagingManager;
messages.RegisterNamedMessageHandler(InputMessage, (sender, reader) =>
{
if (!network.IsServer || !clients.TryGetValue(sender, out var id)) return;
try { var input = JsonUtility.FromJson<PlayerInput>(Read(ref reader, 2048)); Simulation.SetInput(id, input); }
catch (Exception) { Debug.LogWarning("Rejected malformed input packet"); }
});
messages.RegisterNamedMessageHandler(CommandMessage, (sender, reader) =>
{
if (!network.IsServer || !clients.TryGetValue(sender, out var id)) return;
try
{
var command = JsonUtility.FromJson<GameCommand>(Read(ref reader, 4096));
var result = Simulation.Execute(id, command);
Send(sender, ReplyMessage, JsonUtility.ToJson(new CommandReply { sequence = command.sequence, success = result.success, message = result.message }));
}
catch (Exception) { Debug.LogWarning("Rejected malformed command packet"); }
});
messages.RegisterNamedMessageHandler(SnapshotMessage, (sender, reader) =>
{
if (network.IsServer || sender != NetworkManager.ServerClientId) return;
try { ApplyPacket(JsonUtility.FromJson<WorldPacket>(Read(ref reader, 1024 * 1024))); }
catch (Exception ex) { Toast("世界快照无效:" + ex.Message, false); }
});
messages.RegisterNamedMessageHandler(ReplyMessage, (sender, reader) =>
{
if (network.IsServer || sender != NetworkManager.ServerClientId) return;
try { var reply = JsonUtility.FromJson<CommandReply>(Read(ref reader, 4096)); Toast(reply.message, reply.success); }
catch (Exception) { Debug.LogWarning("Invalid command reply"); }
});
}
private static string Read(ref FastBufferReader reader, int limit)
{
reader.ReadValueSafe(out int count);
if (count < 0 || count > limit) throw new InvalidDataException("消息超出限制");
byte[] data = new byte[count]; reader.ReadBytesSafe(ref data, count); return Encoding.UTF8.GetString(data);
}
private void Send(ulong recipient, string channel, string text, NetworkDelivery delivery = NetworkDelivery.ReliableFragmentedSequenced)
{
if (network == null || !network.IsListening || network.CustomMessagingManager == null) return;
byte[] data = Encoding.UTF8.GetBytes(text);
using (var writer = new FastBufferWriter(data.Length + 4, Allocator.Temp))
{ writer.WriteValueSafe(data.Length); writer.WriteBytesSafe(data); network.CustomMessagingManager.SendNamedMessage(channel, recipient, writer, delivery); }
}
private void SendWorld(ulong clientId, bool full)
{
if (!clients.TryGetValue(clientId, out var recipient)) return;
WorldState snapshot = World;
if (!full)
snapshot = new WorldState { tick = World.tick, time = World.time, nextId = World.nextId, players = World.players,
vehicles = World.vehicles, enemies = World.enemies, shots = World.shots, resources = World.resources,
facilities = World.facilities, research = World.research, warehouse = World.warehouse, bossDefeated = World.bossDefeated };
Send(clientId, SnapshotMessage, JsonUtility.ToJson(new WorldPacket { full = full, world = snapshot, recipient = recipient }));
}
private void ApplyPacket(WorldPacket packet)
{
if (packet == null || packet.world == null || packet.recipient != PlayerId) return;
if (World != null && packet.world.tick < World.tick) return;
if (packet.full)
{ SaveStore.Validate(packet.world); Simulation = new GameSimulation(packet.world, Content); Revision++; }
else if (World != null)
{
World.tick = packet.world.tick; World.time = packet.world.time; World.players = packet.world.players;
World.vehicles = packet.world.vehicles; World.enemies = packet.world.enemies; World.shots = packet.world.shots;
World.resources = packet.world.resources; World.facilities = packet.world.facilities; World.research = packet.world.research;
World.warehouse = packet.world.warehouse; World.bossDefeated = packet.world.bossDefeated;
}
if (LocalPlayer == null) return;
commandSequence = Math.Max(commandSequence, LocalPlayer.lastCommand); SnapshotAge = 0;
unacknowledged.RemoveAll(i => i.sequence <= LocalPlayer.acknowledgedInput);
PredictedPlayer = JsonUtility.FromJson<PlayerState>(JsonUtility.ToJson(LocalPlayer));
var controlled = Simulation.StationVehicle(LocalPlayer.stationId);
bool driver = Simulation.FindStation(LocalPlayer.stationId)?.role == StationRole.Driver;
PredictedVehicle = driver && controlled != null ? JsonUtility.FromJson<VehicleState>(JsonUtility.ToJson(controlled)) : null;
foreach (var input in unacknowledged)
{
if (PredictedPlayer.stationId == "" && PredictedPlayer.transitionId == "") GameSimulation.StepPlayer(World, PredictedPlayer, input, GameSimulation.TickSeconds);
if (PredictedVehicle != null) Simulation.StepVehicle(PredictedVehicle, input, GameSimulation.TickSeconds);
}
Status = "合作成员 · 世界由房主保存";
}
private void Update()
{
if (!Running || LocalPlayer == null) return;
SnapshotAge += Time.unscaledDeltaTime; CaptureControls(); accumulator += Time.unscaledDeltaTime;
int steps = 0;
while (accumulator >= GameSimulation.TickSeconds && steps++ < 5)
{
accumulator -= GameSimulation.TickSeconds;
var input = new PlayerInput { sequence = ++inputSequence, move = controls.move, aim = controls.aim, fire = controls.fire,
secondary = controls.secondary, sprint = controls.sprint, brake = controls.brake };
if (IsAuthority) { Simulation.SetInput(PlayerId, input); Simulation.Tick(GameSimulation.TickSeconds); }
else
{
Send(NetworkManager.ServerClientId, InputMessage, JsonUtility.ToJson(input), NetworkDelivery.UnreliableSequenced);
unacknowledged.Add(input); if (unacknowledged.Count > 90) unacknowledged.RemoveAt(0);
if (PredictedPlayer != null && PredictedPlayer.stationId == "" && PredictedPlayer.transitionId == "") GameSimulation.StepPlayer(World, PredictedPlayer, input, GameSimulation.TickSeconds);
if (PredictedVehicle != null) Simulation.StepVehicle(PredictedVehicle, input, GameSimulation.TickSeconds);
}
}
if (steps >= 5) accumulator = Mathf.Min(accumulator, GameSimulation.TickSeconds);
if (!IsAuthority) return;
snapshotTimer += Time.unscaledDeltaTime; saveTimer += Time.unscaledDeltaTime; dirtyTimer += Time.unscaledDeltaTime;
if (network != null && network.IsServer && snapshotTimer >= .1f)
{
snapshotTimer = 0; bool full = topologyDirty; topologyDirty = false;
foreach (var client in clients.Keys.ToArray()) if (client != NetworkManager.ServerClientId) SendWorld(client, full);
}
if (saveTimer >= 300 || (dirty && dirtyTimer >= 2)) Save();
}
private void CaptureControls()
{
if (SmokeHarness.Enabled) { controls = SmokeHarness.InputFor(this); return; }
var keyboard = Keyboard.current; var mouse = Mouse.current; if (keyboard == null) return;
controls = new PlayerInput();
controls.aim = view != null ? view.MouseGround : LocalPlayer.location.position + new V2(0, 1);
if (hud != null && hud.BlocksMovement) return;
controls.move = new V2((keyboard.dKey.isPressed ? 1 : 0) - (keyboard.aKey.isPressed ? 1 : 0), (keyboard.wKey.isPressed ? 1 : 0) - (keyboard.sKey.isPressed ? 1 : 0));
controls.sprint = keyboard.leftShiftKey.isPressed; controls.brake = keyboard.spaceKey.isPressed;
controls.fire = mouse != null && mouse.leftButton.isPressed && (hud == null || !hud.CapturesPointer) && (hud == null || !hud.BuildMode);
controls.secondary = mouse != null && mouse.rightButton.isPressed && (hud == null || (!hud.CapturesPointer && !hud.BuildMode));
if (keyboard.eKey.wasPressedThisFrame && hud != null) hud.Interact();
if (keyboard.digit1Key.wasPressedThisFrame) Command(new GameCommand { kind = LocalPlayer.stationId == "" ? CommandKind.Ability : CommandKind.Cruise });
if (keyboard.digit2Key.wasPressedThisFrame && LocalPlayer.stationId != "") Command(new GameCommand { kind = CommandKind.ChangeAltitude });
}
public void Command(GameCommand command)
{
if (!Running || LocalPlayer == null) return;
command.sequence = ++commandSequence;
if (IsAuthority) { var result = Simulation.Execute(PlayerId, command); Toast(result.message, result.success); }
else Send(NetworkManager.ServerClientId, CommandMessage, JsonUtility.ToJson(command));
}
public void Save()
{
if (!IsAuthority || World == null) return;
try { saves.Save(World); saveTimer = 0; dirtyTimer = 0; dirty = false; }
catch (Exception ex) { Toast("保存失败:" + ex.Message, false); dirtyTimer = 0; }
}
public void Stop()
{
if (stopping) return; stopping = true;
if (IsAuthority && Simulation != null)
{ foreach (var player in World.players.Where(p => p.connected).ToArray()) Simulation.Disconnect(player.id); Save(); }
Running = false;
if (network != null && network.IsListening) network.Shutdown();
clients.Clear(); pending.Clear(); unacknowledged.Clear(); PredictedPlayer = null; PredictedVehicle = null; offline = false;
Simulation = null; Revision++; stopping = false;
}
public void Toast(string text, bool good) { Status = text; Message?.Invoke(text, good); }
private void OnApplicationQuit() { if (Running) Stop(); }
private void OnDestroy() { if (network != null) Destroy(network.gameObject); }
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Ashfall.Core;
using Unity.Collections;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using UnityEngine;
using UnityEngine.InputSystem;
using PlayerInput = Ashfall.Core.PlayerInput;
namespace Ashfall.Runtime
{
public sealed class GameSession : MonoBehaviour
{
private const string InputMessage = "ashfall.input", CommandMessage = "ashfall.command", SnapshotMessage = "ashfall.world", ReplyMessage = "ashfall.reply";
public const ushort Port = 7777;
public ContentCatalog Content { get; private set; }
public GameSimulation Simulation { get; private set; }
public WorldState World => Simulation?.World;
public string PlayerId => identity?.playerId;
public PlayerState LocalPlayer => World?.Player(PlayerId);
public bool Running { get; private set; }
public bool IsAuthority => offline || (network != null && network.IsServer);
public bool IsOffline => offline;
public string Status { get; private set; } = "准备开始远征";
private string saveOverride;
public string SavePath => saveOverride ?? Path.Combine(Application.persistentDataPath, "Worlds", SmokeHarness.Enabled ? "validation.sav" : "frontier.sav");
public void UseTestSave(string absolutePath)
{
if (Running) throw new InvalidOperationException("Stop the session before changing save storage.");
saveOverride = absolutePath; saves = new SaveStore(SavePath, new UnityWorldCodec());
}
public int Revision { get; private set; }
public PlayerState PredictedPlayer { get; private set; }
public VehicleState PredictedVehicle { get; private set; }
public float SnapshotAge { get; private set; }
private JoinIdentity identity;
private NetworkManager network;
private UnityTransport transport;
private SaveStore saves;
private bool offline, stopping, dirty, topologyDirty;
private float accumulator, snapshotTimer, saveTimer, dirtyTimer;
private long commandSequence;
private int inputSequence;
private PlayerInput controls = new PlayerInput();
private readonly Dictionary<ulong, string> clients = new Dictionary<ulong, string>();
private readonly Dictionary<ulong, JoinIdentity> pending = new Dictionary<ulong, JoinIdentity>();
private readonly List<PlayerInput> unacknowledged = new List<PlayerInput>();
private GameHud hud;
private WorldView view;
public event Action<string, bool> Message;
private void Awake()
{
Content = Resources.Load<GameContentAsset>("ContentCatalog")?.catalog ?? ContentCatalog.CreateDefault();
saves = new SaveStore(SavePath, new UnityWorldCodec());
string profile = Argument("--profile", "default");
try { identity = LocalProfile.Load(profile); }
catch (Exception ex) { Status = ex.Message; Debug.LogError(ex); }
}
private static string Argument(string name, string fallback)
{
var args = Environment.GetCommandLineArgs(); int index = Array.IndexOf(args, name);
return index >= 0 && index + 1 < args.Length ? args[index + 1] : fallback;
}
private void Start()
{
hud = GetComponent<GameHud>(); view = GetComponent<WorldView>();
string mode = Argument("--mode", "");
if (mode == "host") StartHost(Species.Human, "房主", !SmokeHarness.Enabled);
if (mode == "client") StartClient(Argument("--address", "127.0.0.1"), Species.Brood, "蠕虫乘员");
}
private bool PrepareWorld(bool load)
{
if (identity == null) { Toast("本地身份未准备好", false); return false; }
try
{
WorldState world = load && saves.Exists ? saves.Load() : WorldFactory.Create();
Simulation = new GameSimulation(world, Content); Simulation.PrepareRestoredWorld();
Simulation.CriticalChange = () => { dirty = topologyDirty = true; Revision++; };
Simulation.Notice = text => Toast(text, true);
commandSequence = world.Player(PlayerId)?.lastCommand ?? 0; inputSequence = 0;
accumulator = 0; saveTimer = 0; dirtyTimer = 0; Revision++;
return true;
}
catch (Exception ex) { Toast("读取失败,原存档保持不变:" + ex.Message, false); return false; }
}
public void StartOffline(Species species, string playerName, bool load)
{
if (Running || !PrepareWorld(load)) return;
identity.species = species; identity.name = playerName; offline = true;
var player = ConnectIdentity(identity);
if (player == null) { offline = false; Toast("此身份无法进入存档", false); return; }
Running = true; Status = "单人 · 本地世界"; Revision++;
}
public void StartFresh(Species species, string playerName)
{
if (Running) return;
try
{
string archive = Path.Combine(Path.GetDirectoryName(SavePath), "Archive", DateTime.UtcNow.ToString("yyyyMMdd-HHmmss-fff"));
for (int i = 0; i <= 3; i++)
{
string source = SavePath + (i == 0 ? "" : ".bak" + i);
if (!File.Exists(source)) continue;
Directory.CreateDirectory(archive); File.Copy(source, Path.Combine(archive, Path.GetFileName(source)));
}
StartOffline(species, playerName, false);
}
catch (Exception ex) { Toast("归档失败,未开始新世界:" + ex.Message, false); }
}
public void StartHost(Species species, string playerName, bool load)
{
if (Running || !PrepareWorld(load)) return;
identity.species = species; identity.name = playerName; offline = false;
SetupNetwork(); transport.SetConnectionData("127.0.0.1", Port, "0.0.0.0");
network.NetworkConfig.ConnectionData = Encoding.UTF8.GetBytes(JsonUtility.ToJson(identity));
try
{
if (!network.StartHost()) { Toast("无法启动房间,请检查端口 7777", false); return; }
RegisterHandlers(); Running = true; Status = "房主 · UDP 7777 · 最多四人";
if (!clients.ContainsKey(NetworkManager.ServerClientId)) OnConnected(NetworkManager.ServerClientId);
}
catch (Exception ex) { Toast(ex.Message, false); Stop(); }
}
public void StartClient(string address, Species species, string playerName)
{
if (Running || identity == null || string.IsNullOrWhiteSpace(address)) return;
identity.species = species; identity.name = playerName; offline = false; Simulation = null;
SetupNetwork(); transport.SetConnectionData(address.Trim(), Port);
network.NetworkConfig.ConnectionData = Encoding.UTF8.GetBytes(JsonUtility.ToJson(identity));
try
{
if (!network.StartClient()) { Toast("无法启动客户端", false); return; }
RegisterHandlers(); Running = true; Status = "正在连接 " + address; inputSequence = 0;
}
catch (Exception ex) { Toast(ex.Message, false); Stop(); }
}
private void SetupNetwork()
{
if (network != null) return;
// NGO requires its manager on a root object, including when it is created at runtime.
var obj = new GameObject("Authoritative session"); DontDestroyOnLoad(obj);
transport = obj.AddComponent<UnityTransport>();
// Joining clients receive a full world snapshot. Four simultaneous joins exceed UTP's default 128 packet queue.
transport.MaxPacketQueueSize = 1024;
transport.MaxSendQueueSize = 2 * 1024 * 1024;
network = obj.AddComponent<NetworkManager>();
network.NetworkConfig = new NetworkConfig { NetworkTransport = transport, TickRate = 30, EnableSceneManagement = false, ConnectionApproval = true };
network.ConnectionApprovalCallback = Approve;
network.OnClientConnectedCallback += OnConnected;
network.OnClientDisconnectCallback += OnDisconnected;
}
private PlayerState ConnectIdentity(JoinIdentity join)
{
var previous = World.Player(join.playerId);
string hash = LocalProfile.TokenHash(join.token);
if (previous != null && previous.reconnectTokenHash != "" && previous.reconnectTokenHash != hash) return null;
var player = Simulation.Connect(join.playerId, join.name, join.species);
if (player != null) player.reconnectTokenHash = hash;
return player;
}
private void Approve(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
{
response.CreatePlayerObject = false; response.Pending = false; response.Approved = false;
try
{
if (request.Payload == null || request.Payload.Length > 1024) { response.Reason = "无效身份数据"; return; }
var join = JsonUtility.FromJson<JoinIdentity>(Encoding.UTF8.GetString(request.Payload));
if (join == null || join.protocol != JoinIdentity.Protocol || !Guid.TryParse(join.playerId, out _) || join.token?.Length != 64
|| !Enum.IsDefined(typeof(Species), join.species)) { response.Reason = "版本或身份不匹配"; return; }
var player = World.Player(join.playerId);
if (player?.connected == true || pending.Values.Any(x => x.playerId == join.playerId)) { response.Reason = "此玩家已经在线"; return; }
if (player != null && player.reconnectTokenHash != "" && player.reconnectTokenHash != LocalProfile.TokenHash(join.token)) { response.Reason = "重连身份校验失败"; return; }
if (World.players.Count(x => x.connected) + pending.Count >= 4) { response.Reason = "房间已满"; return; }
pending[request.ClientNetworkId] = join; response.Approved = true;
}
catch (Exception) { response.Reason = "无法读取连接请求"; }
}
private void OnConnected(ulong clientId)
{
if (!network.IsServer) { Status = "已连接 · 等待世界快照"; return; }
if (clients.ContainsKey(clientId)) return;
JoinIdentity join;
if (clientId == NetworkManager.ServerClientId) join = identity;
else if (!pending.TryGetValue(clientId, out join)) { network.DisconnectClient(clientId); return; }
pending.Remove(clientId);
var player = ConnectIdentity(join);
if (player == null) { network.DisconnectClient(clientId); return; }
clients[clientId] = player.id; Revision++; dirty = topologyDirty = true;
if (clientId != NetworkManager.ServerClientId) SendWorld(clientId, true);
}
private void OnDisconnected(ulong clientId)
{
pending.Remove(clientId);
if (network.IsServer)
{
if (clients.TryGetValue(clientId, out var player)) { Simulation.Disconnect(player); clients.Remove(clientId); dirty = topologyDirty = true; Revision++; }
}
else if (!stopping)
{ string reason = network.DisconnectReason; Stop(); Toast(string.IsNullOrEmpty(reason) ? "连接结束,世界保留在房主电脑" : reason, false); }
}
private void RegisterHandlers()
{
var messages = network.CustomMessagingManager;
messages.RegisterNamedMessageHandler(InputMessage, (sender, reader) =>
{
if (!network.IsServer || !clients.TryGetValue(sender, out var id)) return;
try { var input = JsonUtility.FromJson<PlayerInput>(Read(ref reader, 2048)); Simulation.SetInput(id, input); }
catch (Exception) { Debug.LogWarning("Rejected malformed input packet"); }
});
messages.RegisterNamedMessageHandler(CommandMessage, (sender, reader) =>
{
if (!network.IsServer || !clients.TryGetValue(sender, out var id)) return;
try
{
var command = JsonUtility.FromJson<GameCommand>(Read(ref reader, 4096));
var result = Simulation.Execute(id, command);
Send(sender, ReplyMessage, JsonUtility.ToJson(new CommandReply { sequence = command.sequence, success = result.success, message = result.message }));
}
catch (Exception) { Debug.LogWarning("Rejected malformed command packet"); }
});
messages.RegisterNamedMessageHandler(SnapshotMessage, (sender, reader) =>
{
if (network.IsServer || sender != NetworkManager.ServerClientId) return;
try { ApplyPacket(JsonUtility.FromJson<WorldPacket>(Read(ref reader, 1024 * 1024))); }
catch (Exception ex) { Toast("世界快照无效:" + ex.Message, false); }
});
messages.RegisterNamedMessageHandler(ReplyMessage, (sender, reader) =>
{
if (network.IsServer || sender != NetworkManager.ServerClientId) return;
try { var reply = JsonUtility.FromJson<CommandReply>(Read(ref reader, 4096)); Toast(reply.message, reply.success); }
catch (Exception) { Debug.LogWarning("Invalid command reply"); }
});
}
private static string Read(ref FastBufferReader reader, int limit)
{
reader.ReadValueSafe(out int count);
if (count < 0 || count > limit) throw new InvalidDataException("消息超出限制");
byte[] data = new byte[count]; reader.ReadBytesSafe(ref data, count); return Encoding.UTF8.GetString(data);
}
private void Send(ulong recipient, string channel, string text, NetworkDelivery delivery = NetworkDelivery.ReliableFragmentedSequenced)
{
if (network == null || !network.IsListening || network.CustomMessagingManager == null) return;
byte[] data = Encoding.UTF8.GetBytes(text);
using (var writer = new FastBufferWriter(data.Length + 4, Allocator.Temp))
{ writer.WriteValueSafe(data.Length); writer.WriteBytesSafe(data); network.CustomMessagingManager.SendNamedMessage(channel, recipient, writer, delivery); }
}
private void SendWorld(ulong clientId, bool full)
{
if (!clients.TryGetValue(clientId, out var recipient)) return;
WorldState snapshot = World;
if (!full)
snapshot = new WorldState { tick = World.tick, time = World.time, nextId = World.nextId, players = World.players,
vehicles = World.vehicles, enemies = World.enemies, shots = World.shots, resources = World.resources,
facilities = World.facilities, research = World.research, warehouse = World.warehouse, bossDefeated = World.bossDefeated };
Send(clientId, SnapshotMessage, JsonUtility.ToJson(new WorldPacket { full = full, world = snapshot, recipient = recipient }));
}
private void ApplyPacket(WorldPacket packet)
{
if (packet == null || packet.world == null || packet.recipient != PlayerId) return;
if (World != null && packet.world.tick < World.tick) return;
if (packet.full)
{ SaveStore.Validate(packet.world); Simulation = new GameSimulation(packet.world, Content); Revision++; }
else if (World != null)
{
World.tick = packet.world.tick; World.time = packet.world.time; World.players = packet.world.players;
World.vehicles = packet.world.vehicles; World.enemies = packet.world.enemies; World.shots = packet.world.shots;
World.resources = packet.world.resources; World.facilities = packet.world.facilities; World.research = packet.world.research;
World.warehouse = packet.world.warehouse; World.bossDefeated = packet.world.bossDefeated;
}
if (LocalPlayer == null) return;
commandSequence = Math.Max(commandSequence, LocalPlayer.lastCommand); SnapshotAge = 0;
unacknowledged.RemoveAll(i => i.sequence <= LocalPlayer.acknowledgedInput);
PredictedPlayer = JsonUtility.FromJson<PlayerState>(JsonUtility.ToJson(LocalPlayer));
var controlled = Simulation.StationVehicle(LocalPlayer.stationId);
bool driver = Simulation.FindStation(LocalPlayer.stationId)?.role == StationRole.Driver;
PredictedVehicle = driver && controlled != null ? JsonUtility.FromJson<VehicleState>(JsonUtility.ToJson(controlled)) : null;
foreach (var input in unacknowledged)
{
if (PredictedPlayer.stationId == "" && PredictedPlayer.transitionId == "") GameSimulation.StepPlayer(World, PredictedPlayer, input, GameSimulation.TickSeconds);
if (PredictedVehicle != null) Simulation.StepVehicle(PredictedVehicle, input, GameSimulation.TickSeconds);
}
Status = "合作成员 · 世界由房主保存";
}
private void Update()
{
if (!Running || LocalPlayer == null) return;
SnapshotAge += Time.unscaledDeltaTime; CaptureControls(); accumulator += Time.unscaledDeltaTime;
int steps = 0;
while (accumulator >= GameSimulation.TickSeconds && steps++ < 5)
{
accumulator -= GameSimulation.TickSeconds;
var input = new PlayerInput { sequence = ++inputSequence, move = controls.move, aim = controls.aim, fire = controls.fire,
secondary = controls.secondary, sprint = controls.sprint, brake = controls.brake };
if (IsAuthority) { Simulation.SetInput(PlayerId, input); Simulation.Tick(GameSimulation.TickSeconds); }
else
{
Send(NetworkManager.ServerClientId, InputMessage, JsonUtility.ToJson(input), NetworkDelivery.UnreliableSequenced);
unacknowledged.Add(input); if (unacknowledged.Count > 90) unacknowledged.RemoveAt(0);
if (PredictedPlayer != null && PredictedPlayer.stationId == "" && PredictedPlayer.transitionId == "") GameSimulation.StepPlayer(World, PredictedPlayer, input, GameSimulation.TickSeconds);
if (PredictedVehicle != null) Simulation.StepVehicle(PredictedVehicle, input, GameSimulation.TickSeconds);
}
}
if (steps >= 5) accumulator = Mathf.Min(accumulator, GameSimulation.TickSeconds);
if (!IsAuthority) return;
snapshotTimer += Time.unscaledDeltaTime; saveTimer += Time.unscaledDeltaTime; dirtyTimer += Time.unscaledDeltaTime;
if (network != null && network.IsServer && snapshotTimer >= .1f)
{
snapshotTimer = 0; bool full = topologyDirty; topologyDirty = false;
foreach (var client in clients.Keys.ToArray()) if (client != NetworkManager.ServerClientId) SendWorld(client, full);
}
if (saveTimer >= 300 || (dirty && dirtyTimer >= 2)) Save();
}
private void CaptureControls()
{
if (SmokeHarness.Enabled) { controls = SmokeHarness.InputFor(this); return; }
var keyboard = Keyboard.current; var mouse = Mouse.current; if (keyboard == null) return;
controls = new PlayerInput();
controls.aim = view != null ? view.MouseGround : LocalPlayer.location.position + new V2(0, 1);
if (hud != null && hud.BlocksMovement) return;
controls.move = new V2((keyboard.dKey.isPressed ? 1 : 0) - (keyboard.aKey.isPressed ? 1 : 0), (keyboard.wKey.isPressed ? 1 : 0) - (keyboard.sKey.isPressed ? 1 : 0));
controls.sprint = keyboard.leftShiftKey.isPressed; controls.brake = keyboard.spaceKey.isPressed;
controls.fire = mouse != null && mouse.leftButton.isPressed && (hud == null || !hud.CapturesPointer) && (hud == null || !hud.BuildMode);
controls.secondary = mouse != null && mouse.rightButton.isPressed && (hud == null || (!hud.CapturesPointer && !hud.BuildMode));
if (keyboard.eKey.wasPressedThisFrame && hud != null) hud.Interact();
if (keyboard.digit1Key.wasPressedThisFrame) Command(new GameCommand { kind = LocalPlayer.stationId == "" ? CommandKind.Ability : CommandKind.Cruise });
if (keyboard.digit2Key.wasPressedThisFrame && LocalPlayer.stationId != "") Command(new GameCommand { kind = CommandKind.ChangeAltitude });
}
public void Command(GameCommand command)
{
if (!Running || LocalPlayer == null) return;
command.sequence = ++commandSequence;
if (IsAuthority) { var result = Simulation.Execute(PlayerId, command); Toast(result.message, result.success); }
else Send(NetworkManager.ServerClientId, CommandMessage, JsonUtility.ToJson(command));
}
public void Save()
{
if (!IsAuthority || World == null) return;
try { saves.Save(World); saveTimer = 0; dirtyTimer = 0; dirty = false; }
catch (Exception ex) { Toast("保存失败:" + ex.Message, false); dirtyTimer = 0; }
}
public void Stop()
{
if (stopping) return; stopping = true;
if (IsAuthority && Simulation != null)
{ foreach (var player in World.players.Where(p => p.connected).ToArray()) Simulation.Disconnect(player.id); Save(); }
Running = false;
if (network != null && network.IsListening) network.Shutdown();
clients.Clear(); pending.Clear(); unacknowledged.Clear(); PredictedPlayer = null; PredictedVehicle = null; offline = false;
Simulation = null; Revision++; stopping = false;
}
public void Toast(string text, bool good) { Status = text; Message?.Invoke(text, good); }
private void OnApplicationQuit() { if (Running) Stop(); }
private void OnDestroy() { if (network != null) Destroy(network.gameObject); }
}
}