503 lines
17 KiB
C#
503 lines
17 KiB
C#
/*
|
||
* @Author: 白哉
|
||
* @Description:
|
||
* @Date: 2025年04月03日 星期四 11:04:31
|
||
* @Modify:
|
||
*/
|
||
|
||
|
||
using System;
|
||
using System.Buffers;
|
||
using System.Collections.Generic;
|
||
using System.Runtime.InteropServices;
|
||
using Logic.AI;
|
||
using Logic.CrashSight;
|
||
using MemoryPack;
|
||
using TH1_Logic.Core;
|
||
using TH1_Logic.Net;
|
||
using UnityEngine;
|
||
|
||
|
||
namespace RuntimeData
|
||
{
|
||
public enum NetMode
|
||
{
|
||
None,
|
||
Single,
|
||
Multi,
|
||
Spectator,
|
||
}
|
||
|
||
|
||
public enum MemberNetState
|
||
{
|
||
None,
|
||
OK,
|
||
Leaved,
|
||
Disconnected,
|
||
Timeout,
|
||
Error,
|
||
}
|
||
|
||
|
||
[MemoryPackable]
|
||
public partial class PlayerConfirmData
|
||
{
|
||
public ulong MemberId;
|
||
public MemberNetState State;
|
||
[MemoryPackIgnore]
|
||
public float PingRecordTime;
|
||
public float Ping;
|
||
[MemoryPackIgnore]
|
||
public float ErrorTime;
|
||
[MemoryPackIgnore]
|
||
public float ConfirmTime;
|
||
public bool AIControl;
|
||
|
||
|
||
[MemoryPackConstructor]
|
||
public PlayerConfirmData()
|
||
{
|
||
|
||
}
|
||
|
||
public PlayerConfirmData(ulong id)
|
||
{
|
||
MemberId = id;
|
||
ConfirmTime = Time.time;
|
||
State = MemberNetState.OK;
|
||
AIControl = false;
|
||
}
|
||
|
||
public void OnSendHeartbeat()
|
||
{
|
||
if (PingRecordTime == 0 || Time.time - PingRecordTime > 5f)
|
||
PingRecordTime = Time.time;
|
||
}
|
||
|
||
public void OnReceiveHeartbeat()
|
||
{
|
||
ConfirmTime = Time.time;
|
||
}
|
||
|
||
public void OnReceiveHeartbeatReply()
|
||
{
|
||
if (PingRecordTime == 0f) return;
|
||
Ping = Time.time - PingRecordTime;
|
||
PingRecordTime = 0f;
|
||
}
|
||
|
||
public float GetPing()
|
||
{
|
||
return Ping;
|
||
}
|
||
|
||
public void UpdateErrorTime()
|
||
{
|
||
if (State == MemberNetState.OK) ErrorTime = Time.time;
|
||
}
|
||
|
||
public bool IsNeedAI()
|
||
{
|
||
// 房主可手动指定 AI 接管
|
||
return AIControl && State != MemberNetState.OK;
|
||
}
|
||
}
|
||
|
||
|
||
[MemoryPackable]
|
||
public partial class PlayerConfirmMap
|
||
{
|
||
public Dictionary<ulong, PlayerConfirmData> PlayerConfirm;
|
||
|
||
|
||
public PlayerConfirmMap()
|
||
{
|
||
PlayerConfirm = new Dictionary<ulong, PlayerConfirmData>();
|
||
}
|
||
|
||
public PlayerConfirmData GetPlayerConfirm(ulong memberId)
|
||
{
|
||
if (!PlayerConfirm.ContainsKey(memberId))
|
||
PlayerConfirm[memberId] = new PlayerConfirmData(memberId);
|
||
return PlayerConfirm[memberId];
|
||
}
|
||
|
||
public void UpdatePlayerConfirm(Dictionary<ulong, PlayerConfirmData> playerConfirm)
|
||
{
|
||
foreach (var confirm in playerConfirm)
|
||
{
|
||
var data = GetPlayerConfirm(confirm.Key);
|
||
data.State = confirm.Value.State;
|
||
data.AIControl = confirm.Value.AIControl;
|
||
if (confirm.Key == LobbyManager.Instance.Lobby.GetSelfMemberId()) continue;
|
||
if (confirm.Key == LobbyManager.Instance.Lobby.GetLobbyOwnerId()) continue;
|
||
data.Ping = confirm.Value.Ping;
|
||
}
|
||
}
|
||
|
||
public void InitFromMapSlots(MapData mapData)
|
||
{
|
||
var lobby = LobbyManager.Instance.Lobby;
|
||
if (mapData?.MapConfig?.MultiCivs == null || lobby == null || !lobby.IsInLobby()) return;
|
||
foreach (var slot in mapData.MapConfig.MultiCivs)
|
||
{
|
||
if (slot == null || slot.MemberId == 0) continue;
|
||
var confirm = GetPlayerConfirm(slot.MemberId);
|
||
var inLobby = lobby.IsMemberInLobby(slot.MemberId);
|
||
confirm.State = inLobby ? MemberNetState.OK : MemberNetState.Leaved;
|
||
confirm.AIControl = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// 网络信息
|
||
[MemoryPackable]
|
||
public partial class NetData
|
||
{
|
||
// 网络模式
|
||
public NetMode Mode;
|
||
// 当前操作的玩家
|
||
public uint CurPlayerId;
|
||
// SteamId => PlayerId 的索引
|
||
public Dictionary<ulong, uint> Players;
|
||
// 所有玩家行为的序列
|
||
public List<ActionNetData> Actions;
|
||
|
||
// 随机数种子 开始游戏时由房主进行初始化,整局游戏不可变
|
||
public int RandomSeed;
|
||
// 同步随机源的消费次数,用于 ForceUpdate/重连后恢复随机游标
|
||
public int RandomUseCount;
|
||
[MemoryPackIgnore]
|
||
private System.Random _random;
|
||
private static readonly ArrayBufferWriter<byte> _bufferWriter = new ArrayBufferWriter<byte>(64 * 1024);
|
||
|
||
// 玩家开始时间记录
|
||
[MemoryPackIgnore]
|
||
public float PlayerStartTime;
|
||
[MemoryPackIgnore]
|
||
public float GameStartTime;
|
||
|
||
|
||
[MemoryPackConstructor]
|
||
public NetData()
|
||
{
|
||
Mode = NetMode.None;
|
||
CurPlayerId = 0;
|
||
Players = new Dictionary<ulong, uint>();
|
||
Actions = new List<ActionNetData>();
|
||
// 生成确定性种子(由房主生成并广播)
|
||
RandomSeed = System.Environment.TickCount;
|
||
RandomUseCount = 0;
|
||
GameStartTime = 0;
|
||
PlayerStartTime = 0;
|
||
}
|
||
|
||
// 这里单纯是因为深拷贝目前只用于 AI 演算,故不对 Players 和 Actions 赋值
|
||
public NetData(NetData copyData)
|
||
{
|
||
Mode = copyData.Mode;
|
||
CurPlayerId = copyData.CurPlayerId;
|
||
RandomSeed = copyData.RandomSeed;
|
||
RandomUseCount = copyData.RandomUseCount;
|
||
Players = new Dictionary<ulong, uint>();
|
||
Actions = new List<ActionNetData>();
|
||
}
|
||
|
||
// 这里单纯是因为深拷贝目前只用于 AI 演算,故不对 Players 和 Actions 赋值
|
||
public void DeepCopy(NetData copyData)
|
||
{
|
||
Mode = copyData.Mode;
|
||
CurPlayerId = copyData.CurPlayerId;
|
||
RandomSeed = copyData.RandomSeed;
|
||
RandomUseCount = copyData.RandomUseCount;
|
||
_random = null;
|
||
Players = new Dictionary<ulong, uint>();
|
||
Actions = new List<ActionNetData>();
|
||
}
|
||
|
||
// 只有 Action 的逻辑可以使用这个 Random, 因为要保证计数一致
|
||
public System.Random GetRandom(MapData map)
|
||
{
|
||
if (map != Main.MapData) return CreateRandom(RandomUseCount, null);
|
||
if (_random == null) _random = CreateRandom(RandomUseCount, () => RandomUseCount++);
|
||
return _random;
|
||
}
|
||
|
||
private System.Random CreateRandom(int useCount, System.Action onUse)
|
||
{
|
||
var random = new CountingRandom(RandomSeed, onUse);
|
||
random.Advance(useCount);
|
||
return random;
|
||
}
|
||
|
||
private sealed class CountingRandom : System.Random
|
||
{
|
||
private readonly System.Action _onUse;
|
||
private bool _advancing;
|
||
|
||
public CountingRandom(int seed, System.Action onUse) : base(seed)
|
||
{
|
||
_onUse = onUse;
|
||
}
|
||
|
||
public void Advance(int count)
|
||
{
|
||
if (count <= 0) return;
|
||
_advancing = true;
|
||
for (var i = 0; i < count; i++) base.Next();
|
||
_advancing = false;
|
||
}
|
||
|
||
private void CountUse()
|
||
{
|
||
if (!_advancing) _onUse?.Invoke();
|
||
}
|
||
|
||
public override int Next()
|
||
{
|
||
CountUse();
|
||
return base.Next();
|
||
}
|
||
|
||
public override int Next(int maxValue)
|
||
{
|
||
CountUse();
|
||
return base.Next(maxValue);
|
||
}
|
||
|
||
public override int Next(int minValue, int maxValue)
|
||
{
|
||
CountUse();
|
||
return base.Next(minValue, maxValue);
|
||
}
|
||
|
||
public override double NextDouble()
|
||
{
|
||
CountUse();
|
||
return base.NextDouble();
|
||
}
|
||
|
||
public override void NextBytes(byte[] buffer)
|
||
{
|
||
CountUse();
|
||
base.NextBytes(buffer);
|
||
}
|
||
}
|
||
|
||
public bool RefreshPlayerNet(MapData mapData)
|
||
{
|
||
if (Mode != NetMode.Multi) return true;
|
||
if (mapData?.PlayerMap?.PlayerDataList == null)
|
||
{
|
||
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.InvalidNetworkMapData);
|
||
return false;
|
||
}
|
||
|
||
var lobby = LobbyManager.Instance.Lobby;
|
||
if (lobby == null || !lobby.IsInLobby())
|
||
{
|
||
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.NetworkPlayerMappingFailed);
|
||
return false;
|
||
}
|
||
|
||
Players ??= new Dictionary<ulong, uint>();
|
||
var lobbyMemberIds = lobby.GetAllMemberIds();
|
||
if (lobbyMemberIds == null || lobbyMemberIds.Count == 0)
|
||
{
|
||
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyMembersNotSynced);
|
||
return false;
|
||
}
|
||
var lobbyMemberSet = new HashSet<ulong>(lobbyMemberIds);
|
||
if (lobby.IsLobbyOwner())
|
||
{
|
||
var staleMemberIds = new List<ulong>();
|
||
foreach (var kv in Players)
|
||
{
|
||
if (lobbyMemberSet.Contains(kv.Key)) continue;
|
||
staleMemberIds.Add(kv.Key);
|
||
}
|
||
|
||
foreach (var memberId in staleMemberIds)
|
||
{
|
||
Players.Remove(memberId);
|
||
LogSystem.LogWarning($"RefreshPlayerNet removed stale member mapping: {memberId}");
|
||
}
|
||
|
||
// 创建 Player 的时候会分配一次 PlayerId,这里直接使用
|
||
if (mapData.MapConfig?.MultiCivs != null)
|
||
{
|
||
foreach (var memberCiv in mapData.MapConfig.MultiCivs)
|
||
{
|
||
if (memberCiv == null || memberCiv.PlayerId == 0) continue;
|
||
if (!lobbyMemberSet.Contains(memberCiv.MemberId)) continue;
|
||
if (Players.TryGetValue(memberCiv.MemberId, out var mappedPlayerId))
|
||
{
|
||
if (mappedPlayerId != memberCiv.PlayerId) Players[memberCiv.MemberId] = memberCiv.PlayerId;
|
||
continue;
|
||
}
|
||
if (Players.ContainsValue(memberCiv.PlayerId)) continue;
|
||
Players[memberCiv.MemberId] = memberCiv.PlayerId;
|
||
}
|
||
}
|
||
|
||
// 添加其他人
|
||
foreach (var memberId in lobbyMemberIds)
|
||
{
|
||
if (Players.ContainsKey(memberId)) continue;
|
||
foreach (var player in mapData.PlayerMap.PlayerDataList)
|
||
{
|
||
if (player == null) continue;
|
||
if (Players.ContainsValue(player.Id)) continue;
|
||
Players[memberId] = player.Id;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
var selfMemberId = lobby.GetSelfMemberId();
|
||
foreach (var memberId in lobbyMemberIds)
|
||
{
|
||
if (!Players.TryGetValue(memberId, out var playerId)
|
||
|| playerId == 0
|
||
|| !mapData.PlayerMap.GetPlayerDataByPlayerID(playerId, out _))
|
||
{
|
||
LogSystem.LogWarning($"RefreshPlayerNet missing PlayerId mapping for lobby member: member={memberId}, player={playerId}");
|
||
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.NetworkPlayerMappingFailed);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
var seenPlayerIds = new HashSet<uint>();
|
||
foreach (var kv in Players)
|
||
{
|
||
if (!lobby.IsMemberInLobby(kv.Key)) continue;
|
||
if (kv.Value == 0 || !mapData.PlayerMap.GetPlayerDataByPlayerID(kv.Value, out _))
|
||
{
|
||
LogSystem.LogWarning($"RefreshPlayerNet invalid PlayerId mapping: member={kv.Key}, player={kv.Value}");
|
||
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.NetworkPlayerMappingFailed);
|
||
return false;
|
||
}
|
||
|
||
if (!seenPlayerIds.Add(kv.Value))
|
||
{
|
||
LogSystem.LogWarning($"RefreshPlayerNet duplicate PlayerId mapping: player={kv.Value}");
|
||
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.NetworkPlayerMappingFailed);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (Players.TryGetValue(selfMemberId, out var id)
|
||
&& id != 0
|
||
&& mapData.PlayerMap.GetPlayerDataByPlayerID(id, out _))
|
||
{
|
||
mapData.PlayerMap.SelfPlayerId = id;
|
||
return true;
|
||
}
|
||
else
|
||
{
|
||
LogSystem.LogWarning($"RefreshPlayerNet could not find PlayerId for self member: {selfMemberId}");
|
||
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.NetworkPlayerMappingFailed);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public uint GetActionVersion()
|
||
{
|
||
if (Actions.Count == 0) return 0;
|
||
return Actions[^1].Version + 1;
|
||
}
|
||
|
||
public ulong GetmemberIdId(uint playerId)
|
||
{
|
||
foreach (var kv in Players)
|
||
{
|
||
if (kv.Value == playerId) return kv.Key;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
public uint GetPlayerId(ulong memberId)
|
||
{
|
||
return Players.GetValueOrDefault(memberId);
|
||
}
|
||
|
||
public void CompareEqual(NetData net)
|
||
{
|
||
var differences = new List<string>();
|
||
|
||
// 使用序列化比较各个组件
|
||
MapData.CompareComponent(Mode, net.Mode, "Mode", differences);
|
||
MapData.CompareComponent(CurPlayerId, net.CurPlayerId, "CurPlayerId", differences);
|
||
MapData.CompareComponent(RandomUseCount, net.RandomUseCount, "RandomUseCount", differences);
|
||
MapData.CompareComponent(Players, net.Players, "Players", differences);
|
||
MapData.CompareComponent(Actions, net.Actions, "Actions", differences);
|
||
|
||
// 输出结果
|
||
foreach (var diff in differences) LogSystem.LogWarning($" - {diff}");
|
||
}
|
||
|
||
public void Clear()
|
||
{
|
||
Mode = NetMode.None;
|
||
CurPlayerId = 0;
|
||
Players ??= new Dictionary<ulong, uint>();
|
||
Players.Clear();
|
||
Actions ??= new List<ActionNetData>();
|
||
Actions.Clear();
|
||
// 生成确定性种子(由房主生成并广播)
|
||
RandomSeed = System.Environment.TickCount;
|
||
RandomUseCount = 0;
|
||
_random = null;
|
||
GameStartTime = 0;
|
||
PlayerStartTime = 0;
|
||
}
|
||
|
||
// public static string GetMapDataHash(MapData mapData)
|
||
// {
|
||
// var actions = mapData.Net.Actions;
|
||
// mapData.Net.Actions = null;
|
||
//
|
||
// _bufferWriter.Clear();
|
||
// MemoryPackSerializer.Serialize(_bufferWriter, mapData);
|
||
//
|
||
// mapData.Net.Actions = actions;
|
||
//
|
||
// var hash128 = new Hash128();
|
||
// hash128.Append(_bufferWriter.WrittenSpan.ToArray()); // 这里仍有 GC
|
||
// return hash128.ToString();
|
||
// }
|
||
|
||
public static string GetMapDataHash(MapData mapData)
|
||
{
|
||
lock (_bufferWriter)
|
||
{
|
||
var actions = mapData.Net.Actions;
|
||
mapData.Net.Actions = null;
|
||
try
|
||
{
|
||
_bufferWriter.Clear();
|
||
MemoryPackSerializer.Serialize(_bufferWriter, mapData);
|
||
var hash128 = new Hash128();
|
||
var writtenMemory = _bufferWriter.WrittenMemory;
|
||
if (MemoryMarshal.TryGetArray(writtenMemory, out ArraySegment<byte> segment) && segment.Array != null)
|
||
{
|
||
hash128.Append(segment.Array, segment.Offset, segment.Count);
|
||
}
|
||
else
|
||
{
|
||
hash128.Append(writtenMemory.ToArray());
|
||
}
|
||
|
||
return hash128.ToString();
|
||
}
|
||
finally
|
||
{
|
||
mapData.Net.Actions = actions;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|