XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

AshfallFrontier

【Unity】灰烬边境,一款生存策略游戏

公开
关注 0 Fork 0 Star 0
UTF-8
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); }
    }
}