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 clients = new Dictionary(); private readonly Dictionary pending = new Dictionary(); private readonly List unacknowledged = new List(); private GameHud hud; private WorldView view; public event Action Message; private void Awake() { Content = Resources.Load("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(); view = GetComponent(); 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(); // 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(); 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(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(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(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(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(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(JsonUtility.ToJson(LocalPlayer)); var controlled = Simulation.StationVehicle(LocalPlayer.stationId); bool driver = Simulation.FindStation(LocalPlayer.stationId)?.role == StationRole.Driver; PredictedVehicle = driver && controlled != null ? JsonUtility.FromJson(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); } } }