3382 lines
129 KiB
C#
3382 lines
129 KiB
C#
/*
|
||
* @Author: 白哉
|
||
* @Description:
|
||
* @Date: 2025年04月03日 星期四 11:04:31
|
||
* @Modify:
|
||
*/
|
||
|
||
|
||
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Reflection;
|
||
using Logic;
|
||
using Logic.Action;
|
||
using Logic.AI;
|
||
using Logic.CrashSight;
|
||
using Logic.Pool;
|
||
using Logic.Skill;
|
||
using MemoryPack;
|
||
using TH1_Core.Events;
|
||
using TH1_Core.Managers;
|
||
using TH1_Logic.Collect;
|
||
using TH1_Logic.Config;
|
||
using TH1_Logic.Core;
|
||
using TH1_Logic.MatchConfig;
|
||
using TH1_Logic.Net;
|
||
using TH1_Logic.Steam;
|
||
using TH1_Logic.Tools;
|
||
using TH1Renderer;
|
||
using TH1_Renderer;
|
||
using UnityEngine;
|
||
using MemberInfo = TH1_Logic.Net.MemberInfo;
|
||
|
||
|
||
namespace RuntimeData
|
||
{
|
||
public enum GameMode
|
||
{
|
||
DOMINATION,
|
||
PERFECT,
|
||
CREATIVE
|
||
}
|
||
|
||
|
||
// 一场游戏的设置数据
|
||
[MemoryPackable]
|
||
public partial class MapConfig
|
||
{
|
||
public uint Id;
|
||
public uint Width;
|
||
public uint Height;
|
||
public uint PlayerCount;
|
||
public AIDifficult AIDiff;
|
||
public GameMode GameMode;
|
||
|
||
// 指定地图配置
|
||
public bool IsCustomMap;
|
||
public string MapName;
|
||
|
||
// 游戏结算配置
|
||
public MatchSettlementType MatchSettlement;
|
||
public List<PlayerSettlementInfo> PlayerSettlements;
|
||
|
||
// 旧存档兼容字段。新逻辑统一从 MultiCivs 读取单机/联机玩家槽位。
|
||
public uint selfCivId;
|
||
public uint selfForceId;
|
||
|
||
// 玩家位置槽位。旧名保留用于兼容现有序列化/联机消息。
|
||
public List<MemberCiv> MultiCivs;
|
||
private Dictionary<ulong, MemberCiv> _memberCivs;
|
||
|
||
// 校验用
|
||
[MemoryPackIgnore]
|
||
private string _hash;
|
||
|
||
// 关卡限制数据
|
||
public List<MatchLimitType> MatchLimits;
|
||
|
||
// 超时时间
|
||
public bool IsLimitTime = false;
|
||
public int TimeLimitSeconds = 180;
|
||
|
||
// 水域类型,默认为 Pangea
|
||
public Logic.MapWaterType WaterType = Logic.MapWaterType.Pangea;
|
||
public bool IsResumeArchiveSelected;
|
||
|
||
public const int NoTeamId = 0;
|
||
|
||
[MemoryPackConstructor]
|
||
public MapConfig()
|
||
{
|
||
MultiCivs = new List<MemberCiv>();
|
||
PlayerSettlements = new List<PlayerSettlementInfo>();
|
||
_memberCivs = new Dictionary<ulong, MemberCiv>();
|
||
MatchLimits = new List<MatchLimitType>();
|
||
}
|
||
|
||
public MapConfig(uint width, uint height, uint playerCount,uint civId, uint forceId, AIDifficult aiDiff = AIDifficult.LUNATIC)
|
||
{
|
||
Width = width;
|
||
Height = height;
|
||
PlayerCount = playerCount;
|
||
AIDiff = aiDiff;
|
||
MultiCivs = new List<MemberCiv>();
|
||
_memberCivs = new Dictionary<ulong, MemberCiv>();
|
||
PlayerSettlements = new List<PlayerSettlementInfo>();
|
||
MatchLimits = new List<MatchLimitType>();
|
||
SetSinglePlayerCiv(civId, forceId);
|
||
}
|
||
|
||
// MemoryPack 反序列化之后的后处理
|
||
[MemoryPackOnDeserialized]
|
||
public void OnAfterMemoryPackDeserialize()
|
||
{
|
||
MatchSettlement = MatchSettlementInfo.NormalizeType(MatchSettlement);
|
||
if (TimeLimitSeconds == 0) TimeLimitSeconds = 180;
|
||
// 旧存档兼容:WaterType 字段不存在时默认为 Pangea
|
||
if (!System.Enum.IsDefined(typeof(Logic.MapWaterType), WaterType))
|
||
WaterType = Logic.MapWaterType.Pangea;
|
||
MultiCivs ??= new List<MemberCiv>();
|
||
PlayerSettlements ??= new List<PlayerSettlementInfo>();
|
||
MatchLimits ??= new List<MatchLimitType>();
|
||
EnsurePlayerSlots(NetMode.Multi);
|
||
RefreshMultiCivsDict();
|
||
}
|
||
|
||
public void EnsurePlayerSlots(NetMode netMode = NetMode.Multi)
|
||
{
|
||
MultiCivs ??= new List<MemberCiv>();
|
||
var targetCount = GetRequiredPlayerSlotCount((int)PlayerCount, netMode);
|
||
PlayerCount = (uint)targetCount;
|
||
|
||
for (int i = 0; i < MultiCivs.Count; i++)
|
||
{
|
||
MultiCivs[i] ??= CreateDefaultPlayerSlot(i, netMode);
|
||
}
|
||
|
||
while (MultiCivs.Count < targetCount)
|
||
{
|
||
MultiCivs.Add(CreateDefaultPlayerSlot(MultiCivs.Count, netMode));
|
||
}
|
||
|
||
if (MultiCivs.Count > targetCount)
|
||
{
|
||
TrimRemovablePlayerSlots(targetCount);
|
||
if (MultiCivs.Count > targetCount)
|
||
{
|
||
targetCount = MultiCivs.Count;
|
||
PlayerCount = (uint)targetCount;
|
||
LogSystem.LogWarning($"玩家槽位中真人/保留槽位数量超过目标人数,保留现有槽位: playerCount={PlayerCount}");
|
||
}
|
||
}
|
||
|
||
for (int i = 0; i < MultiCivs.Count; i++)
|
||
{
|
||
NormalizePlayerSlot(MultiCivs[i], i);
|
||
}
|
||
|
||
if (netMode == NetMode.Single) ApplySinglePlayerSlotDefaults();
|
||
RefreshMultiCivsDict();
|
||
}
|
||
|
||
public bool SetPlayerCount(uint playerCount, NetMode netMode = NetMode.Multi)
|
||
{
|
||
var oldPlayerCount = PlayerCount;
|
||
var oldSlotCount = MultiCivs?.Count ?? 0;
|
||
PlayerCount = playerCount;
|
||
EnsurePlayerSlots(netMode);
|
||
return oldPlayerCount != PlayerCount || oldSlotCount != (MultiCivs?.Count ?? 0);
|
||
}
|
||
|
||
private int GetRequiredPlayerSlotCount(int targetCount, NetMode netMode)
|
||
{
|
||
targetCount = Math.Max(0, targetCount);
|
||
var lobby = LobbyManager.Instance?.Lobby;
|
||
if (netMode != NetMode.Multi || lobby == null || !lobby.IsInLobby()) return targetCount;
|
||
|
||
var memberIds = lobby.GetAllMemberIds();
|
||
return Math.Max(targetCount, memberIds?.Count ?? 0);
|
||
}
|
||
|
||
private void TrimRemovablePlayerSlots(int targetCount)
|
||
{
|
||
for (int i = MultiCivs.Count - 1; i >= 0 && MultiCivs.Count > targetCount; i--)
|
||
{
|
||
if (!CanRemovePlayerSlot(MultiCivs[i])) continue;
|
||
MultiCivs.RemoveAt(i);
|
||
}
|
||
}
|
||
|
||
private static bool CanRemovePlayerSlot(MemberCiv slot)
|
||
{
|
||
return slot == null || slot.MemberId == 0 && slot.IsAI;
|
||
}
|
||
|
||
private MemberCiv CreateDefaultPlayerSlot(int index, NetMode netMode)
|
||
{
|
||
var slot = new MemberCiv
|
||
{
|
||
Index = index,
|
||
MemberId = 0,
|
||
PlayerId = 0,
|
||
CivId = 0,
|
||
ForceId = 0,
|
||
TeamId = NoTeamId,
|
||
IsAI = true,
|
||
IsReady = false,
|
||
IsCivFixed = false
|
||
};
|
||
|
||
return slot;
|
||
}
|
||
|
||
private void ApplySinglePlayerSlotDefaults()
|
||
{
|
||
if (MultiCivs.Count == 0) return;
|
||
|
||
var selfSlot = MultiCivs[0];
|
||
PrepareSinglePlayerSlot(selfSlot, index: 0, isAI: false);
|
||
selfSlot.IsCivFixed = true;
|
||
|
||
var usedCivs = new HashSet<uint> { selfSlot.CivId };
|
||
for (int i = 1; i < MultiCivs.Count; i++)
|
||
{
|
||
var slot = MultiCivs[i];
|
||
PrepareSinglePlayerSlot(slot, i, isAI: true);
|
||
if (!slot.IsCivFixed || usedCivs.Contains(slot.CivId))
|
||
{
|
||
var civId = PickDefaultCivId(i, usedCivs);
|
||
slot.CivId = civId;
|
||
slot.ForceId = civId;
|
||
slot.IsCivFixed = true;
|
||
}
|
||
usedCivs.Add(slot.CivId);
|
||
}
|
||
}
|
||
|
||
private static void PrepareSinglePlayerSlot(MemberCiv slot, int index, bool isAI)
|
||
{
|
||
slot.Index = index;
|
||
slot.MemberId = 0;
|
||
slot.IsReady = false;
|
||
slot.PlayerId = 0;
|
||
slot.TeamId = NoTeamId;
|
||
slot.IsAI = isAI;
|
||
}
|
||
|
||
private static uint PickDefaultCivId(int preferredIndex, HashSet<uint> usedCivs)
|
||
{
|
||
const int defaultCivCount = 17;
|
||
for (int offset = 0; offset < defaultCivCount; offset++)
|
||
{
|
||
var civId = (uint)((preferredIndex + offset) % defaultCivCount);
|
||
if (!usedCivs.Contains(civId)) return civId;
|
||
}
|
||
|
||
return (uint)preferredIndex;
|
||
}
|
||
|
||
private void NormalizePlayerSlot(MemberCiv slot, int index)
|
||
{
|
||
slot.Index = index;
|
||
if (slot.TeamId < NoTeamId) slot.TeamId = NoTeamId;
|
||
if (slot.MemberId != 0) slot.IsAI = false;
|
||
if (slot.MemberId == 0) slot.PlayerId = 0;
|
||
}
|
||
|
||
// 根据房间成员信息更新 mapconfig 信息
|
||
public bool UpdateLobbyMember(Dictionary<ulong, MemberInfo> memberInfos)
|
||
{
|
||
if (memberInfos == null) return false;
|
||
EnsurePlayerSlots(NetMode.Multi);
|
||
var changed = false;
|
||
// 先解绑已离开 lobby 的成员,避免槽位留下幽灵真人。
|
||
foreach (var slot in MultiCivs)
|
||
{
|
||
if (slot == null || slot.MemberId == 0 || memberInfos.ContainsKey(slot.MemberId)) continue;
|
||
ClearPlayerSlotMember(slot, makeAi: true, clearCiv: true);
|
||
changed = true;
|
||
}
|
||
|
||
if (changed) EnsurePlayerSlots(NetMode.Multi);
|
||
RefreshMultiCivsDict();
|
||
foreach (var kv in memberInfos)
|
||
{
|
||
if (_memberCivs.ContainsKey(kv.Key)) continue;
|
||
var slot = GetFirstAssignableSlot();
|
||
if (slot == null)
|
||
{
|
||
LogSystem.LogError($"房间人数超过玩家槽位数量: member={kv.Key}, playerCount={PlayerCount}");
|
||
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyMembersNotSynced);
|
||
continue;
|
||
}
|
||
|
||
slot.MemberId = kv.Key;
|
||
slot.PlayerId = 0;
|
||
slot.IsAI = false;
|
||
slot.IsReady = LobbyManager.Instance.Lobby.IsInLobby()
|
||
&& kv.Key == LobbyManager.Instance.Lobby.GetLobbyOwnerId();
|
||
changed = true;
|
||
}
|
||
changed |= EnsureLobbyOwnerReady();
|
||
RefreshMultiCivsDict();
|
||
return changed;
|
||
}
|
||
|
||
private MemberCiv GetFirstAssignableSlot()
|
||
{
|
||
foreach (var slot in MultiCivs)
|
||
{
|
||
if (slot == null || slot.MemberId != 0) continue;
|
||
return slot;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static void ClearPlayerSlotMember(MemberCiv slot, bool makeAi, bool clearCiv = false)
|
||
{
|
||
slot.MemberId = 0;
|
||
slot.PlayerId = 0;
|
||
slot.IsReady = false;
|
||
slot.IsAI = makeAi;
|
||
if (!clearCiv) return;
|
||
slot.CivId = 0;
|
||
slot.ForceId = 0;
|
||
slot.IsCivFixed = false;
|
||
}
|
||
|
||
private bool EnsureLobbyOwnerReady()
|
||
{
|
||
if (!LobbyManager.Instance.Lobby.IsInLobby()) return false;
|
||
var ownerId = LobbyManager.Instance.Lobby.GetLobbyOwnerId();
|
||
if (ownerId == 0) return false;
|
||
|
||
var changed = false;
|
||
foreach (var memberCiv in MultiCivs)
|
||
{
|
||
if (memberCiv == null || memberCiv.MemberId != ownerId || memberCiv.IsReady) continue;
|
||
memberCiv.IsReady = true;
|
||
changed = true;
|
||
}
|
||
|
||
return changed;
|
||
}
|
||
|
||
// 内部刷新
|
||
private void RefreshMultiCivsDict()
|
||
{
|
||
_memberCivs ??= new Dictionary<ulong, MemberCiv>();
|
||
MultiCivs ??= new List<MemberCiv>();
|
||
_memberCivs.Clear();
|
||
foreach (var memberCiv in MultiCivs)
|
||
{
|
||
if (memberCiv == null) continue;
|
||
if (memberCiv.MemberId == 0) continue;
|
||
_memberCivs[memberCiv.MemberId] = memberCiv;
|
||
}
|
||
}
|
||
|
||
public MemberCiv GetMemberCiv(ulong memberId)
|
||
{
|
||
MultiCivs ??= new List<MemberCiv>();
|
||
foreach (var memberCiv in MultiCivs)
|
||
{
|
||
if (memberCiv == null) continue;
|
||
if (memberCiv.MemberId == memberId) return memberCiv;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
public MemberCiv GetPlayerSlot(int index, NetMode netMode = NetMode.Multi)
|
||
{
|
||
EnsurePlayerSlots(netMode);
|
||
if (index < 0 || index >= MultiCivs.Count) return null;
|
||
return MultiCivs[index];
|
||
}
|
||
|
||
public bool SetPlayerSlotTeam(int index, int teamId, NetMode netMode = NetMode.Multi)
|
||
{
|
||
var slot = GetPlayerSlot(index, netMode);
|
||
if (slot == null) return false;
|
||
teamId = Math.Max(NoTeamId, teamId);
|
||
if (slot.TeamId == teamId) return false;
|
||
slot.TeamId = teamId;
|
||
return true;
|
||
}
|
||
|
||
public bool SetPlayerSlotAI(int index, bool isAI, NetMode netMode = NetMode.Multi)
|
||
{
|
||
var slot = GetPlayerSlot(index, netMode);
|
||
if (slot == null) return false;
|
||
if (isAI)
|
||
{
|
||
if (slot.MemberId != 0) return false;
|
||
var changed = !slot.IsAI;
|
||
ClearPlayerSlotMember(slot, makeAi: true);
|
||
return changed;
|
||
}
|
||
|
||
if (!slot.IsAI) return false;
|
||
slot.IsAI = false;
|
||
return true;
|
||
}
|
||
|
||
public bool SetPlayerSlotCiv(int index, uint civId, uint forceId, NetMode netMode = NetMode.Multi)
|
||
{
|
||
var slot = GetPlayerSlot(index, netMode);
|
||
if (slot == null) return false;
|
||
if (slot.CivId == civId && slot.ForceId == forceId && slot.IsCivFixed) return false;
|
||
slot.CivId = civId;
|
||
slot.ForceId = forceId;
|
||
slot.IsCivFixed = true;
|
||
return true;
|
||
}
|
||
|
||
public bool SetSinglePlayerSlotCiv(int index, uint civId, uint forceId)
|
||
{
|
||
var slot = GetPlayerSlot(index, NetMode.Single);
|
||
if (slot == null) return false;
|
||
|
||
var isAI = index != 0;
|
||
var changed = slot.CivId != civId
|
||
|| slot.ForceId != forceId
|
||
|| slot.IsAI != isAI
|
||
|| !slot.IsCivFixed
|
||
|| slot.MemberId != 0;
|
||
|
||
PrepareSinglePlayerSlot(slot, index, isAI);
|
||
slot.CivId = civId;
|
||
slot.ForceId = forceId;
|
||
slot.IsCivFixed = true;
|
||
RefreshMultiCivsDict();
|
||
return changed;
|
||
}
|
||
|
||
public bool SetSinglePlayerCiv(uint civId, uint forceId)
|
||
{
|
||
var changed = SetSinglePlayerSlotCiv(0, civId, forceId);
|
||
ApplySinglePlayerSlotDefaults();
|
||
RefreshMultiCivsDict();
|
||
return changed;
|
||
}
|
||
|
||
public bool SetMemberSlot(ulong memberId, int index)
|
||
{
|
||
if (memberId == 0) return false;
|
||
EnsurePlayerSlots(NetMode.Multi);
|
||
if (index < 0 || index >= MultiCivs.Count) return false;
|
||
|
||
var current = GetMemberCiv(memberId);
|
||
var target = MultiCivs[index];
|
||
if (target.MemberId != 0 && target.MemberId != memberId) return false;
|
||
if (current == target) return false;
|
||
|
||
if (current != null) ClearPlayerSlotMember(current, makeAi: true, clearCiv: true);
|
||
target.MemberId = memberId;
|
||
target.PlayerId = 0;
|
||
target.IsAI = false;
|
||
target.IsReady = false;
|
||
EnsureLobbyOwnerReady();
|
||
RefreshMultiCivsDict();
|
||
return true;
|
||
}
|
||
|
||
public bool TryGetTeamIdByPlayerId(uint playerId, out int teamId)
|
||
{
|
||
teamId = NoTeamId;
|
||
if (playerId == 0 || MultiCivs == null) return false;
|
||
foreach (var slot in MultiCivs)
|
||
{
|
||
if (slot == null || slot.PlayerId != playerId) continue;
|
||
teamId = slot.TeamId;
|
||
return teamId != NoTeamId;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public bool ArePlayersInSameTeam(uint playerIdA, uint playerIdB)
|
||
{
|
||
if (playerIdA == 0 || playerIdB == 0 || playerIdA == playerIdB) return false;
|
||
return TryGetTeamIdByPlayerId(playerIdA, out var teamA)
|
||
&& TryGetTeamIdByPlayerId(playerIdB, out var teamB)
|
||
&& teamA != NoTeamId
|
||
&& teamA == teamB;
|
||
}
|
||
|
||
public void ApplyTeamDiplomacy(MapData mapData)
|
||
{
|
||
if (mapData?.PlayerMap?.PlayerDataList == null) return;
|
||
EnsurePlayerSlots(mapData.Net?.Mode ?? NetMode.Multi);
|
||
BindSlotPlayerIdsByIndex(mapData);
|
||
|
||
foreach (var player in mapData.PlayerMap.PlayerDataList)
|
||
{
|
||
if (player == null) continue;
|
||
foreach (var target in mapData.PlayerMap.PlayerDataList)
|
||
{
|
||
if (target == null || player.Id == target.Id) continue;
|
||
if (!ArePlayersInSameTeam(player.Id, target.Id)) continue;
|
||
if (player.GetCountryDiplomacyInfo(target.Id, out var info))
|
||
{
|
||
ApplyTeammateDiplomacy(info);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private void BindSlotPlayerIdsByIndex(MapData mapData)
|
||
{
|
||
if (mapData?.PlayerMap?.PlayerDataList == null || MultiCivs == null) return;
|
||
for (int i = 0; i < MultiCivs.Count; i++)
|
||
{
|
||
if (i >= mapData.PlayerMap.PlayerDataList.Count) break;
|
||
if (MultiCivs[i] == null) continue;
|
||
MultiCivs[i].PlayerId = mapData.PlayerMap.PlayerDataList[i].Id;
|
||
}
|
||
}
|
||
|
||
private static void ApplyTeammateDiplomacy(CountryDiplomacyInfo info)
|
||
{
|
||
if (info == null) return;
|
||
info.IsTeammate = true;
|
||
info.DiplomacyState = DiplomacyState.League;
|
||
info.IsLeagueRequest = false;
|
||
info.IsLeagueRupture = false;
|
||
}
|
||
|
||
// 主从端一致的更新某一个成员信息
|
||
public bool UpdateMemberCiv(MemberCiv civ)
|
||
{
|
||
if (civ == null) return false;
|
||
var lobby = LobbyManager.Instance.Lobby;
|
||
if (lobby.IsInLobby() && civ.MemberId == lobby.GetLobbyOwnerId())
|
||
civ.IsReady = true;
|
||
|
||
if (LobbyManager.Instance.Lobby.IsInLobby() && !LobbyManager.Instance.Lobby.IsLobbyOwner())
|
||
{
|
||
var selfMemberId = LobbyManager.Instance.Lobby.GetSelfMemberId();
|
||
if (civ.MemberId != selfMemberId)
|
||
{
|
||
LogSystem.LogError($"客户端只能修改自己的阵营: self={selfMemberId}, target={civ.MemberId}");
|
||
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyMemberConfigFailed);
|
||
return false;
|
||
}
|
||
|
||
if (!GameNetSender.Instance.ChangeCiv(civ))
|
||
{
|
||
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyMemberConfigFailed);
|
||
return false;
|
||
}
|
||
ApplyMemberCivLocal(civ);
|
||
return true;
|
||
}
|
||
|
||
if (LobbyManager.Instance.Lobby.IsInLobby() && !LobbyManager.Instance.Lobby.IsMemberInLobby(civ.MemberId))
|
||
{
|
||
LogSystem.LogError($"不能修改不在房间内的成员阵营: target={civ.MemberId}");
|
||
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyMemberConfigFailed);
|
||
return false;
|
||
}
|
||
|
||
return ApplyMemberCivLocal(civ);
|
||
}
|
||
|
||
public bool UpdateSelfMemberSlot(int index)
|
||
{
|
||
if (!LobbyManager.Instance.Lobby.IsInLobby()) return false;
|
||
var selfMemberId = LobbyManager.Instance.Lobby.GetSelfMemberId();
|
||
var memberCiv = GetMemberCiv(selfMemberId);
|
||
if (memberCiv == null) return false;
|
||
var targetSlot = GetPlayerSlot(index, NetMode.Multi);
|
||
if (targetSlot == null) return false;
|
||
if (targetSlot.MemberId != 0 && targetSlot.MemberId != selfMemberId) return false;
|
||
|
||
var next = CopyMemberCiv(memberCiv);
|
||
next.Index = index;
|
||
next.IsReady = false;
|
||
return UpdateMemberCiv(next);
|
||
}
|
||
|
||
public bool UpdateSelfMemberTeam(int teamId)
|
||
{
|
||
if (!LobbyManager.Instance.Lobby.IsInLobby()) return false;
|
||
var memberCiv = GetMemberCiv(LobbyManager.Instance.Lobby.GetSelfMemberId());
|
||
if (memberCiv == null) return false;
|
||
|
||
var next = CopyMemberCiv(memberCiv);
|
||
next.TeamId = Math.Max(NoTeamId, teamId);
|
||
next.IsReady = false;
|
||
return UpdateMemberCiv(next);
|
||
}
|
||
|
||
public bool UpdateSelfMemberCiv(uint civId, uint forceId)
|
||
{
|
||
if (!LobbyManager.Instance.Lobby.IsInLobby()) return false;
|
||
var memberCiv = GetMemberCiv(LobbyManager.Instance.Lobby.GetSelfMemberId());
|
||
if (memberCiv == null) return false;
|
||
|
||
var next = CopyMemberCiv(memberCiv);
|
||
next.CivId = civId;
|
||
next.ForceId = forceId;
|
||
next.IsCivFixed = true;
|
||
next.IsReady = false;
|
||
return UpdateMemberCiv(next);
|
||
}
|
||
|
||
public bool UpdateSelfMemberConfig(int index, uint civId, uint forceId, int teamId)
|
||
{
|
||
if (!LobbyManager.Instance.Lobby.IsInLobby()) return false;
|
||
var selfMemberId = LobbyManager.Instance.Lobby.GetSelfMemberId();
|
||
var memberCiv = GetMemberCiv(selfMemberId);
|
||
if (memberCiv == null) return false;
|
||
var targetSlot = GetPlayerSlot(index, NetMode.Multi);
|
||
if (targetSlot == null) return false;
|
||
if (targetSlot.MemberId != 0 && targetSlot.MemberId != selfMemberId) return false;
|
||
|
||
var next = CopyMemberCiv(memberCiv);
|
||
next.Index = index;
|
||
next.CivId = civId;
|
||
next.ForceId = forceId;
|
||
next.TeamId = Math.Max(NoTeamId, teamId);
|
||
next.IsCivFixed = true;
|
||
next.IsReady = false;
|
||
return UpdateMemberCiv(next);
|
||
}
|
||
|
||
private static MemberCiv CopyMemberCiv(MemberCiv memberCiv)
|
||
{
|
||
return new MemberCiv
|
||
{
|
||
Index = memberCiv.Index,
|
||
MemberId = memberCiv.MemberId,
|
||
PlayerId = memberCiv.PlayerId,
|
||
CivId = memberCiv.CivId,
|
||
ForceId = memberCiv.ForceId,
|
||
TeamId = memberCiv.TeamId,
|
||
IsAI = memberCiv.IsAI,
|
||
IsCivFixed = memberCiv.IsCivFixed,
|
||
IsReady = memberCiv.IsReady
|
||
};
|
||
}
|
||
|
||
public bool UpdateMemberReady(ulong memberId, bool isReady)
|
||
{
|
||
var memberCiv = GetMemberCiv(memberId);
|
||
if (memberCiv == null) return false;
|
||
|
||
var lobby = LobbyManager.Instance.Lobby;
|
||
var ownerId = lobby.IsInLobby() ? lobby.GetLobbyOwnerId() : 0;
|
||
var next = new MemberCiv
|
||
{
|
||
Index = memberCiv.Index,
|
||
MemberId = memberCiv.MemberId,
|
||
PlayerId = memberCiv.PlayerId,
|
||
CivId = memberCiv.CivId,
|
||
ForceId = memberCiv.ForceId,
|
||
TeamId = memberCiv.TeamId,
|
||
IsAI = memberCiv.IsAI,
|
||
IsCivFixed = memberCiv.IsCivFixed,
|
||
IsReady = memberId == ownerId || isReady
|
||
};
|
||
return UpdateMemberCiv(next);
|
||
}
|
||
|
||
public bool SetSelfReady(bool isReady)
|
||
{
|
||
if (!LobbyManager.Instance.Lobby.IsInLobby()) return false;
|
||
return UpdateMemberReady(LobbyManager.Instance.Lobby.GetSelfMemberId(), isReady);
|
||
}
|
||
|
||
public bool ToggleSelfReady()
|
||
{
|
||
if (!LobbyManager.Instance.Lobby.IsInLobby()) return false;
|
||
var selfMemberId = LobbyManager.Instance.Lobby.GetSelfMemberId();
|
||
return UpdateMemberReady(selfMemberId, !IsMemberReady(selfMemberId));
|
||
}
|
||
|
||
public bool IsMemberReady(ulong memberId)
|
||
{
|
||
if (LobbyManager.Instance.Lobby.IsInLobby()
|
||
&& memberId == LobbyManager.Instance.Lobby.GetLobbyOwnerId())
|
||
{
|
||
return true;
|
||
}
|
||
|
||
return GetMemberCiv(memberId)?.IsReady ?? false;
|
||
}
|
||
|
||
public bool AreAllLobbyMembersReady()
|
||
{
|
||
if (!LobbyManager.Instance.Lobby.IsInLobby()) return false;
|
||
var memberInfos = LobbyManager.Instance.Lobby.GetAllMemberInfo();
|
||
if (!HasSameLobbyMembers(memberInfos)) return false;
|
||
foreach (var slot in MultiCivs)
|
||
{
|
||
if (slot == null) return false;
|
||
if (slot.MemberId == 0 && !slot.IsAI) return false;
|
||
}
|
||
|
||
var ownerId = LobbyManager.Instance.Lobby.GetLobbyOwnerId();
|
||
foreach (var memberId in memberInfos.Keys)
|
||
{
|
||
if (memberId == ownerId) continue;
|
||
if (!IsMemberReady(memberId)) return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public bool ResetGuestReadyStates()
|
||
{
|
||
if (!LobbyManager.Instance.Lobby.IsInLobby() || !LobbyManager.Instance.Lobby.IsLobbyOwner()) return false;
|
||
MultiCivs ??= new List<MemberCiv>();
|
||
var ownerId = LobbyManager.Instance.Lobby.GetLobbyOwnerId();
|
||
var changed = false;
|
||
foreach (var memberCiv in MultiCivs)
|
||
{
|
||
if (memberCiv == null) continue;
|
||
var shouldReady = memberCiv.MemberId == ownerId;
|
||
if (memberCiv.IsReady == shouldReady) continue;
|
||
memberCiv.IsReady = shouldReady;
|
||
changed = true;
|
||
}
|
||
|
||
return changed;
|
||
}
|
||
|
||
public bool SetResumeArchiveSelected(bool selected)
|
||
{
|
||
if (IsResumeArchiveSelected == selected) return false;
|
||
IsResumeArchiveSelected = selected;
|
||
return true;
|
||
}
|
||
|
||
private bool ApplyMemberCivLocal(MemberCiv civ)
|
||
{
|
||
EnsurePlayerSlots(NetMode.Multi);
|
||
var memberCiv = civ.MemberId != 0 ? GetMemberCiv(civ.MemberId) : null;
|
||
var targetIndex = civ.Index;
|
||
var targetSlot = targetIndex >= 0 && targetIndex < MultiCivs.Count ? MultiCivs[targetIndex] : null;
|
||
|
||
if (memberCiv != null && targetSlot != null && memberCiv != targetSlot && targetSlot.MemberId == 0)
|
||
{
|
||
ClearPlayerSlotMember(memberCiv, makeAi: true, clearCiv: true);
|
||
memberCiv = targetSlot;
|
||
}
|
||
|
||
memberCiv ??= targetSlot;
|
||
if (memberCiv == null) return false;
|
||
|
||
var nextMemberId = civ.MemberId;
|
||
var nextIsAI = nextMemberId == 0 && civ.IsAI;
|
||
if (nextMemberId != 0) nextIsAI = false;
|
||
|
||
if (memberCiv.MemberId == nextMemberId
|
||
&& memberCiv.CivId == civ.CivId
|
||
&& memberCiv.ForceId == civ.ForceId
|
||
&& memberCiv.PlayerId == civ.PlayerId
|
||
&& memberCiv.TeamId == civ.TeamId
|
||
&& memberCiv.IsAI == nextIsAI
|
||
&& memberCiv.IsCivFixed == civ.IsCivFixed
|
||
&& memberCiv.IsReady == civ.IsReady) return false;
|
||
|
||
memberCiv.MemberId = nextMemberId;
|
||
memberCiv.CivId = civ.CivId;
|
||
memberCiv.ForceId = civ.ForceId;
|
||
memberCiv.PlayerId = civ.PlayerId;
|
||
memberCiv.TeamId = civ.TeamId;
|
||
memberCiv.IsAI = nextIsAI;
|
||
memberCiv.IsCivFixed = civ.IsCivFixed;
|
||
memberCiv.IsReady = civ.IsReady;
|
||
NormalizePlayerSlot(memberCiv, memberCiv.Index);
|
||
RefreshMultiCivsDict();
|
||
return true;
|
||
}
|
||
|
||
public bool HasSameLobbyMembers(Dictionary<ulong, MemberInfo> memberInfos)
|
||
{
|
||
if (memberInfos == null) return false;
|
||
EnsurePlayerSlots(NetMode.Multi);
|
||
|
||
var seen = new HashSet<ulong>();
|
||
foreach (var memberCiv in MultiCivs)
|
||
{
|
||
if (memberCiv == null) return false;
|
||
if (memberCiv.MemberId == 0) continue;
|
||
if (!memberInfos.ContainsKey(memberCiv.MemberId)) return false;
|
||
if (!seen.Add(memberCiv.MemberId)) return false;
|
||
}
|
||
|
||
return seen.Count == memberInfos.Count;
|
||
}
|
||
|
||
// 主从端一致的本地数据检测
|
||
public void CheckMapConfigChanged()
|
||
{
|
||
if (!LobbyManager.Instance.Lobby.IsInLobby()) return;
|
||
|
||
byte[] bytes = MemoryPackSerializer.Serialize(this);
|
||
var hash128 = new Hash128();
|
||
hash128.Append(bytes);
|
||
var hash = hash128.ToString();
|
||
if (hash == _hash) return;
|
||
_hash = hash;
|
||
EventManager.Publish(new UpdateUIOutsideMultiplayRoomSetting());
|
||
if (LobbyManager.Instance.Lobby.IsLobbyOwner())
|
||
GameNetSender.Instance.UpdateLobbyData(Main.Instance.MapConfig);
|
||
}
|
||
|
||
public void ClearMultiCivs()
|
||
{
|
||
if (MultiCivs != null)
|
||
{
|
||
foreach (var slot in MultiCivs)
|
||
{
|
||
ClearPlayerSlotMember(slot, makeAi: true, clearCiv: true);
|
||
slot.TeamId = NoTeamId;
|
||
}
|
||
}
|
||
_memberCivs?.Clear();
|
||
}
|
||
|
||
public void ChangeByMapConfig(MapConfig other)
|
||
{
|
||
// [LimitsDebug] 追溯 MatchLimits 污染来源
|
||
var thisLimitsStr = MatchLimits != null ? string.Join(",", MatchLimits) : "null";
|
||
var otherLimitsStr = other?.MatchLimits != null ? string.Join(",", other.MatchLimits) : "null";
|
||
UnityEngine.Debug.Log($"[LimitsDebug] ChangeByMapConfig BEFORE: this.MatchLimits=[{thisLimitsStr}] (count={MatchLimits?.Count ?? -1}) other.MatchLimits=[{otherLimitsStr}] (count={other?.MatchLimits?.Count ?? -1}) thisHash={GetHashCode()} otherHash={other?.GetHashCode()}\n{System.Environment.StackTrace}");
|
||
if (other == null)
|
||
{
|
||
// 没有匹配到关卡配置时,回退为普通随机地图
|
||
IsCustomMap = false;
|
||
MapName = null;
|
||
return;
|
||
}
|
||
GameMode = other.GameMode;
|
||
IsCustomMap = other.IsCustomMap;
|
||
MapName = other.MapName;
|
||
MatchSettlement = other.MatchSettlement;
|
||
PlayerSettlements = other.PlayerSettlements;
|
||
}
|
||
|
||
// 刷新 Limits 移除重复 Limit
|
||
public void RefreshLimits()
|
||
{
|
||
if (MatchLimits == null || MatchLimits.Count == 0) return;
|
||
|
||
// 使用 HashSet 去重
|
||
var uniqueLimits = new HashSet<MatchLimitType>(MatchLimits);
|
||
MatchLimits.Clear();
|
||
MatchLimits.AddRange(uniqueLimits);
|
||
}
|
||
|
||
// 判断是否限制
|
||
public bool IsLimit(MatchLimitType limitType)
|
||
{
|
||
return MatchLimits != null && MatchLimits.Contains(limitType);
|
||
}
|
||
}
|
||
|
||
|
||
[MemoryPackable(GenerateType.NoGenerate)]
|
||
public partial class MemberCiv : IMemoryPackable<MemberCiv>
|
||
{
|
||
private const byte CurrentSerializedMemberCount = 9;
|
||
private const byte LegacySerializedMemberCountWithoutTeam = 8;
|
||
|
||
public ulong MemberId;
|
||
public uint PlayerId;
|
||
public uint CivId;
|
||
public uint ForceId;
|
||
public bool IsReady;
|
||
public int Index;
|
||
public int TeamId;
|
||
public bool IsAI;
|
||
public bool IsCivFixed;
|
||
|
||
public static void RegisterFormatter()
|
||
{
|
||
MemoryPackFormatterProvider.Register(new MemberCivFormatter());
|
||
}
|
||
|
||
public static void Serialize(ref MemoryPackWriter writer, ref MemberCiv value)
|
||
{
|
||
if (value == null)
|
||
{
|
||
writer.WriteNullObjectHeader();
|
||
return;
|
||
}
|
||
|
||
writer.WriteObjectHeader(CurrentSerializedMemberCount);
|
||
writer.WriteUnmanaged(value.MemberId);
|
||
writer.WriteUnmanaged(value.PlayerId);
|
||
writer.WriteUnmanaged(value.CivId);
|
||
writer.WriteUnmanaged(value.ForceId);
|
||
writer.WriteUnmanaged(value.IsReady);
|
||
writer.WriteUnmanaged(value.Index);
|
||
writer.WriteUnmanaged(value.TeamId);
|
||
writer.WriteUnmanaged(value.IsAI);
|
||
writer.WriteUnmanaged(value.IsCivFixed);
|
||
}
|
||
|
||
public static void Deserialize(ref MemoryPackReader reader, ref MemberCiv value)
|
||
{
|
||
if (!reader.TryReadObjectHeader(out var memberCount))
|
||
{
|
||
value = null;
|
||
return;
|
||
}
|
||
|
||
if (memberCount != CurrentSerializedMemberCount
|
||
&& memberCount != LegacySerializedMemberCountWithoutTeam)
|
||
{
|
||
MemoryPackSerializationException.ThrowInvalidPropertyCount(
|
||
typeof(MemberCiv),
|
||
CurrentSerializedMemberCount,
|
||
memberCount);
|
||
}
|
||
|
||
value ??= new MemberCiv();
|
||
value.MemberId = reader.ReadUnmanaged<ulong>();
|
||
value.PlayerId = reader.ReadUnmanaged<uint>();
|
||
value.CivId = reader.ReadUnmanaged<uint>();
|
||
value.ForceId = reader.ReadUnmanaged<uint>();
|
||
value.IsReady = reader.ReadUnmanaged<bool>();
|
||
value.Index = reader.ReadUnmanaged<int>();
|
||
if (memberCount == LegacySerializedMemberCountWithoutTeam)
|
||
{
|
||
value.TeamId = GetLegacyDefaultTeamId(value.Index);
|
||
value.IsAI = reader.ReadUnmanaged<bool>();
|
||
value.IsCivFixed = reader.ReadUnmanaged<bool>();
|
||
}
|
||
else
|
||
{
|
||
value.TeamId = Math.Max(MapConfig.NoTeamId, reader.ReadUnmanaged<int>());
|
||
value.IsAI = reader.ReadUnmanaged<bool>();
|
||
value.IsCivFixed = reader.ReadUnmanaged<bool>();
|
||
}
|
||
}
|
||
|
||
private static int GetLegacyDefaultTeamId(int index)
|
||
{
|
||
return Math.Max(MapConfig.NoTeamId + 1, index + 1);
|
||
}
|
||
|
||
private sealed class MemberCivFormatter : MemoryPackFormatter<MemberCiv>
|
||
{
|
||
public override void Serialize(ref MemoryPackWriter writer, ref MemberCiv value)
|
||
{
|
||
MemberCiv.Serialize(ref writer, ref value);
|
||
}
|
||
|
||
public override void Deserialize(ref MemoryPackReader reader, ref MemberCiv value)
|
||
{
|
||
MemberCiv.Deserialize(ref reader, ref value);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// 一场游戏的地图数据
|
||
[MemoryPackable]
|
||
public partial class MapData
|
||
{
|
||
//--------------- 基础核心数据 ---------------//
|
||
// 地图 ID
|
||
public uint MapID;
|
||
// 地图配置数据
|
||
public MapConfig MapConfig;
|
||
// 地块数据
|
||
public GridMapData GridMap;
|
||
// 玩家数据
|
||
public PlayerMapData PlayerMap;
|
||
// 城市数据
|
||
public CityMapData CityMap;
|
||
// 小兵数据
|
||
public UnitMapData UnitMap;
|
||
// 网络数据
|
||
public NetData Net;
|
||
// 结算数据
|
||
public MatchSettlementInfo MatchSettlement;
|
||
|
||
[MemoryPackIgnore]
|
||
public bool DeserializedMissingCriticalData { get; private set; }
|
||
|
||
// 当前玩家
|
||
[MemoryPackIgnore]
|
||
public PlayerData CurPlayer => PlayerMap?.GetPlayerData(Net?.CurPlayerId ?? 0);
|
||
|
||
// 城市 -> 玩家, 将引用关系转化为ID关系
|
||
public Dictionary<uint, uint> CityToPlayerDict;
|
||
// 小兵 -> 城市, 将引用关系转化为ID关系
|
||
public Dictionary<uint, uint> UnitToCityDict;
|
||
// 小兵 -> 格子, 将引用关系转化为ID关系
|
||
public Dictionary<uint, uint> UnitToGridDict;
|
||
// 城市 -> 格子, 将引用关系转化为ID关系
|
||
public Dictionary<uint, uint> CityToGridDict;
|
||
|
||
//--------------- 核心数据派生的数据 ---------------//
|
||
// 格子 -> 小兵, 缓存数据
|
||
private Dictionary<uint, uint> _gridToUnitDict;
|
||
// 格子 -> 城市, 缓存数据
|
||
private Dictionary<uint, uint> _gridToCityDict;
|
||
|
||
//--------------- 其他数据 ---------------//
|
||
// 地图 ID 生成器
|
||
[MemoryPackInclude]
|
||
private MapIdGenerator _idGenerator;
|
||
[MemoryPackIgnore]
|
||
public MapIdGenerator IdGenerator => _idGenerator;
|
||
|
||
//-------------- 读写城市方法 -------------------//
|
||
// 通过格子 gid 找城市 cid
|
||
public bool GetCityIdByGridId(uint gid, out uint cid)
|
||
{
|
||
return _gridToCityDict.TryGetValue(gid, out cid);
|
||
}
|
||
|
||
// 通过格子 gid 找城市数据 cityData
|
||
public bool GetCityDataByGid(uint gid, out CityData cityData)
|
||
{
|
||
if (_gridToCityDict.TryGetValue(gid, out var cid))
|
||
{
|
||
return CityMap.GetCityById(_gridToCityDict[gid], out cityData);
|
||
}
|
||
|
||
cityData = null;
|
||
return false;
|
||
}
|
||
|
||
// 通过小兵 ID 找城市 cid
|
||
public bool GetCityIdByUnitId(uint uid, out uint cid)
|
||
{
|
||
return UnitToCityDict.TryGetValue(uid, out cid);
|
||
}
|
||
|
||
// 通过小兵 ID 找城市 data
|
||
public bool GetCityDataByUnitId(uint uid, out CityData cityData)
|
||
{
|
||
if (UnitToCityDict.TryGetValue(uid, out var cid))
|
||
{
|
||
return CityMap.GetCityById(cid, out cityData);
|
||
}
|
||
|
||
cityData = null;
|
||
return false;
|
||
}
|
||
|
||
// 通过领土格子 gid 找城市数据 cityData
|
||
public bool GetCityDataByTerritoryGid(uint gid, out CityData cityData)
|
||
{
|
||
return CityMap.GetCityDataByTerritoryGridId(gid, out cityData);
|
||
}
|
||
|
||
// 通过玩家 ID 找城市列表
|
||
public void GetCityDataListByPlayerId(uint pid, List<CityData> cityDataList)
|
||
{
|
||
foreach (var kv in CityToPlayerDict)
|
||
{
|
||
if (kv.Value != pid) continue;
|
||
if (!CityMap.GetCityById(kv.Key, out var cityData)) continue;
|
||
cityDataList.Add(cityData);
|
||
}
|
||
}
|
||
|
||
// 通过玩家 ID 找城市Set
|
||
public void GetCityDataListByPlayerId(uint pid, HashSet<CityData> cityDataList)
|
||
{
|
||
foreach (var kv in CityToPlayerDict)
|
||
{
|
||
if (kv.Value != pid) continue;
|
||
if (!CityMap.GetCityById(kv.Key, out var cityData)) continue;
|
||
cityDataList.Add(cityData);
|
||
}
|
||
}
|
||
|
||
// 通过玩家 ID 找城市 Set
|
||
public HashSet<CityData> GetCityDataSetByPlayerId(uint pid)
|
||
{
|
||
var citySet = new HashSet<CityData>();
|
||
foreach (var kv in CityToPlayerDict)
|
||
{
|
||
if (kv.Value != pid) continue;
|
||
if (!CityMap.GetCityById(kv.Key, out var cityData)) continue;
|
||
citySet.Add(cityData);
|
||
}
|
||
|
||
return citySet;
|
||
}
|
||
|
||
//通过玩家ID 找 领土Set
|
||
public HashSet<GridData> GetGridDataSetByPlayerId(uint pid)
|
||
{
|
||
var citySet = GetCityDataSetByPlayerId(pid);
|
||
var gridSet = new HashSet<GridData>();
|
||
foreach (var city in citySet)
|
||
{
|
||
var cityGridSet = new HashSet<uint>();
|
||
city.Territory.GetAllTerritoryArea(cityGridSet);
|
||
foreach (var gid in cityGridSet)
|
||
{
|
||
GridMap.GetGridDataByGid(gid, out var grid);
|
||
gridSet.Add(grid);
|
||
}
|
||
}
|
||
return gridSet;
|
||
}
|
||
|
||
//通过玩家ID 找 领土ID Set
|
||
public HashSet<uint> GetGridIdSetByPlayerId(uint pid)
|
||
{
|
||
var citySet = GetCityDataSetByPlayerId(pid);
|
||
var gridSet = new HashSet<uint>();
|
||
foreach (var city in citySet)
|
||
{
|
||
var cityGridSet = new HashSet<uint>();
|
||
city.Territory.GetAllTerritoryArea(cityGridSet);
|
||
gridSet.UnionWith(cityGridSet);
|
||
}
|
||
return gridSet;
|
||
}
|
||
|
||
// 通过玩家 ID 查找首都城市 cityData
|
||
public bool GetCapitalCityDataByPlayerId(uint pid, out CityData cityData)
|
||
{
|
||
using var pooledCityList = THCollectionPool.GetListHandle<CityData>(out var cityList);
|
||
GetCityDataListByPlayerId(pid, cityList);
|
||
foreach (var city in cityList)
|
||
{
|
||
if (!city.IsCapital) continue;
|
||
cityData = city;
|
||
return true;
|
||
}
|
||
|
||
cityData = null;
|
||
return false;
|
||
}
|
||
|
||
// 改变城市所属关系,如果原城市的国家死亡,那么会处理死亡国家的所有单位死亡的视觉逻辑
|
||
public bool SetCityIdToPlayerId(uint cid, uint pid)
|
||
{
|
||
if (!GetPlayerDataByCityId(cid, out var oldPlayerData)) return false;
|
||
if(!PlayerMap.GetPlayerDataByPlayerID(pid, out var newPlayerData)) return false;
|
||
if (!GetCapitalCityDataByPlayerId(oldPlayerData.Id, out var oldPlayerCapitalCityData)) return false;
|
||
if (!GetCapitalCityDataByPlayerId(pid, out var newPlayerCapitalCityData)) return false;
|
||
if (!CityMap.GetCityById(cid, out var city)) return false;
|
||
//playerData.RenderMark = true;
|
||
//part #1 如果cid的老东家没有别的城市了,判负
|
||
if (GetCityCount(oldPlayerData.Id) == 1)
|
||
{
|
||
//step #1 玩家死亡(设置mark)
|
||
oldPlayerData.Alive = false;
|
||
oldPlayerData.DieMark = true;
|
||
|
||
// Collect 调用
|
||
CollectManager.Instance.PlayerGameEndCollect(this, oldPlayerData, newPlayerData);
|
||
|
||
//step #2 所有unit都销毁
|
||
using var pooledUnitDataList = THCollectionPool.GetListHandle<UnitData>(out var unitDataList);
|
||
GetUnitDataListByCityId(cid,unitDataList);
|
||
foreach (var unitData in unitDataList)
|
||
unitData.Renderer(this)?.Die();
|
||
|
||
foreach (var unitData in unitDataList)
|
||
{
|
||
Main.UnitLogic.UnitUnnaturalDie(this,unitData);
|
||
//unitData.Renderer(this)
|
||
}
|
||
|
||
|
||
//step #3 撤销首都
|
||
city.IsCapital = false;
|
||
}
|
||
//否则要将所有unit转移到老东家的capital下面去
|
||
else
|
||
{
|
||
//如果cid的老东家的首都就是cid,那么老东家首先要更换首都
|
||
if (oldPlayerCapitalCityData.Id == cid)
|
||
{
|
||
//遍历cityList,找到第一个老东家的其他城市,让他变成新的首都
|
||
foreach (var tmpCityData in CityMap.CityList)
|
||
{
|
||
if (tmpCityData.Id == cid) continue;
|
||
if (GetPlayerIdByCityId(tmpCityData.Id, out var tmpPlayerId) && tmpPlayerId == oldPlayerData.Id)
|
||
{
|
||
tmpCityData.IsCapital = true;
|
||
oldPlayerCapitalCityData.IsCapital = false;
|
||
oldPlayerCapitalCityData = tmpCityData;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
//开始将所有cid的小兵都转移到oldPlayerCapitalCityData下去
|
||
using var pooledTmpUnitDataList = THCollectionPool.GetListHandle<UnitData>(out var tmpUnitDataList);
|
||
GetUnitDataListByCityId(cid, tmpUnitDataList);
|
||
foreach(var tmpUnitData in tmpUnitDataList)
|
||
SetUnitIdToCityId(tmpUnitData.Id,oldPlayerCapitalCityData.Id);
|
||
oldPlayerCapitalCityData.CityInfoRenderer(this)?.InstantUpdateCityInfo();
|
||
|
||
}
|
||
|
||
//如果这次这个城市是newPlayer的原始首都,那么new Player要更换首都
|
||
if (newPlayerData.CradleCityId == cid)
|
||
{
|
||
city.IsCapital = true;
|
||
newPlayerCapitalCityData.IsCapital = false;
|
||
}
|
||
|
||
//正式更换城市归属
|
||
CityToPlayerDict[cid] = pid;
|
||
if (CityMap.GetCityById(cid, out var cityData))
|
||
{
|
||
cityData.SetCityRenderer(this);
|
||
if (GetGridDataByCityId(cid, out var gridData))
|
||
gridData.Renderer(this)?.InstantUpdateCityBuilding(cityData.Level,cityData.Civ(this));
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
//改变小兵的位置
|
||
public void SetUnitIdToGridId(uint uid, uint gid)
|
||
{
|
||
if (UnitToGridDict.TryGetValue(uid, out var oldGid))
|
||
{
|
||
if (_gridToUnitDict.TryGetValue(oldGid, out var oldUid) && oldUid == uid)
|
||
_gridToUnitDict.Remove(oldGid);
|
||
}
|
||
|
||
UnitToGridDict[uid] = gid;
|
||
_gridToUnitDict[gid] = uid;
|
||
/*if (UnitMap.GetUnitDataByUnitId(uid, out var unitData))
|
||
unitData.Renderer(this)?.InstantUpdateUnit();*/
|
||
}
|
||
|
||
public void SetUnitDataDie(UnitData unitData)
|
||
{
|
||
unitData.SetDie();
|
||
|
||
// 在清除绑定前获取所属城市id和格子id
|
||
UnitToCityDict.TryGetValue(unitData.Id, out var cityId);
|
||
GetGridIdByUnitId(unitData.Id, out var gridId);
|
||
if (gridId != 0 && _gridToUnitDict.TryGetValue(gridId, out var gridUnitId) && gridUnitId == unitData.Id)
|
||
_gridToUnitDict.Remove(gridId);
|
||
|
||
// 清除数据层绑定
|
||
RemoveUnitData(unitData.Id);
|
||
|
||
// 刷新原来所在格子的视觉(Fire等VFX)
|
||
if (gridId != 0 && GridMap.GetGridDataByGid(gridId, out var gridData))
|
||
gridData.Renderer(this)?.InstantUpdateGrid();
|
||
|
||
// 刷新所属城市的CityInfo(人口数变化)
|
||
if (cityId != 0 && CityMap.GetCityById(cityId, out var cityData))
|
||
cityData.CityInfoRenderer(this)?.InstantUpdateCityInfo();
|
||
|
||
// [Fix 2026/05/15] 视野外死亡的 UnitRenderer 孤儿兜底
|
||
// 场景:AOE/溅射/远程等路径在视野外击杀单位时,调用方的 Die() 被 InMainSight 守卫跳过,
|
||
// 数据层已 RemoveUnitData 但 ROUnitMap 中 UnitRenderer 残留 → 后续地块进入视野
|
||
// 时由于 GameObject.SetActive 残留为 true 而显示出"尸体"。
|
||
// 修复:只对"该格子不在玩家视野"路径兜底,视野内死亡仍走原 Fragment Die 流程,避免打断动画。
|
||
if (this == Main.MapData && gridId != 0
|
||
&& PlayerMap?.SelfPlayerData?.Sight != null
|
||
&& !PlayerMap.SelfPlayerData.Sight.CheckIsInSight(gridId)
|
||
&& MapRenderer.Instance != null
|
||
&& MapRenderer.Instance.ROUnitMap.TryGetValue(unitData.Id, out var orphanRenderer))
|
||
{
|
||
orphanRenderer.Die();
|
||
}
|
||
}
|
||
|
||
//改变小兵到城市的所属关系
|
||
public void SetUnitIdToCityId(uint uid, uint cid)
|
||
{
|
||
UnitToCityDict[uid] = cid;
|
||
|
||
//if (CityMap.GetCityById(cid, out var cityData)) cityData.RenderMark = true;
|
||
//if (PlayerMap.GetPlayerDataByPlayerID(pid, out var playerData)) playerData.RenderMark = true;
|
||
}
|
||
|
||
|
||
//获得一个玩家有多少个城市
|
||
public int GetCityCount(uint pid)
|
||
{
|
||
int ret = 0;
|
||
foreach (var cityData in CityMap.CityList)
|
||
if (CityToPlayerDict.ContainsKey(cityData.Id) && CityToPlayerDict[cityData.Id] == pid)
|
||
ret++;
|
||
|
||
return ret;
|
||
}
|
||
|
||
//获得一共城市有多少单位
|
||
public int GetUnitCount(uint cid)
|
||
{
|
||
int ret = 0;
|
||
foreach (var unit in UnitMap.UnitList)
|
||
if (UnitToCityDict.ContainsKey(unit.Id) && UnitToCityDict[unit.Id] == cid)
|
||
ret+= unit.GetPopulation();
|
||
return ret;
|
||
}
|
||
|
||
//-------------- 读写小兵方法 -------------------//
|
||
// 通过格子 gid 找小兵 uid
|
||
public bool GetUnitIdByGid(uint gid, out uint uid)
|
||
{
|
||
return _gridToUnitDict.TryGetValue(gid, out uid);
|
||
}
|
||
|
||
|
||
//只能给GridData/PlayerData/CityData等调用的核心方法,外部业务不允许调用
|
||
public bool GetUnitDataByGid_Core(uint gid, out UnitData unitData)
|
||
{
|
||
unitData = null;
|
||
if (_gridToUnitDict.TryGetValue(gid, out var uid))
|
||
{
|
||
return UnitMap.GetUnitDataByUnitId(uid, out unitData);
|
||
}
|
||
return false;
|
||
}
|
||
// 通过格子 gid 找小兵 data
|
||
private bool GetUnitDataByGid(uint gid, out UnitData unitData)
|
||
{
|
||
unitData = null;
|
||
|
||
/*foreach(var tt in UnitMap.UnitList)
|
||
if (GetGridDataByUnitId(tt.Id,out var gridData))
|
||
if (gridData.Id == gid)
|
||
{
|
||
unitData = tt;
|
||
return true;
|
||
}
|
||
|
||
return false;*/
|
||
//TODO _gridToUnitDict这个缓存有错,和unitToGrid没有对上。 暂时用了上面的办法,之后需要修改这里的缓存
|
||
if (_gridToUnitDict.TryGetValue(gid, out var uid))
|
||
{
|
||
return UnitMap.GetUnitDataByUnitId(uid, out unitData);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 通过城市 ID 找小兵列表
|
||
public void GetUnitDataListByCityId(uint cid, List<UnitData> unitDataList)
|
||
{
|
||
foreach (var kv in UnitToCityDict)
|
||
{
|
||
if (kv.Value != cid) continue;
|
||
if (!UnitMap.GetUnitDataByUnitId(kv.Key, out var unitData)) continue;
|
||
unitDataList.Add(unitData);
|
||
}
|
||
}
|
||
|
||
// 通过玩家 ID 找小兵列表
|
||
public void GetUnitDataListByPlayerId(uint pid, List<UnitData> unitDataList)
|
||
{
|
||
foreach (var kv in CityToPlayerDict)
|
||
{
|
||
if (kv.Value != pid) continue;
|
||
GetUnitDataListByCityId(kv.Key, unitDataList);
|
||
}
|
||
}
|
||
|
||
// 通过城市 ID 找小兵 Set
|
||
public void GetUnitDataListByCityId(uint cid, HashSet<UnitData> unitDataList)
|
||
{
|
||
foreach (var kv in UnitToCityDict)
|
||
{
|
||
if (kv.Value != cid) continue;
|
||
if (!UnitMap.GetUnitDataByUnitId(kv.Key, out var unitData)) continue;
|
||
unitDataList.Add(unitData);
|
||
}
|
||
}
|
||
|
||
// 通过玩家 ID 找小兵 Set
|
||
public void GetUnitDataListByPlayerId(uint pid, HashSet<UnitData> unitDataList)
|
||
{
|
||
foreach (var kv in CityToPlayerDict)
|
||
{
|
||
if (kv.Value != pid) continue;
|
||
GetUnitDataListByCityId(kv.Key, unitDataList);
|
||
}
|
||
}
|
||
|
||
// 通过玩家 ID 找敌方小兵列表
|
||
public void GetOtherUnitDataListByPlayerId(uint pid, List<UnitData> unitDataList)
|
||
{
|
||
foreach (var kv in CityToPlayerDict)
|
||
{
|
||
if (kv.Value == pid) continue;
|
||
GetUnitDataListByCityId(kv.Key, unitDataList);
|
||
}
|
||
}
|
||
|
||
// 判断是否是友军
|
||
public bool IsLeagueUnitByUnit(uint uidA, uint uidB)
|
||
{
|
||
if (!GetPlayerIdByUnitId(uidA, out var pidA)) return false;
|
||
if (!GetPlayerIdByUnitId(uidB, out var pidB)) return false;
|
||
return SameUnion(pidA, pidB);
|
||
}
|
||
|
||
// 判断是否是友军(含刚背盟的盟友)
|
||
public bool IsLeagueOrJustBreakByUnit(uint uidA, uint uidB)
|
||
{
|
||
if (!GetPlayerIdByUnitId(uidA, out var pidA)) return false;
|
||
if (!GetPlayerIdByUnitId(uidB, out var pidB)) return false;
|
||
return SameUnionOrJustBreakUnion(pidA, pidB);
|
||
}
|
||
|
||
// 判断格子是否为友方/刚背盟盟友的城市中心
|
||
public bool IsLeagueOrJustBreakCityCenter(uint unitId, GridData grid)
|
||
{
|
||
if (grid.Resource != ResourceType.CityCenter) return false;
|
||
if (!GetCityDataByGid(grid.Id, out var city)) return false;
|
||
if (!GetPlayerIdByCityId(city.Id, out var cityPid)) return false;
|
||
if (!GetPlayerIdByUnitId(unitId, out var unitPid)) return false;
|
||
return SameUnionOrJustBreakUnion(unitPid, cityPid);
|
||
}
|
||
|
||
// 判断是否是友军
|
||
public bool IsLeagueUnitByPlayer(uint playerId, uint uidB)
|
||
{
|
||
if (!GetPlayerIdByUnitId(uidB, out var pidB)) return false;
|
||
return SameUnion(playerId, pidB);
|
||
}
|
||
|
||
// 判断是否是友军
|
||
public bool IsLeagueUnitByCity(uint cityId, uint uidB)
|
||
{
|
||
if (!GetPlayerIdByCityId(cityId, out var pidA)) return false;
|
||
if (!GetPlayerIdByUnitId(uidB, out var pidB)) return false;
|
||
return SameUnion(pidA, pidB);
|
||
}
|
||
|
||
public bool GetUnitByGiantType(GiantType giantType, uint unitLevel, out UnitData unitData)
|
||
{
|
||
foreach(var t in UnitMap.UnitList)
|
||
if (t.GiantType == giantType && t.UnitLevel == unitLevel)
|
||
{
|
||
unitData = t;
|
||
return true;
|
||
}
|
||
|
||
unitData = null;
|
||
return false;
|
||
}
|
||
|
||
|
||
//-------------- 读写玩家方法 -------------------//
|
||
// 通过城市找玩家 pid
|
||
public bool GetPlayerIdByCityId(uint cityId, out uint playerId)
|
||
{
|
||
return CityToPlayerDict.TryGetValue(cityId, out playerId);
|
||
}
|
||
|
||
// 通过城市找玩家 data
|
||
public bool GetPlayerDataByCityId(uint cityId, out PlayerData playerData)
|
||
{
|
||
playerData = null;
|
||
|
||
if (CityToPlayerDict.TryGetValue(cityId, out var playerId))
|
||
{
|
||
playerData = PlayerMap.GetPlayerData(playerId);
|
||
}
|
||
return playerData != null;
|
||
}
|
||
|
||
// 通过小兵 uid 找城市 pid
|
||
public bool GetPlayerIdByUnitId(uint uid, out uint pid)
|
||
{
|
||
if (UnitToCityDict.TryGetValue(uid, out var cid))
|
||
{
|
||
return CityToPlayerDict.TryGetValue(cid, out pid);
|
||
}
|
||
|
||
pid = 0;
|
||
return false;
|
||
}
|
||
|
||
// 通过小兵 uid 找城市数据 playerData
|
||
public bool GetPlayerDataByUnitId(uint uid, out PlayerData playerData)
|
||
{
|
||
playerData = null;
|
||
if (UnitToCityDict.TryGetValue(uid, out var cid))
|
||
{
|
||
if (CityToPlayerDict.TryGetValue(cid, out var pid))
|
||
{
|
||
playerData = PlayerMap.GetPlayerData(pid);
|
||
}
|
||
}
|
||
|
||
return playerData != null;
|
||
}
|
||
|
||
// 通过领土位置找玩家
|
||
public bool GetPlayerDataByTerritoryGridId(uint gid, out PlayerData playerData)
|
||
{
|
||
if (CityMap.GetCityDataByTerritoryGridId(gid, out var cityData))
|
||
{
|
||
return GetPlayerDataByCityId(cityData.Id, out playerData);
|
||
}
|
||
|
||
playerData = null;
|
||
return false;
|
||
}
|
||
|
||
|
||
//判断playerA和playerB是否同一个联盟
|
||
public bool SameUnion(uint pidA, uint pidB)
|
||
{
|
||
if (pidA == pidB) return true;
|
||
if (!PlayerMap.GetPlayerDataByPlayerID(pidA, out var pA)) return false;
|
||
pA.GetCountryDiplomacyInfo(pidB,out var dipInfo);
|
||
return dipInfo != null && (dipInfo.IsTeammate || dipInfo.DiplomacyState == DiplomacyState.League);
|
||
}
|
||
|
||
public bool SameUnionByUnitId(uint uidA, uint uidB)
|
||
{
|
||
if (!GetPlayerDataByUnitId(uidA, out var pA)) return false;
|
||
if (!GetPlayerDataByUnitId(uidB, out var pB)) return false;
|
||
return SameUnion(pA.Id,pB.Id);
|
||
}
|
||
|
||
|
||
public bool SameUnionOrJustBreakUnion(uint pidA, uint pidB)
|
||
{
|
||
if (pidA == pidB) return true;
|
||
if (!PlayerMap.GetPlayerDataByPlayerID(pidA, out var pA)) return false;
|
||
pA.GetCountryDiplomacyInfo(pidB,out var dipInfo);
|
||
return dipInfo != null
|
||
&& (dipInfo.IsTeammate
|
||
|| dipInfo.DiplomacyState is DiplomacyState.League or DiplomacyState.LeagueRupture);
|
||
}
|
||
|
||
//-------------- 查询方法方法 -------------------//
|
||
// 通过小兵 uid 找格子 gid
|
||
public bool GetGridIdByUnitId(uint uid, out uint gid)
|
||
{
|
||
return UnitToGridDict.TryGetValue(uid, out gid);
|
||
}
|
||
|
||
// 通过小兵 uid 找格子数据 data
|
||
public bool GetGridDataByUnitId(uint uid, out GridData data)
|
||
{
|
||
if (UnitToGridDict.TryGetValue(uid, out var gid))
|
||
{
|
||
return GridMap.GetGridDataByGid(gid, out data);
|
||
}
|
||
|
||
data = null;
|
||
return false;
|
||
}
|
||
|
||
// 通过城市 cid 找格子 gid
|
||
public bool GetGridIdByCityId(uint cid, out uint gid)
|
||
{
|
||
return CityToGridDict.TryGetValue(cid, out gid);
|
||
}
|
||
|
||
//根据曼哈顿距离返回最近的城市
|
||
public bool GetNearestCity(UnitData unit,out CityData outCity)
|
||
{
|
||
outCity = null;
|
||
int score = -1;
|
||
var unitPlayer = unit.Player(this);
|
||
var unitGrid = unit.Grid(this);
|
||
if (unitPlayer == null || unitGrid == null) return false;
|
||
foreach (var city in CityMap.CityList)
|
||
{
|
||
var cityPlayer = city.Player(this);
|
||
var cityGrid = city.Grid(this);
|
||
if (cityPlayer == null || cityGrid == null || cityPlayer != unitPlayer) continue;
|
||
if (score == -1 || GridMap.CalcManhattanDistance(cityGrid, unitGrid) < score)
|
||
{
|
||
outCity = city;
|
||
score = GridMap.CalcManhattanDistance(cityGrid, unitGrid);
|
||
}
|
||
|
||
}
|
||
return score != -1;
|
||
}
|
||
|
||
// 通过城市 cid 找格子数据 data
|
||
public bool GetGridDataByCityId(uint cid, out GridData data)
|
||
{
|
||
if (CityToGridDict.TryGetValue(cid, out var gid))
|
||
{
|
||
return GridMap.GetGridDataByGid(gid, out data);
|
||
}
|
||
|
||
data = null;
|
||
return false;
|
||
}
|
||
//判断当前map是不是Main.MapData
|
||
public bool IsCurrentShowMap()
|
||
{
|
||
return this == Main.MapData;
|
||
}
|
||
|
||
//------------- 计算地图信息方法 -----------------//
|
||
public bool CheckIfNearByGridSamePlayer(GridData gridData, int dir)
|
||
{
|
||
dir--;
|
||
int x = gridData.Pos.X + dir % 3 - 1, y = gridData.Pos.Y + dir / 3 - 1;
|
||
if(x < 0 || x >= MapConfig.Width || y < 0 || y >= MapConfig.Height)
|
||
return false;
|
||
if (GridMap.GetGridDataByPos(x, y, out GridData gridData2))
|
||
{
|
||
GetPlayerDataByTerritoryGridId(gridData.Id,out var p1);
|
||
GetPlayerDataByTerritoryGridId(gridData2.Id,out var p2);
|
||
return p1 == p2;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
public bool CheckIfNearByGridSameCity(GridData gridData, int dir)
|
||
{
|
||
dir--;
|
||
int x = gridData.Pos.X + dir % 3 - 1, y = gridData.Pos.Y + dir / 3 - 1;
|
||
if(x < 0 || x >= MapConfig.Width || y < 0 || y >= MapConfig.Height)
|
||
return false;
|
||
if (GridMap.GetGridDataByPos(x, y, out GridData gridData2))
|
||
{
|
||
GetCityDataByTerritoryGid(gridData.Id,out var c1);
|
||
GetCityDataByTerritoryGid(gridData2.Id,out var c2);
|
||
return c1 == c2;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
//返回在gid周围1格内,dir方向的那格是否是freeland
|
||
public bool CheckIfNearByGridFreeland(GridData gridData, int dir,out GridData gridData2)
|
||
{
|
||
gridData2 = null;
|
||
dir--;
|
||
int x = (int)gridData.Pos.X + dir % 3 - 1, y = (int)gridData.Pos.Y + dir / 3 - 1;
|
||
if(x < 0 || x >= MapConfig.Width || y < 0 || y >= MapConfig.Height)
|
||
return false;
|
||
if(GridMap.GetGridDataByPos(x, y, out gridData2))
|
||
return !GetPlayerDataByTerritoryGridId(gridData2.Id,out var playerData);
|
||
return false;
|
||
}
|
||
|
||
//判断grid的dir方向是不是道路或者海路联通的。注意要区分 陆地港口桥梁 和 港口海域 两种情况,港口是两种情况都出现的
|
||
public bool CheckIfNearByGridRoadCanConnenct(GridData gridData, int dir)
|
||
{
|
||
int x = gridData.Pos.X + dir % 3 - 1, y = gridData.Pos.Y + dir / 3 - 1;
|
||
if(x < 0 || x >= MapConfig.Width || y < 0 || y >= MapConfig.Height) return false;
|
||
if (!GridMap.GetGridDataByPos(x, y, out var gridData2)) return false;
|
||
|
||
//如果有一个没有road return
|
||
if (gridData.Feature != TerrainFeature.Road || gridData2.Feature != TerrainFeature.Road) return false;
|
||
|
||
//判断主权情况 均有主权且不是一个同盟要return false
|
||
if (GetPlayerDataByTerritoryGridId(gridData.Id, out var playerData1) &&
|
||
GetPlayerDataByTerritoryGridId(gridData2.Id, out var playerData2) &&
|
||
!SameUnion(playerData1.Id, playerData2.Id))
|
||
return false;
|
||
|
||
//排除一个是非港口海域,一个是陆地的情况
|
||
if (gridData.Terrain != TerrainType.Land && gridData.Resource != ResourceType.Port &&
|
||
gridData2.Terrain == TerrainType.Land)
|
||
{
|
||
//bridge的情况特判
|
||
if (gridData.Resource == ResourceType.Bridge) return true;
|
||
return false;
|
||
}
|
||
|
||
if (gridData2.Terrain != TerrainType.Land && gridData2.Resource != ResourceType.Port &&
|
||
gridData.Terrain == TerrainType.Land)
|
||
{
|
||
//bridge的情况特判
|
||
if (gridData2.Resource == ResourceType.Bridge) return true;
|
||
return false;
|
||
}
|
||
|
||
//剩下情况都是true
|
||
return true;
|
||
}
|
||
|
||
//返回在gid周围1个内,dir方向的那格是不是land
|
||
public bool CheckIfNearByGridIsLand(GridData gridData, int dir,out GridData gridData2)
|
||
{
|
||
gridData2 = null;
|
||
dir--;
|
||
int x = (int)gridData.Pos.X + dir % 3 - 1, y = (int)gridData.Pos.Y + dir / 3 - 1;
|
||
if(x < 0 || x >= MapConfig.Width || y < 0 || y >= MapConfig.Height)
|
||
return false;
|
||
GridMap.GetGridDataByPos(x, y, out gridData2);
|
||
return gridData2.Terrain == TerrainType.Land;
|
||
}
|
||
|
||
public List<uint> GetNearby1RadiusFreelandGidList(uint gid)
|
||
{
|
||
var ret = new List<uint>();
|
||
GridMap.GetGridDataByGid(gid,out GridData gridData);
|
||
//遍历周围一圈方向,将所有freeland加入列表
|
||
for (int dir = 1; dir <= 9; dir++)
|
||
if(CheckIfNearByGridFreeland(gridData, dir, out var gridData2))
|
||
ret.Add(gridData2.Id);
|
||
return ret;
|
||
}
|
||
|
||
//返回在gridData上是否存在建设桥梁的基础地理条件
|
||
public bool HasBridgeSideLand(GridData gridData,out bool isMirror)
|
||
{
|
||
isMirror = false;
|
||
if (gridData.Terrain == TerrainType.Land)
|
||
return false;
|
||
GridData t;
|
||
if (CheckIfNearByGridIsLand(gridData, 2, out t) && CheckIfNearByGridIsLand(gridData, 8, out t))
|
||
return true;
|
||
isMirror = true;
|
||
if (CheckIfNearByGridIsLand(gridData, 4, out t) && CheckIfNearByGridIsLand(gridData, 6, out t))
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
// 获取玩家的所有领土格子 ID - Buffer版本
|
||
private readonly List<CityData> _territoryTmpCityList = new List<CityData>();
|
||
public void GetPlayerTerritoryGridIdSet(uint pid, HashSet<uint> gridSet)
|
||
{
|
||
_territoryTmpCityList.Clear();
|
||
GetCityDataListByPlayerId(pid, _territoryTmpCityList);
|
||
foreach (var cityData in _territoryTmpCityList)
|
||
{
|
||
cityData.Territory.GetAllTerritoryArea(gridSet);
|
||
}
|
||
}
|
||
|
||
// 获取玩家的所有领土格子 ID - 兼容版本(会分配新集合)
|
||
public HashSet<uint> GetPlayerTerritoryGridIdSet(uint pid)
|
||
{
|
||
var gridSet = new HashSet<uint>();
|
||
GetPlayerTerritoryGridIdSet(pid, gridSet);
|
||
return gridSet;
|
||
}
|
||
|
||
public bool CheckIfCityFullPopulation(CityData cityData)
|
||
{
|
||
if (cityData == null)
|
||
{
|
||
//LogSystem.LogError($"CheckIfCityFullPopulation cityData is null");
|
||
return true;
|
||
}
|
||
|
||
return GetUnitCount(cityData.Id) >= cityData.Level;
|
||
}
|
||
|
||
public uint GetMapSize()
|
||
{
|
||
return MapConfig.Width * MapConfig.Height;
|
||
}
|
||
|
||
|
||
//-------------- 新建数据的方法 -------------------//
|
||
// 新建城市数据
|
||
public CityData AddCityData(uint gid, uint pid)
|
||
{
|
||
var player = PlayerMap.GetPlayerData(pid);
|
||
uint civId = player.PlayerCivId;
|
||
|
||
CityLibrary nameEnum = player.PlayerNamerData.GetSetNextCity();
|
||
CityData cityData = CityMap.AddCityData(GetNearby1RadiusFreelandGidList(gid), nameEnum, _idGenerator);
|
||
CityToPlayerDict[cityData.Id] = pid;
|
||
CityToGridDict[cityData.Id] = gid;
|
||
_gridToCityDict[gid] = cityData.Id;
|
||
//建立city会在改格子上自动建立road
|
||
if (GridMap.GetGridDataByGid(gid, out var gridData))
|
||
gridData.Feature = TerrainFeature.Road;
|
||
|
||
//处理rendermark
|
||
var gridIdSet = GetPlayerTerritoryGridIdSet(pid);
|
||
foreach(var gridId in gridIdSet)
|
||
if (GridMap.GetGridDataByGid(gridId, out var gridDataTerr))
|
||
gridDataTerr.Renderer(this)?.InstantUpdateGrid(true);
|
||
CityMap.CityMapRenderMark = true;
|
||
cityData.SetCityRenderer(this);
|
||
return cityData;
|
||
}
|
||
|
||
|
||
//TODO 这里要研究下,怎么迭代动画系统 让创生的动画和数据分离
|
||
// 新建小兵数据
|
||
public bool AddUnitData(uint gid, uint cid, UnitFullType unitFullType,out UnitData newUnit,float waitTime = 0f)
|
||
{
|
||
|
||
//Step #1 先检查是否能生成(比如在水上生成landOnly的单位就不行)
|
||
newUnit = null;
|
||
if (!GridMap.GetGridDataByGid(gid, out GridData grid)) return false;
|
||
if(!CheckLandTypeForGrid(unitFullType, grid))
|
||
return false;
|
||
|
||
//Step #2 再生成单位
|
||
newUnit = UnitMap.AddUnitData(unitFullType, _idGenerator);
|
||
UnitToCityDict[newUnit.Id] = cid;
|
||
UnitToGridDict[newUnit.Id] = gid;
|
||
_gridToUnitDict[gid] = newUnit.Id;
|
||
//Step #3 增加技能
|
||
AddUnitSkill(newUnit);
|
||
OnAnyUnitCreate(this, newUnit);
|
||
|
||
// collect 调用
|
||
CollectManager.Instance.OnAddUnitCollect(this, newUnit);
|
||
|
||
if (waitTime > 0f)
|
||
{
|
||
var tmpUnit = newUnit;
|
||
Timer.Instance.TimerRegister(this, () =>
|
||
{
|
||
//新增renderer
|
||
MapRenderer.Instance.RenderUpdateUnitMap();
|
||
//看情况是否显示,是否播放雾效
|
||
if(tmpUnit.Renderer(this)?.InstantUpdateUnit(true)??false)
|
||
tmpUnit.Grid(Main.MapData)?.Renderer(Main.MapData)?.PlayVFXInSight(new GridVFXParams(GridVFXType.Fog));
|
||
//刷新所属城市的CityInfo(人口数变化)
|
||
if (CityMap.GetCityById(cid, out var cityData))
|
||
cityData.CityInfoRenderer(this)?.InstantUpdateCityInfo();
|
||
},waitTime,"REISEN ILLUSION ADDUNITDATA");
|
||
}
|
||
else
|
||
{
|
||
//新增renderer
|
||
MapRenderer.Instance.RenderUpdateUnitMap();
|
||
//看情况是否显示,是否播放雾效
|
||
if(newUnit.Renderer(this)?.InstantUpdateUnit(true)??false)
|
||
newUnit.Grid(Main.MapData)?.Renderer(Main.MapData)?.PlayVFXInSight(new GridVFXParams(GridVFXType.Fog));
|
||
//刷新所属城市的CityInfo(人口数变化)
|
||
if (CityMap.GetCityById(cid, out var cityData))
|
||
cityData.CityInfoRenderer(this)?.InstantUpdateCityInfo();
|
||
}
|
||
|
||
if (GetPlayerDataByUnitId(newUnit.Id, out var playerData))
|
||
{
|
||
foreach (var kv in playerData.PlayerHeroData.HeroTaskDict)
|
||
kv.Value.OnTrainUnit(this, playerData, newUnit);
|
||
foreach (var item in playerData.MomentData.Items) item.OnTrainUnit(this, playerData, newUnit);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
//
|
||
public bool CheckLandTypeForGrid(UnitFullType unitFullType, GridData grid)
|
||
{
|
||
if (!Table.Instance.UnitTypeDataAssets.GetUnitTypeInfo(unitFullType, out var info)) return false;
|
||
if (grid.Terrain is TerrainType.Land)
|
||
return info.LandType is LandType.Fly or LandType.LandAndWater or LandType.LandAndPort
|
||
or LandType.LandOnly or LandType.WaterAndAshore;
|
||
if (grid.Terrain is TerrainType.ShallowSea or TerrainType.DeepSea)
|
||
{
|
||
if (grid.Resource == ResourceType.Bridge)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
if (grid.Resource == ResourceType.Port)
|
||
{
|
||
return info.LandType is LandType.Fly or LandType.WaterAndAshore or LandType.WaterOnly
|
||
or LandType.LandAndWater or LandType.LandAndPort;
|
||
}
|
||
return info.LandType is LandType.Fly or LandType.WaterAndAshore or LandType.WaterOnly
|
||
or LandType.LandAndWater;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
//给Unit增加skill ,通用类,
|
||
public void AddUnitSkill(UnitData unit)
|
||
{
|
||
if (!Table.Instance.UnitTypeDataAssets.GetUnitTypeInfo(unit.UnitType, unit.GiantType, unit.UnitLevel,out var unitInfo)) return;
|
||
if (!GetPlayerDataByUnitId(unit.Id, out var player)) return;
|
||
//var common = new List<SkillType>() { SkillType.WATERDEFENSE ,SkillType.WATERMOVE,SkillType.OCEANDEFENSE,SkillType.OCEANMOVE,SkillType.MOUNTAINDEFENSE,SkillType.MOUNTAINMOVE,SkillType.FORESTDEFENSE};
|
||
|
||
//Step #1先添加小兵在配表中自带的技能
|
||
foreach (var skill in unitInfo.Skills)
|
||
{
|
||
unit.AddInitSkill(skill, this);
|
||
if (unit.GetSkill(skill, out var addedSkill))
|
||
addedSkill.OnSkillAdd(this, unit.Id);
|
||
}
|
||
|
||
//Step #2再添加小兵在配表中自带的技能
|
||
var t = new CommonActionId();
|
||
t.ActionType = CommonActionType.UnitSkill;
|
||
|
||
//遍历所有techAtom
|
||
foreach (var atom in player.TechTree.TechAtomCacheSet)
|
||
{
|
||
if(!Table.Instance.TechDataAssets.GetTechAtomInfo(atom,out var info))continue;
|
||
if (!info.IsAddSkill) continue;
|
||
if (!info.CheckCondition(unit.UnitFullType)) continue;
|
||
unit.AddInitSkill(info.AddSkillType, this);
|
||
if (unit.GetSkill(info.AddSkillType, out var addedSkill))
|
||
addedSkill.OnSkillAdd(this, unit.Id);
|
||
}
|
||
}
|
||
|
||
// 移除小兵数据
|
||
public void RemoveUnitData(uint uid)
|
||
{
|
||
UnitMap.RemoveUnitData(uid);
|
||
if (UnitToGridDict.Remove(uid, out var gid))
|
||
{
|
||
if (_gridToUnitDict.TryGetValue(gid, out var gridUnitId) && gridUnitId == uid)
|
||
_gridToUnitDict.Remove(gid);
|
||
}
|
||
UnitToCityDict.Remove(uid);
|
||
}
|
||
|
||
// 获取所有 ID 单位
|
||
public List<IdentifierBase> GetAllIdentifierBase()
|
||
{
|
||
var list = new List<IdentifierBase>();
|
||
foreach (var idBase in PlayerMap.PlayerDataList) list.Add(idBase);
|
||
foreach (var idBase in CityMap.CityList) list.Add(idBase);
|
||
foreach (var idBase in UnitMap.UnitList) list.Add(idBase);
|
||
foreach (var idBase in GridMap.GridList) list.Add(idBase);
|
||
return list;
|
||
}
|
||
|
||
// 获取 ID 单位
|
||
public IdentifierBase GetIdentifierBase(uint id)
|
||
{
|
||
if (PlayerMap.GetPlayerDataByPlayerID(id, out var player)) return player;
|
||
if (CityMap.GetCityById(id, out var city)) return city;
|
||
if (UnitMap.GetUnitDataByUnitId(id, out var unit)) return unit;
|
||
if (GridMap.GetGridDataByGid(id, out var grid)) return grid;
|
||
return null;
|
||
}
|
||
|
||
//-------------- 生命周期方法 -------------------//
|
||
// 用于网络传输的默认反序列化构造
|
||
[MemoryPackConstructor]
|
||
public MapData()
|
||
{
|
||
CityToPlayerDict = new Dictionary<uint, uint>();
|
||
UnitToCityDict = new Dictionary<uint, uint>();
|
||
UnitToGridDict = new Dictionary<uint, uint>();
|
||
CityToGridDict = new Dictionary<uint, uint>();
|
||
_gridToCityDict = new Dictionary<uint, uint>();
|
||
_gridToUnitDict = new Dictionary<uint, uint>();
|
||
_idGenerator = new MapIdGenerator();
|
||
}
|
||
|
||
// 从地图配置初始化
|
||
public MapData(MapConfig mapCfg, NetMode netMode)
|
||
{
|
||
MapID = (uint)Guid.NewGuid().GetHashCode();
|
||
MapConfig = mapCfg;
|
||
_idGenerator = new MapIdGenerator();
|
||
|
||
GridMap = new GridMapData(mapCfg, _idGenerator);
|
||
PlayerMap = new PlayerMapData(this, _idGenerator, netMode);
|
||
CityMap = new CityMapData();
|
||
UnitMap = new UnitMapData();
|
||
Net = new NetData();
|
||
MatchSettlement = new MatchSettlementInfo();
|
||
|
||
CityToPlayerDict = new Dictionary<uint, uint>();
|
||
UnitToCityDict = new Dictionary<uint, uint>();
|
||
UnitToGridDict = new Dictionary<uint, uint>();
|
||
CityToGridDict = new Dictionary<uint, uint>();
|
||
_gridToCityDict = new Dictionary<uint, uint>();
|
||
_gridToUnitDict = new Dictionary<uint, uint>();
|
||
MatchSettlement.Init(this, mapCfg);
|
||
}
|
||
|
||
public MapData(MapData copyMap)
|
||
{
|
||
MapID = copyMap.MapID;
|
||
MapConfig = copyMap.MapConfig;
|
||
_idGenerator = copyMap._idGenerator.DeepCopy();
|
||
GridMap = new GridMapData(copyMap.GridMap);
|
||
PlayerMap = new PlayerMapData(copyMap.PlayerMap);
|
||
CityMap = new CityMapData(copyMap.CityMap);
|
||
UnitMap = new UnitMapData(copyMap.UnitMap);
|
||
Net = new NetData(copyMap.Net);
|
||
MatchSettlement = new MatchSettlementInfo(copyMap.MatchSettlement);
|
||
|
||
CityToPlayerDict = new Dictionary<uint, uint>();
|
||
UnitToCityDict = new Dictionary<uint, uint>();
|
||
UnitToGridDict = new Dictionary<uint, uint>();
|
||
CityToGridDict = new Dictionary<uint, uint>();
|
||
_gridToCityDict = new Dictionary<uint, uint>();
|
||
_gridToUnitDict = new Dictionary<uint, uint>();
|
||
foreach (var kv in copyMap.CityToPlayerDict) CityToPlayerDict[kv.Key] = kv.Value;
|
||
foreach (var kv in copyMap.UnitToCityDict) UnitToCityDict[kv.Key] = kv.Value;
|
||
foreach (var kv in copyMap.UnitToGridDict) UnitToGridDict[kv.Key] = kv.Value;
|
||
foreach (var kv in copyMap.CityToGridDict) CityToGridDict[kv.Key] = kv.Value;
|
||
foreach (var kv in copyMap._gridToCityDict) _gridToCityDict[kv.Key] = kv.Value;
|
||
foreach (var kv in copyMap._gridToUnitDict) _gridToUnitDict[kv.Key] = kv.Value;
|
||
}
|
||
|
||
// 获取深拷贝的一份新的地图数据
|
||
public MapData GetDeepCopyMapData()
|
||
{
|
||
return new MapData(this);
|
||
}
|
||
|
||
// 深度拷贝另一封 map 数据
|
||
public void DeepCopy(MapData copyMap)
|
||
{
|
||
MapID = copyMap.MapID;
|
||
MapConfig = copyMap.MapConfig;
|
||
_idGenerator.DeepCopy(copyMap._idGenerator);
|
||
GridMap.DeepCopy(copyMap.GridMap);
|
||
PlayerMap.DeepCopy(copyMap.PlayerMap);
|
||
CityMap.DeepCopy(copyMap.CityMap);
|
||
UnitMap.DeepCopy(copyMap.UnitMap);
|
||
Net.DeepCopy(copyMap.Net);
|
||
MatchSettlement.DeepCopy(copyMap.MatchSettlement);
|
||
|
||
CityToPlayerDict.Clear();
|
||
UnitToCityDict.Clear();
|
||
UnitToGridDict.Clear();
|
||
CityToGridDict.Clear();
|
||
_gridToCityDict.Clear();
|
||
_gridToUnitDict.Clear();
|
||
foreach (var kv in copyMap.CityToPlayerDict) CityToPlayerDict[kv.Key] = kv.Value;
|
||
foreach (var kv in copyMap.UnitToCityDict) UnitToCityDict[kv.Key] = kv.Value;
|
||
foreach (var kv in copyMap.UnitToGridDict) UnitToGridDict[kv.Key] = kv.Value;
|
||
foreach (var kv in copyMap.CityToGridDict) CityToGridDict[kv.Key] = kv.Value;
|
||
foreach (var kv in copyMap._gridToCityDict) _gridToCityDict[kv.Key] = kv.Value;
|
||
foreach (var kv in copyMap._gridToUnitDict) _gridToUnitDict[kv.Key] = kv.Value;
|
||
}
|
||
|
||
// MemoryPack 反序列化之后的后处理
|
||
[MemoryPackOnDeserialized]
|
||
public void OnAfterMemoryPackDeserialize()
|
||
{
|
||
// 重建缓存字典
|
||
DeserializedMissingCriticalData = MapConfig == null
|
||
|| GridMap == null
|
||
|| PlayerMap == null
|
||
|| CityMap == null
|
||
|| UnitMap == null
|
||
|| Net == null;
|
||
Net ??= new NetData();
|
||
Net.Players ??= new Dictionary<ulong, uint>();
|
||
Net.Actions ??= new List<ActionNetData>();
|
||
if (MapConfig != null) MapConfig.MatchSettlement = MatchSettlementInfo.NormalizeType(MapConfig.MatchSettlement);
|
||
if (MatchSettlement != null) MatchSettlement.SettlementType = MatchSettlementInfo.NormalizeType(MatchSettlement.SettlementType);
|
||
CityToPlayerDict ??= new Dictionary<uint, uint>();
|
||
UnitToCityDict ??= new Dictionary<uint, uint>();
|
||
UnitToGridDict ??= new Dictionary<uint, uint>();
|
||
CityToGridDict ??= new Dictionary<uint, uint>();
|
||
_gridToCityDict ??= new Dictionary<uint, uint>();
|
||
_gridToUnitDict ??= new Dictionary<uint, uint>();
|
||
_gridToCityDict.Clear();
|
||
_gridToUnitDict.Clear();
|
||
|
||
// 从主字典重建反向索引
|
||
foreach (var kv in CityToGridDict) _gridToCityDict[kv.Value] = kv.Key;
|
||
foreach (var kv in UnitToGridDict) _gridToUnitDict[kv.Value] = kv.Key;
|
||
|
||
// 重绑定
|
||
GridMap?.BindMapConfig(MapConfig);
|
||
|
||
// 刷新
|
||
foreach (var action in Net.Actions)
|
||
{
|
||
if (action?.Param == null) continue;
|
||
action.Param.MapData = this;
|
||
action.Param.RefreshParams();
|
||
}
|
||
MapConfig?.ApplyTeamDiplomacy(this);
|
||
}
|
||
|
||
// 当场上有小兵受伤前
|
||
public void BeforeUnitDamaged(SettlementInfo info)
|
||
{
|
||
using var unitHandle = THCollectionPool.GetListHandle<UnitData>(out var copyUnits);
|
||
copyUnits.AddRange(UnitMap.UnitList);
|
||
foreach (var unit in copyUnits)
|
||
{
|
||
//避免在遍历的时候,直接改到了skillsList
|
||
using var skillHandle = THCollectionPool.GetListHandle<SkillBase>(out var copy);
|
||
copy.AddRange(unit.Skills);
|
||
foreach (var skill in copy) skill.BeforeUnitDamaged(unit, this, info);
|
||
}
|
||
}
|
||
|
||
// 当场上有小兵受伤时
|
||
public void OnUnitDamaged(SettlementInfo info)
|
||
{
|
||
using var unitHandle = THCollectionPool.GetListHandle<UnitData>(out var copyUnits);
|
||
copyUnits.AddRange(UnitMap.UnitList);
|
||
foreach (var unit in copyUnits)
|
||
{
|
||
//避免在遍历的时候,直接改到了skillsList
|
||
using var skillHandle = THCollectionPool.GetListHandle<SkillBase>(out var copy);
|
||
copy.AddRange(unit.Skills);
|
||
foreach (var skill in copy) skill.OnUnitDamaged(unit, this, info);
|
||
}
|
||
}
|
||
|
||
// 当场上有小兵移动时
|
||
public void OnAnyUnitMove(MapData map, UnitData moveUnit, GridData target, MoveType moveType)
|
||
{
|
||
using var unitHandle = THCollectionPool.GetListHandle<UnitData>(out var copyUnits);
|
||
copyUnits.AddRange(UnitMap.UnitList);
|
||
foreach (var unit in copyUnits)
|
||
{
|
||
using var skillHandle = THCollectionPool.GetListHandle<SkillBase>(out var copy);
|
||
copy.AddRange(unit.Skills);
|
||
foreach (var skill in copy) skill.OnAnyUnitMove(map, unit, moveUnit, target, moveType);
|
||
}
|
||
if (target?.Skills == null) return;
|
||
using var gridSkillHandle = THCollectionPool.GetListHandle<SkillBase>(out var gridCopy);
|
||
gridCopy.AddRange(target.Skills);
|
||
foreach (var skill in gridCopy) skill.OnAnyUnitMove(map, target, moveUnit, target, moveType);
|
||
}
|
||
|
||
// 当行为执行后
|
||
public void OnActionExecuted(ActionLogicBase logic, CommonActionParams param)
|
||
{
|
||
using var unitHandle = THCollectionPool.GetListHandle<UnitData>(out var unitListCopy);
|
||
unitListCopy.AddRange(UnitMap.UnitList);
|
||
foreach (var unit in unitListCopy)
|
||
{
|
||
using var skillHandle = THCollectionPool.GetListHandle<SkillBase>(out var copy);
|
||
copy.AddRange(unit.Skills);
|
||
foreach (var skill in copy) skill.OnActionExecuted(logic, param, unit);
|
||
}
|
||
}
|
||
|
||
// 当场上有小兵死亡时
|
||
public void OnAnyUnitDie(MapData map, UnitData dieUnit)
|
||
{
|
||
using var unitHandle = THCollectionPool.GetListHandle<UnitData>(out var unitListCopy);
|
||
unitListCopy.AddRange(UnitMap.UnitList);
|
||
foreach (var unit in unitListCopy)
|
||
{
|
||
//避免在遍历的时候,直接改到了skillsList
|
||
using var skillHandle = THCollectionPool.GetListHandle<SkillBase>(out var copy);
|
||
copy.AddRange(unit.Skills);
|
||
foreach (var skill in copy) skill.OnAnyUnitDie(map, unit, dieUnit);
|
||
}
|
||
}
|
||
|
||
// 当场上有小兵创建时
|
||
public void OnAnyUnitCreate(MapData map, UnitData newUnit)
|
||
{
|
||
using var unitHandle = THCollectionPool.GetListHandle<UnitData>(out var unitListCopy);
|
||
unitListCopy.AddRange(UnitMap.UnitList);
|
||
foreach (var unit in unitListCopy)
|
||
{
|
||
//避免在遍历的时候,直接改到了skillsList
|
||
using var skillHandle = THCollectionPool.GetListHandle<SkillBase>(out var copy);
|
||
copy.AddRange(unit.Skills);
|
||
foreach (var skill in copy) skill.OnAnyUnitCreate(map, unit, newUnit);
|
||
}
|
||
}
|
||
|
||
public static bool SaveMatchConfig(MapConfig config)
|
||
{
|
||
if (config == null) return false;
|
||
|
||
// 改为二进制文件扩展名
|
||
string path = Application.persistentDataPath + "/../Config/match_config.dat";
|
||
int retryCount = 3;
|
||
|
||
while (retryCount > 0)
|
||
{
|
||
try
|
||
{
|
||
byte[] bytes = MemoryPackSerializer.Serialize(config);
|
||
if (FileTools.SafeWriteFile(path, bytes)) return true;
|
||
retryCount--;
|
||
if (retryCount <= 0)
|
||
{
|
||
LogSystem.LogError($"保存地图配置数据失败: 安全写入失败");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
retryCount--;
|
||
if (retryCount <= 0)
|
||
{
|
||
LogSystem.LogError($"保存地图配置数据失败: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public static MapConfig GetMatchConfig()
|
||
{
|
||
string path = Application.persistentDataPath + "/../Config/match_config.dat";
|
||
return ReadMatchConfigWithBackup(path);
|
||
}
|
||
|
||
private static MapConfig ReadMatchConfigWithBackup(string path)
|
||
{
|
||
var config = ReadMatchConfigFile(path);
|
||
if (config != null) return config;
|
||
|
||
var backupPath = path + ".bak";
|
||
if (!File.Exists(backupPath)) return null;
|
||
|
||
LogSystem.LogWarning($"读取地图配置数据失败,尝试读取备份: {backupPath}");
|
||
return ReadMatchConfigFile(backupPath);
|
||
}
|
||
|
||
private static MapConfig ReadMatchConfigFile(string path)
|
||
{
|
||
if (!File.Exists(path)) return null;
|
||
int retryCount = 3;
|
||
while (retryCount > 0)
|
||
{
|
||
try
|
||
{
|
||
byte[] bytes = File.ReadAllBytes(path);
|
||
var config = MemoryPackSerializer.Deserialize<MapConfig>(bytes);
|
||
|
||
config.ClearMultiCivs();
|
||
return config;
|
||
}
|
||
catch (IOException ex)
|
||
{
|
||
retryCount--;
|
||
if (retryCount <= 0)
|
||
{
|
||
LogSystem.LogError($"读取地图数据失败: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
catch (MemoryPackSerializationException ex)
|
||
{
|
||
LogSystem.LogError($"地图数据反序列化失败,可能是版本不兼容: {ex.Message}");
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogSystem.LogError($"读取地图数据时发生未知错误: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
public static bool SaveMapData(MapData map, bool isBegin=false, bool isEnd=false)
|
||
{
|
||
if (map == null) return false;
|
||
if (ShouldSkipMapArchive(map)) return false;
|
||
|
||
// 改为二进制文件扩展名
|
||
string path = Application.persistentDataPath + "/../Config/map_archive";
|
||
if (isBegin) path += "_begin";
|
||
else if (isEnd) path += "_end";
|
||
else path += "_continue";
|
||
if (map.Net.Mode == NetMode.Multi) path += "_multi";
|
||
path += $"_{map.MapID}.dat";
|
||
int retryCount = 3;
|
||
|
||
while (retryCount > 0)
|
||
{
|
||
try
|
||
{
|
||
byte[] bytes = SerializeMapArchive(map);
|
||
if (FileTools.SafeWriteFile(path, bytes))
|
||
{
|
||
InvalidateMapArchiveAvailabilityCache();
|
||
return true;
|
||
}
|
||
retryCount--;
|
||
if (retryCount <= 0)
|
||
{
|
||
LogSystem.LogError($"保存地图数据失败: 安全写入失败");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
retryCount--;
|
||
if (retryCount <= 0)
|
||
{
|
||
LogSystem.LogError($"保存地图数据失败: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private enum MapArchiveKind
|
||
{
|
||
Begin,
|
||
Continue,
|
||
End
|
||
}
|
||
|
||
private static readonly TimeSpan MapArchiveAvailabilityCacheRefreshInterval = TimeSpan.FromSeconds(1);
|
||
private static readonly Dictionary<bool, MapArchiveAvailabilityCache> MapArchiveAvailabilityCaches =
|
||
new Dictionary<bool, MapArchiveAvailabilityCache>();
|
||
|
||
private sealed class MapArchiveAvailabilityCache
|
||
{
|
||
public DateTime CheckedAtUtc;
|
||
public string Fingerprint;
|
||
public bool HasArchive;
|
||
}
|
||
|
||
public static bool HasMapArchive(bool isMulti = false)
|
||
{
|
||
var now = DateTime.UtcNow;
|
||
if (MapArchiveAvailabilityCaches.TryGetValue(isMulti, out var cache)
|
||
&& now - cache.CheckedAtUtc < MapArchiveAvailabilityCacheRefreshInterval)
|
||
{
|
||
return cache.HasArchive;
|
||
}
|
||
|
||
var fingerprint = GetMapArchiveAvailabilityFingerprint(isMulti);
|
||
if (cache != null && cache.Fingerprint == fingerprint)
|
||
{
|
||
cache.CheckedAtUtc = now;
|
||
return cache.HasArchive;
|
||
}
|
||
|
||
var hasArchive = GetLatestReadableMapArchive(isMulti, MapArchiveKind.Continue, 0) != null;
|
||
MapArchiveAvailabilityCaches[isMulti] = new MapArchiveAvailabilityCache
|
||
{
|
||
CheckedAtUtc = now,
|
||
Fingerprint = fingerprint,
|
||
HasArchive = hasArchive
|
||
};
|
||
return hasArchive;
|
||
}
|
||
|
||
public static MapData GetMapData(bool isMulti = false, bool isBegin = false, bool isEnd = false, uint mapId = 0)
|
||
{
|
||
var kind = GetMapArchiveKind(isBegin, isEnd);
|
||
return GetLatestReadableMapArchive(isMulti, kind, mapId);
|
||
}
|
||
|
||
private static MapArchiveKind GetMapArchiveKind(bool isBegin, bool isEnd)
|
||
{
|
||
if (isBegin) return MapArchiveKind.Begin;
|
||
if (isEnd) return MapArchiveKind.End;
|
||
return MapArchiveKind.Continue;
|
||
}
|
||
|
||
private static MapData GetLatestReadableMapArchive(bool isMulti, MapArchiveKind kind, uint mapId)
|
||
{
|
||
var files = GetMapArchiveCandidates(isMulti, kind, mapId);
|
||
var targetFile = files.FirstOrDefault();
|
||
if (targetFile == null) return null;
|
||
|
||
if (!TryParseMapArchiveFileName(targetFile, out _, out _, out var archiveMapId)) return null;
|
||
var endArchiveTimes = kind == MapArchiveKind.Continue
|
||
? GetLatestEndMapArchiveTimes(isMulti)
|
||
: null;
|
||
if (endArchiveTimes != null
|
||
&& endArchiveTimes.TryGetValue(archiveMapId, out var endWriteTime)
|
||
&& endWriteTime >= GetMapArchiveLastWriteTime(targetFile))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var mapData = ReadMapDataWithBackup(targetFile);
|
||
if (mapData == null) return null;
|
||
if (ShouldSkipMapArchive(mapData)) return null;
|
||
|
||
var expectedMode = isMulti ? NetMode.Multi : NetMode.Single;
|
||
if (mapData.Net.Mode != expectedMode)
|
||
{
|
||
LogSystem.LogWarning($"存档模式与文件名不一致,跳过: {targetFile}");
|
||
return null;
|
||
}
|
||
|
||
if (kind == MapArchiveKind.Continue && mapData.PlayerMap.SelfPlayerData?.IsSurrender == true)
|
||
return null;
|
||
|
||
return mapData;
|
||
}
|
||
|
||
private static bool ShouldSkipMapArchive(MapData map)
|
||
{
|
||
return map?.Net?.Mode == NetMode.Spectator
|
||
|| map?.MapConfig?.MatchSettlement == MatchSettlementType.Story;
|
||
}
|
||
|
||
private static void InvalidateMapArchiveAvailabilityCache()
|
||
{
|
||
MapArchiveAvailabilityCaches.Clear();
|
||
}
|
||
|
||
private static string GetMapArchiveAvailabilityFingerprint(bool isMulti)
|
||
{
|
||
string directory = Application.persistentDataPath + "/../Config/";
|
||
if (!Directory.Exists(directory)) return string.Empty;
|
||
|
||
unchecked
|
||
{
|
||
long hash = 17;
|
||
int count = 0;
|
||
AddMapArchiveFingerprint(directory, "map_archive_continue*.dat", isMulti, ref hash, ref count);
|
||
AddMapArchiveFingerprint(directory, "map_archive_continue*.dat.bak", isMulti, ref hash, ref count);
|
||
AddMapArchiveFingerprint(directory, "map_archive_end*.dat", isMulti, ref hash, ref count);
|
||
AddMapArchiveFingerprint(directory, "map_archive_end*.dat.bak", isMulti, ref hash, ref count);
|
||
return $"{count}:{hash}";
|
||
}
|
||
}
|
||
|
||
private static void AddMapArchiveFingerprint(string directory, string pattern, bool isMulti,
|
||
ref long hash, ref int count)
|
||
{
|
||
try
|
||
{
|
||
foreach (var path in Directory.EnumerateFiles(directory, pattern))
|
||
{
|
||
if (!TryParseMapArchiveFileName(path, out var kind, out var fileIsMulti, out _)) continue;
|
||
if (fileIsMulti != isMulti) continue;
|
||
if (kind != MapArchiveKind.Continue && kind != MapArchiveKind.End) continue;
|
||
|
||
count++;
|
||
hash = hash * 31 + Path.GetFileName(path).GetHashCode();
|
||
hash = hash * 31 + GetMapArchiveLastWriteTime(path).Ticks;
|
||
hash = hash * 31 + GetMapArchiveFileLength(path);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogSystem.LogError($"读取存档目录失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private static long GetMapArchiveFileLength(string path)
|
||
{
|
||
try
|
||
{
|
||
return new FileInfo(path).Length;
|
||
}
|
||
catch
|
||
{
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
private static List<string> GetMapArchiveCandidates(bool isMulti, MapArchiveKind kind, uint mapId)
|
||
{
|
||
string directory = Application.persistentDataPath + "/../Config/";
|
||
if (!Directory.Exists(directory)) return new List<string>();
|
||
|
||
try
|
||
{
|
||
return Directory.GetFiles(directory, "map_archive_*.dat")
|
||
.Concat(Directory.GetFiles(directory, "map_archive_*.dat.bak"))
|
||
.Where(path => IsMatchingMapArchive(path, isMulti, kind, mapId))
|
||
.OrderByDescending(GetMapArchiveLastWriteTime)
|
||
.ToList();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogSystem.LogError($"读取存档目录失败: {ex.Message}");
|
||
return new List<string>();
|
||
}
|
||
}
|
||
|
||
private static bool IsMatchingMapArchive(string path, bool isMulti, MapArchiveKind kind, uint mapId)
|
||
{
|
||
if (!TryParseMapArchiveFileName(path, out var fileKind, out var fileIsMulti, out var fileMapId)) return false;
|
||
if (fileKind != kind || fileIsMulti != isMulti) return false;
|
||
return mapId == 0 || fileMapId == mapId;
|
||
}
|
||
|
||
private static bool TryParseMapArchiveFileName(string path, out MapArchiveKind kind, out bool isMulti, out uint mapId)
|
||
{
|
||
kind = MapArchiveKind.Continue;
|
||
isMulti = false;
|
||
mapId = 0;
|
||
|
||
var fileName = Path.GetFileName(path);
|
||
if (string.IsNullOrEmpty(fileName)) return false;
|
||
if (fileName.EndsWith(".bak", StringComparison.OrdinalIgnoreCase))
|
||
fileName = fileName.Substring(0, fileName.Length - ".bak".Length);
|
||
if (!fileName.EndsWith(".dat", StringComparison.OrdinalIgnoreCase)) return false;
|
||
|
||
var stem = fileName.Substring(0, fileName.Length - ".dat".Length);
|
||
const string prefix = "map_archive_";
|
||
if (!stem.StartsWith(prefix, StringComparison.Ordinal)) return false;
|
||
|
||
var rest = stem.Substring(prefix.Length);
|
||
if (rest.StartsWith("begin_", StringComparison.Ordinal))
|
||
{
|
||
kind = MapArchiveKind.Begin;
|
||
rest = rest.Substring("begin_".Length);
|
||
}
|
||
else if (rest.StartsWith("continue_", StringComparison.Ordinal))
|
||
{
|
||
kind = MapArchiveKind.Continue;
|
||
rest = rest.Substring("continue_".Length);
|
||
}
|
||
else if (rest.StartsWith("end_", StringComparison.Ordinal))
|
||
{
|
||
kind = MapArchiveKind.End;
|
||
rest = rest.Substring("end_".Length);
|
||
}
|
||
else
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (rest.StartsWith("multi_", StringComparison.Ordinal))
|
||
{
|
||
isMulti = true;
|
||
rest = rest.Substring("multi_".Length);
|
||
}
|
||
|
||
return uint.TryParse(rest, out mapId);
|
||
}
|
||
|
||
private static Dictionary<uint, DateTime> GetLatestEndMapArchiveTimes(bool isMulti)
|
||
{
|
||
var latestTimes = new Dictionary<uint, DateTime>();
|
||
var endFiles = GetMapArchiveCandidates(isMulti, MapArchiveKind.End, 0);
|
||
foreach (var endFile in endFiles)
|
||
{
|
||
if (!TryParseMapArchiveFileName(endFile, out _, out _, out var mapId)) continue;
|
||
|
||
var writeTime = GetMapArchiveLastWriteTime(endFile);
|
||
if (!latestTimes.TryGetValue(mapId, out var existingTime) || writeTime > existingTime)
|
||
latestTimes[mapId] = writeTime;
|
||
}
|
||
|
||
return latestTimes;
|
||
}
|
||
|
||
private static DateTime GetMapArchiveLastWriteTime(string path)
|
||
{
|
||
try
|
||
{
|
||
return File.GetLastWriteTime(path);
|
||
}
|
||
catch
|
||
{
|
||
return DateTime.MinValue;
|
||
}
|
||
}
|
||
|
||
private static MapData ReadMapDataWithBackup(string targetFile)
|
||
{
|
||
if (targetFile.EndsWith(".bak", StringComparison.OrdinalIgnoreCase))
|
||
return ReadMapDataFile(targetFile);
|
||
|
||
var mapData = ReadMapDataFile(targetFile);
|
||
if (mapData != null) return mapData;
|
||
|
||
var backupPath = targetFile + ".bak";
|
||
if (!File.Exists(backupPath)) return null;
|
||
|
||
LogSystem.LogWarning($"读取地图数据失败,尝试读取备份: {backupPath}");
|
||
return ReadMapDataFile(backupPath);
|
||
}
|
||
|
||
private static MapData ReadMapDataFile(string targetFile)
|
||
{
|
||
if (!File.Exists(targetFile)) return null;
|
||
|
||
int retryCount = 3;
|
||
while (retryCount > 0)
|
||
{
|
||
try
|
||
{
|
||
byte[] bytes = File.ReadAllBytes(targetFile);
|
||
var mapData = DeserializeMapArchive(bytes);
|
||
|
||
// 版本校验:检查反序列化后的数据是否有效
|
||
if (mapData == null
|
||
|| mapData.DeserializedMissingCriticalData
|
||
|| mapData.MapConfig == null
|
||
|| mapData.GridMap == null
|
||
|| mapData.PlayerMap == null
|
||
|| mapData.CityMap == null
|
||
|| mapData.UnitMap == null
|
||
|| mapData.Net == null)
|
||
{
|
||
LogSystem.LogError($"反序列化后的地图数据不完整,可能是版本不兼容");
|
||
return null;
|
||
}
|
||
|
||
return mapData;
|
||
}
|
||
catch (IOException ex)
|
||
{
|
||
retryCount--;
|
||
if (retryCount <= 0)
|
||
{
|
||
LogSystem.LogError($"读取地图数据失败: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
catch (MemoryPackSerializationException ex)
|
||
{
|
||
LogSystem.LogError($"地图数据反序列化失败,可能是版本不兼容: {ex.Message}");
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogSystem.LogError($"读取地图数据时发生未知错误: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static byte[] SerializeMapArchive(MapData map)
|
||
{
|
||
var rawBytes = MemoryPackSerializer.Serialize(map);
|
||
return NetworkPayloadCodec.Encode(rawBytes);
|
||
}
|
||
|
||
private static MapData DeserializeMapArchive(byte[] bytes)
|
||
{
|
||
var rawBytes = NetworkPayloadCodec.DecodeIfNeeded(bytes);
|
||
return MemoryPackSerializer.Deserialize<MapData>(rawBytes);
|
||
}
|
||
|
||
public GameRecord ExportGameRecord()
|
||
{
|
||
var gameRecord = new GameRecord();
|
||
gameRecord.Mode = MapConfig.GameMode;
|
||
gameRecord.AIDiff = MapConfig.AIDiff;
|
||
gameRecord.Turn = PlayerMap.SelfPlayerData.Turn;
|
||
// 警告:存档以 (PlayerCivId, PlayerForceId) 为玩家身份。多人允许相同 Empire 后,
|
||
// 同 Empire 多个玩家在 RegenerateMap 按 (Civ,Force) 匹配时只会命中第一个 PlayerData,存档恢复存在风险,待后续以 PlayerData.Id 替代。
|
||
gameRecord.PlayerCivId = PlayerMap.SelfPlayerData.PlayerCivId;
|
||
gameRecord.PlayerForceId = PlayerMap.SelfPlayerData.PlayerForceId;
|
||
gameRecord.Score = PlayerMap.SelfPlayerData.PlayerScore;
|
||
DateTime now = DateTime.Now;
|
||
gameRecord.Time = now.ToString("yyyy.MM.dd HH:mm");
|
||
gameRecord.CityCount = (uint)GetCityCount(PlayerMap.SelfPlayerId);
|
||
gameRecord.MapWidth = MapConfig.Width;
|
||
gameRecord.MapHeight = MapConfig.Height;
|
||
gameRecord.PlayerCount = MapConfig.PlayerCount;
|
||
gameRecord.MatchSettlement = MapConfig.MatchSettlement;
|
||
return gameRecord;
|
||
}
|
||
|
||
#region --- 判断数据间关系的快捷方法 ---
|
||
|
||
//判断gid是否属于pid
|
||
public bool CheckIfGidBelongPid(uint gid,uint pid)
|
||
{
|
||
if(!GetPlayerDataByTerritoryGridId(gid, out var playerData)) return false;
|
||
if (playerData.Id != pid) return false;
|
||
return true;
|
||
}
|
||
|
||
//判断gid是否属于pid的联盟
|
||
|
||
public bool CheckIfGidBelongPidUnion(uint gid,uint pid)
|
||
{
|
||
if(!GetPlayerDataByTerritoryGridId(gid, out var playerData)) return false;
|
||
return SameUnion(playerData.Id,pid);
|
||
}
|
||
|
||
public bool CheckCityIdBelongPlayerId(uint cid,uint pid)
|
||
{
|
||
return GetPlayerIdByCityId(cid, out var pid1) && pid1 == pid;
|
||
}
|
||
|
||
public bool CheckUnitIdBelongPlayerId(uint uid,uint pid)
|
||
{
|
||
return GetPlayerIdByUnitId(uid, out var pid1) && pid1 == pid;
|
||
}
|
||
|
||
//判断一个unit是否属于pid及其盟友
|
||
public bool CheckUnitIdBelongPlayerIdUnion(uint uid,uint pid)
|
||
{
|
||
|
||
return GetPlayerIdByUnitId(uid, out var pid1) && SameUnion(pid1,pid);
|
||
}
|
||
|
||
|
||
//判断一个grid是否属于pid及其盟友
|
||
public bool CheckGridIdBelongPlayerIdUnion(uint gid,uint pid)
|
||
{
|
||
//TODO #外交系统更新
|
||
return GetPlayerDataByTerritoryGridId(gid, out var player) && player.Id == pid;
|
||
}
|
||
|
||
public bool CheckUnitIdBelongCityId(uint uid,uint cid)
|
||
{
|
||
return GetCityIdByUnitId(uid, out var cid1) && cid1 == cid;
|
||
}
|
||
|
||
public bool CheckCityIdBelongSelfPlayer(uint cid)
|
||
{
|
||
return GetPlayerIdByCityId(cid, out var pid1) && pid1 == PlayerMap.SelfPlayerData.Id;
|
||
}
|
||
|
||
public bool CheckUnitIdBelongSelfPlayer(uint uid)
|
||
{
|
||
return GetPlayerIdByUnitId(uid, out var pid1) && pid1 == PlayerMap.SelfPlayerData.Id;
|
||
}
|
||
|
||
#endregion
|
||
|
||
// 更新回合
|
||
public void RefreshTurn(bool suppressNetworkBroadcast = false)
|
||
{
|
||
// 联机状态下只有房主有权限更新
|
||
if (Main.MapData.Net.Mode == NetMode.Multi && !LobbyManager.Instance.Lobby.IsLobbyOwner()) return;
|
||
|
||
if (PlayerMap.PlayerDataList.Count == 0)
|
||
{
|
||
LogSystem.LogError($"UpdateNextPlayer Error : PlayerDataList Count = 0!!!");
|
||
return;
|
||
}
|
||
|
||
// 检查下超时
|
||
if (Main.MapData.Net.Mode == NetMode.Multi && Main.MapData.MapConfig.IsLimitTime &&
|
||
Net.CurPlayerId != 0 && Time.time - Net.PlayerStartTime >= Main.MapData.MapConfig.TimeLimitSeconds)
|
||
Main.PlayerLogic.EndPlayerTurn(Main.MapData, Net.CurPlayerId);
|
||
if (Net.CurPlayerId != 0) return;
|
||
|
||
// 获取下一位
|
||
var nextPlayer = GetNextPlayer();
|
||
if (nextPlayer == null)
|
||
{
|
||
LogSystem.LogError($"UpdateNextPlayer Error : nextPlayer is null!!!");
|
||
return;
|
||
}
|
||
|
||
if (Main.MapData.Net.Mode == NetMode.Spectator)
|
||
{
|
||
Main.PlayerLogic.StartPlayerTurn(this, nextPlayer.Id);
|
||
return;
|
||
}
|
||
|
||
// 存档
|
||
var saveSucceeded = SaveMapData(Main.MapData);
|
||
AchievementDataManager.Instance.SaveAchievementData();
|
||
if (saveSucceeded)
|
||
{
|
||
if (Main.MapData.Net.Mode == NetMode.Single) PlayerPrefs.SetInt("Archive", 1);
|
||
if (Main.MapData.Net.Mode == NetMode.Multi) PlayerPrefs.SetInt("MultiArchive", 1);
|
||
PlayerPrefs.Save();
|
||
}
|
||
// 设置当前玩家
|
||
Main.PlayerLogic.StartPlayerTurn(this, nextPlayer.Id, suppressNetworkBroadcast);
|
||
}
|
||
|
||
// 获取下一位执行玩家
|
||
public PlayerData GetNextPlayer()
|
||
{
|
||
if (PlayerMap.PlayerDataList.Count == 0)
|
||
{
|
||
LogSystem.LogError($"UpdateNextPlayer Error : PlayerDataList Count = 0!!!");
|
||
return null;
|
||
}
|
||
|
||
var curTurn = uint.MaxValue;
|
||
foreach (var player in PlayerMap.PlayerDataList)
|
||
{
|
||
if (!player.IsSurvival) continue;
|
||
if (player.Turn >= curTurn) continue;
|
||
curTurn = player.Turn;
|
||
}
|
||
|
||
foreach (var player in PlayerMap.PlayerDataList)
|
||
{
|
||
// 如果要做复活逻辑,需要处理复活玩家的 Turn
|
||
if (!player.IsSurvival) continue;
|
||
if (player.Turn > curTurn) continue;
|
||
return player;
|
||
}
|
||
|
||
foreach (var player in PlayerMap.PlayerDataList)
|
||
{
|
||
if (!player.IsSurvival) continue;
|
||
return player;
|
||
}
|
||
|
||
LogSystem.LogError($"UpdateNextPlayer Error : PlayerDataList Alive Count = 0!!!");
|
||
return PlayerMap.PlayerDataList[0];
|
||
}
|
||
|
||
// 回合开始
|
||
public void OnTurnStart(uint playerId)
|
||
{
|
||
if (playerId == Net.CurPlayerId) return;
|
||
if (!PlayerMap.GetPlayerDataByPlayerID(playerId, out var player)) return;
|
||
Net.CurPlayerId = playerId;
|
||
PlayerMap.OnTurnStart(this, player);
|
||
CityMap.OnTurnStart(this, player);
|
||
UnitMap.OnTurnStart(this, player);
|
||
GridMap.OnTurnStart(this);
|
||
Net.PlayerStartTime = Time.time;
|
||
}
|
||
|
||
public void OnAfterTurnStart(uint playerId)
|
||
{
|
||
if (!PlayerMap.GetPlayerDataByPlayerID(playerId, out var player)) return;
|
||
UnitMap.OnAfterTurnStart(this, player);
|
||
}
|
||
|
||
// 回合结束
|
||
public void OnTurnEnd(uint playerId)
|
||
{
|
||
if (playerId != Net.CurPlayerId) return;
|
||
if (!PlayerMap.GetPlayerDataByPlayerID(playerId, out var player)) return;
|
||
PlayerMap.OnTurnEnd(this, player);
|
||
CityMap.OnTurnEnd(this, player);
|
||
UnitMap.OnTurnEnd(this, player);
|
||
GridMap.OnTurnEnd(this);
|
||
Net.CurPlayerId = 0;
|
||
}
|
||
|
||
// 游戏结束时获取胜利玩家 ID
|
||
public PlayerData GetWinPlayer()
|
||
{
|
||
foreach (var player in PlayerMap.PlayerDataList)
|
||
{
|
||
if (MatchSettlement.IsWin(player.Id)) return player;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// 游戏是否结束
|
||
public bool CheckIfGameEnd(out bool isWin)
|
||
{
|
||
isWin = IsPlayerOrTeammateWin(PlayerMap.SelfPlayerId);
|
||
return MatchSettlement.IsFinished;
|
||
}
|
||
|
||
public bool IsPlayerOrTeammateWin(uint playerId)
|
||
{
|
||
if (MatchSettlement == null) return false;
|
||
if (MatchSettlement.IsWin(playerId)) return true;
|
||
if (MapConfig == null || PlayerMap?.PlayerDataList == null) return false;
|
||
|
||
foreach (var player in PlayerMap.PlayerDataList)
|
||
{
|
||
if (player == null || player.Id == playerId) continue;
|
||
if (!MapConfig.ArePlayersInSameTeam(playerId, player.Id)) continue;
|
||
if (MatchSettlement.IsWin(player.Id)) return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// // 游戏是否结束
|
||
// public bool CheckIfGameEnd(out bool isWin)
|
||
// {
|
||
// #if GAME_AUTO_DEBUG
|
||
// if (CurPlayer != null && CurPlayer.Turn > 40)
|
||
// {
|
||
// uint maxId = 0;
|
||
// int maxScore = 0;
|
||
// foreach (var player in PlayerMap.PlayerDataList)
|
||
// {
|
||
// if (player.PlayerScore <= maxScore) continue;
|
||
// maxScore = player.PlayerScore;
|
||
// maxId = player.Id;
|
||
// }
|
||
// isWin = maxId == PlayerMap.SelfPlayerId;
|
||
// return true;
|
||
// }
|
||
// #endif
|
||
//
|
||
// if (Net.Mode == NetMode.Multi) return CheckIfGameEndMulti(out isWin);
|
||
//
|
||
// var aliveCount = 0;
|
||
// foreach (var player in PlayerMap.PlayerDataList)
|
||
// {
|
||
// if (player.Alive) aliveCount++;
|
||
// }
|
||
//
|
||
// isWin = aliveCount <= 1 && PlayerMap.SelfPlayerData.Alive;
|
||
//
|
||
// #if GAME_AUTO_DEBUG
|
||
// var isLose = aliveCount <= 1 && !PlayerMap.SelfPlayerData.Alive;
|
||
// #else
|
||
// var isLose = !PlayerMap.SelfPlayerData.Alive;
|
||
// #endif
|
||
//
|
||
// return isWin || isLose;
|
||
// }
|
||
//
|
||
// public bool CheckIfGameEndMulti(out bool isWin)
|
||
// {
|
||
// var aliveCount = 0;
|
||
// foreach (var player in PlayerMap.PlayerDataList)
|
||
// {
|
||
// if (player.Alive) aliveCount++;
|
||
// }
|
||
//
|
||
// var alivePlayerCount = 0;
|
||
// foreach (var kv in Net.Players)
|
||
// {
|
||
// if (kv.Key == 0 || kv.Value == 0) continue;
|
||
// if(!PlayerMap.GetPlayerDataByPlayerID(kv.Value, out var player)) continue;
|
||
// if (player.Alive) alivePlayerCount++;
|
||
// }
|
||
//
|
||
// isWin = aliveCount <= 1 && PlayerMap.SelfPlayerData.Alive;
|
||
// return alivePlayerCount == 0 || aliveCount <= 1;
|
||
// }
|
||
|
||
public bool CheckIsRealPlayer(uint playerId)
|
||
{
|
||
if (Net.Mode == NetMode.Multi)
|
||
{
|
||
foreach (var kv in Net.Players)
|
||
{
|
||
if (kv.Value == playerId) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
return playerId == PlayerMap.SelfPlayerId;
|
||
}
|
||
|
||
public bool CheckIsAI(uint playerId)
|
||
{
|
||
if (Net.Mode == NetMode.Multi)
|
||
{
|
||
foreach (var kv in Net.Players)
|
||
{
|
||
if (kv.Value == playerId) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
return playerId != PlayerMap.SelfPlayerId;
|
||
}
|
||
|
||
// 用于使用指定地图的地图相关内容重生成
|
||
public void RegenerateMap(MapRecordData recordData)
|
||
{
|
||
foreach (var player in PlayerMap.PlayerDataList)
|
||
{
|
||
player.Alive = false;
|
||
player.DieMark = false;
|
||
player.IsSurrender = false;
|
||
}
|
||
|
||
foreach (var grid in GridMap.GridList)
|
||
{
|
||
if (recordData.GridMap.GetGridDataByPos(grid.Pos.X, grid.Pos.Y, out var recordGrid))
|
||
{
|
||
var id = grid.Id;
|
||
grid.DeepCopy(recordGrid);
|
||
grid.Id = id;
|
||
}
|
||
else
|
||
{
|
||
LogSystem.LogError($"RegenerateMap Error 找不到地图 {recordData.MapName} 的格子信息 Pos=({grid.Pos.X},{grid.Pos.Y})");
|
||
}
|
||
}
|
||
|
||
// 警告:存档恢复以 (Civ,Force) 为主键匹配 PlayerData。多人允许相同 Empire 后,
|
||
// 同 Empire 的多个玩家会全部命中同一条记录(首匹配),后续玩家恢复结果错乱,待后续改为以 PlayerData.Id 匹配。
|
||
foreach (var playerInfo in recordData.PlayerInfos)
|
||
{
|
||
PlayerData p = null;
|
||
foreach (var player in PlayerMap.PlayerDataList)
|
||
{
|
||
if(player.PlayerCivId == playerInfo.Civ && player.PlayerForceId == playerInfo.Force)
|
||
{
|
||
p = player;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (p == null)
|
||
{
|
||
LogSystem.LogError($"RegenerateMap Error 找不到地图 {recordData.MapName} 的玩家文明信息 Civ={playerInfo.Civ} Force={playerInfo.Force}");
|
||
continue;
|
||
}
|
||
|
||
foreach (var cityInfo in playerInfo.Cities)
|
||
{
|
||
if (!GridMap.GetGridDataByPos(cityInfo.X, cityInfo.Y, out var cityGrid))
|
||
{
|
||
LogSystem.LogError($"RegenerateMap Error 找不到地图 {recordData.MapName} 的城市格子信息 Pos=({cityInfo.X},{cityInfo.Y})");
|
||
continue;
|
||
}
|
||
|
||
//建设一个新城市
|
||
CityData c = AddCityData(cityGrid.Id, p.Id);
|
||
c.Level = cityInfo.Level;
|
||
p.Alive = true;
|
||
if (cityInfo.IsCapital)
|
||
{
|
||
//设置该城市为首都
|
||
Main.CityLogic.SetCapital(this, c.Id);
|
||
//设置该玩家的文明摇篮城市为该城市
|
||
p.CradleCityId = c.Id;
|
||
}
|
||
|
||
foreach (var unitInfo in cityInfo.Units)
|
||
{
|
||
if (!GridMap.GetGridDataByPos(unitInfo.X, unitInfo.Y, out var unitGrid))
|
||
{
|
||
LogSystem.LogError($"RegenerateMap Error 找不到地图 {recordData.MapName} 的城市格子信息 Pos=({unitInfo.X},{unitInfo.Y})");
|
||
continue;
|
||
}
|
||
|
||
if (AddUnitData(unitGrid.Id, c.Id, unitInfo.UnitFullType, out var newUnit))
|
||
newUnit.SetFullActionPoint();
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach (var player in PlayerMap.PlayerDataList)
|
||
{
|
||
if (GetCityCount(player.Id) > 0)
|
||
{
|
||
player.Alive = true;
|
||
continue;
|
||
}
|
||
|
||
player.Alive = false;
|
||
player.DieMark = false;
|
||
}
|
||
}
|
||
|
||
// 用于使用指定地图进游戏的清理工作
|
||
public void Clear(MapConfig config)
|
||
{
|
||
Net.Clear();
|
||
MatchSettlement.Init(this, config);
|
||
}
|
||
|
||
// 检查两个 map 的不同之处
|
||
public void CompareEqual(MapData map)
|
||
{
|
||
var differences = new List<string>();
|
||
|
||
// 使用序列化比较各个组件
|
||
CompareComponent(MapConfig, map.MapConfig, "MapConfig", differences);
|
||
CompareComponent(GridMap, map.GridMap, "GridMap", differences);
|
||
CompareComponent(PlayerMap, map.PlayerMap, "PlayerMap", differences);
|
||
CompareComponent(PlayerMap.PlayerDataList, map.PlayerMap.PlayerDataList, "PlayerMap.PlayerDataList", differences);
|
||
CompareComponent(CityMap, map.CityMap, "CityMap", differences);
|
||
CompareComponent(UnitMap, map.UnitMap, "UnitMap", differences);
|
||
Net.CompareEqual(map.Net);
|
||
|
||
// 比较字典
|
||
CompareComponent(CityToPlayerDict, map.CityToPlayerDict, "CityToPlayerDict", differences);
|
||
CompareComponent(UnitToCityDict, map.UnitToCityDict, "UnitToCityDict", differences);
|
||
CompareComponent(UnitToGridDict, map.UnitToGridDict, "UnitToGridDict", differences);
|
||
CompareComponent(CityToGridDict, map.CityToGridDict, "CityToGridDict", differences);
|
||
|
||
// 输出结果
|
||
foreach (var diff in differences)
|
||
{
|
||
LogSystem.LogWarning($" - {diff}");
|
||
}
|
||
}
|
||
|
||
// 检查两个 map 的不同之处
|
||
public List<string> GetCompareEqual(MapData map)
|
||
{
|
||
var differences = new List<string>();
|
||
|
||
// 使用序列化比较各个组件
|
||
CompareComponent(MapConfig, map.MapConfig, "MapConfig", differences);
|
||
CompareComponent(GridMap, map.GridMap, "GridMap", differences);
|
||
CompareComponent(PlayerMap, map.PlayerMap, "PlayerMap", differences);
|
||
CompareComponent(PlayerMap.PlayerDataList, map.PlayerMap.PlayerDataList, "PlayerMap.PlayerDataList", differences);
|
||
CompareComponent(CityMap, map.CityMap, "CityMap", differences);
|
||
CompareComponent(UnitMap, map.UnitMap, "UnitMap", differences);
|
||
Net.CompareEqual(map.Net);
|
||
|
||
// 比较字典
|
||
CompareComponent(CityToPlayerDict, map.CityToPlayerDict, "CityToPlayerDict", differences);
|
||
CompareComponent(UnitToCityDict, map.UnitToCityDict, "UnitToCityDict", differences);
|
||
CompareComponent(UnitToGridDict, map.UnitToGridDict, "UnitToGridDict", differences);
|
||
CompareComponent(CityToGridDict, map.CityToGridDict, "CityToGridDict", differences);
|
||
|
||
return differences;
|
||
}
|
||
|
||
// 获取 player 是否在大回合内有没有行动
|
||
public bool GetPlayerHasActedInBigTurn(PlayerData player)
|
||
{
|
||
uint bigTurn = 0;
|
||
foreach (var p in PlayerMap.PlayerDataList)
|
||
{
|
||
if (p.Turn >= bigTurn) bigTurn = p.Turn;
|
||
}
|
||
return player.Turn < bigTurn;
|
||
}
|
||
|
||
public static bool CompareComponent2<T>(T obj1, T obj2)
|
||
{
|
||
try
|
||
{
|
||
var bytes1 = MemoryPackSerializer.Serialize(obj1);
|
||
var bytes2 = MemoryPackSerializer.Serialize(obj2);
|
||
|
||
return bytes1.SequenceEqual(bytes2);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogSystem.LogError($"comparison failed: {ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public static void CompareComponent<T>(T obj1, T obj2, string name, List<string> differences)
|
||
{
|
||
try
|
||
{
|
||
var bytes1 = MemoryPackSerializer.Serialize(obj1);
|
||
var bytes2 = MemoryPackSerializer.Serialize(obj2);
|
||
|
||
if (!bytes1.SequenceEqual(bytes2))
|
||
{
|
||
differences.Add($"{name} differs (serialized data mismatch)");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
differences.Add($"{name} comparison failed: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
public static List<string> FindDifferences<T>(T obj1, T obj2, string path = "", HashSet<object> visited = null)
|
||
{
|
||
var differences = new List<string>();
|
||
visited ??= new HashSet<object>();
|
||
|
||
if (obj1 == null && obj2 == null) return differences;
|
||
if (obj1 == null || obj2 == null)
|
||
{
|
||
differences.Add($"{path}: one is null ({obj1?.ToString() ?? "null"} vs {obj2?.ToString() ?? "null"})");
|
||
return differences;
|
||
}
|
||
|
||
// 防止循环引用
|
||
if (visited.Contains(obj1)) return differences;
|
||
visited.Add(obj1);
|
||
|
||
// 使用运行时类型
|
||
var type1 = obj1.GetType();
|
||
var type2 = obj2.GetType();
|
||
|
||
if (type1 != type2)
|
||
{
|
||
differences.Add($"{path}: type mismatch ({type1.Name} vs {type2.Name})");
|
||
return differences;
|
||
}
|
||
|
||
var type = type1;
|
||
|
||
// 处理基础类型
|
||
if (type.IsPrimitive || type == typeof(string) || type == typeof(decimal) || type.IsEnum)
|
||
{
|
||
if (!obj1.Equals(obj2))
|
||
differences.Add($"{path}: {obj1} != {obj2}");
|
||
return differences;
|
||
}
|
||
|
||
// 处理集合类型
|
||
if (obj1 is IEnumerable enum1 && obj2 is IEnumerable enum2 && !(obj1 is string))
|
||
{
|
||
var list1 = enum1.Cast<object>().ToList();
|
||
var list2 = enum2.Cast<object>().ToList();
|
||
|
||
if (list1.Count != list2.Count)
|
||
{
|
||
differences.Add($"{path}.Count: {list1.Count} != {list2.Count}");
|
||
}
|
||
|
||
var minCount = Math.Min(list1.Count, list2.Count);
|
||
for (int i = 0; i < minCount; i++)
|
||
{
|
||
var itemDiffs = FindDifferences(list1[i], list2[i], $"{path}[{i}]", new HashSet<object>(visited));
|
||
differences.AddRange(itemDiffs);
|
||
}
|
||
|
||
return differences;
|
||
}
|
||
|
||
// 处理字典类型
|
||
if (type.GetInterfaces()
|
||
.Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>)))
|
||
{
|
||
var keysProperty = type.GetProperty("Keys");
|
||
var itemProperty = type.GetProperty("Item");
|
||
|
||
if (keysProperty != null && itemProperty != null)
|
||
{
|
||
var keys1 = (IEnumerable)keysProperty.GetValue(obj1);
|
||
var keys2 = (IEnumerable)keysProperty.GetValue(obj2);
|
||
|
||
var keysList1 = keys1.Cast<object>().ToHashSet();
|
||
var keysList2 = keys2.Cast<object>().ToHashSet();
|
||
|
||
var allKeys = keysList1.Union(keysList2);
|
||
foreach (var key in allKeys)
|
||
{
|
||
var hasKey1 = keysList1.Contains(key);
|
||
var hasKey2 = keysList2.Contains(key);
|
||
|
||
if (!hasKey1 || !hasKey2)
|
||
{
|
||
differences.Add($"{path}[{key}]: exists in {(hasKey1 ? "obj1" : "obj2")} only");
|
||
continue;
|
||
}
|
||
|
||
var value1 = itemProperty.GetValue(obj1, new[] { key });
|
||
var value2 = itemProperty.GetValue(obj2, new[] { key });
|
||
|
||
var itemDiffs = FindDifferences(value1, value2, $"{path}[{key}]", new HashSet<object>(visited));
|
||
differences.AddRange(itemDiffs);
|
||
}
|
||
}
|
||
|
||
return differences;
|
||
}
|
||
|
||
// 处理复杂对象 - 只检查 MemoryPack 序列化的成员
|
||
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||
.Where(p => p.CanRead && IsMemoryPackSerializable(p));
|
||
|
||
foreach (var prop in properties)
|
||
{
|
||
try
|
||
{
|
||
var value1 = prop.GetValue(obj1);
|
||
var value2 = prop.GetValue(obj2);
|
||
|
||
var propPath = string.IsNullOrEmpty(path) ? prop.Name : $"{path}.{prop.Name}";
|
||
var propDiffs = FindDifferences(value1, value2, propPath, new HashSet<object>(visited));
|
||
differences.AddRange(propDiffs);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
differences.Add($"{path}.{prop.Name}: reflection error - {ex.Message}");
|
||
}
|
||
}
|
||
|
||
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance)
|
||
.Where(f => IsMemoryPackSerializable(f));
|
||
|
||
foreach (var field in fields)
|
||
{
|
||
try
|
||
{
|
||
var value1 = field.GetValue(obj1);
|
||
var value2 = field.GetValue(obj2);
|
||
|
||
var fieldPath = string.IsNullOrEmpty(path) ? field.Name : $"{path}.{field.Name}";
|
||
var fieldDiffs = FindDifferences(value1, value2, fieldPath, new HashSet<object>(visited));
|
||
differences.AddRange(fieldDiffs);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
differences.Add($"{path}.{field.Name}: reflection error - {ex.Message}");
|
||
}
|
||
}
|
||
|
||
return differences;
|
||
}
|
||
|
||
// 更完整的 MemoryPack 序列化检测
|
||
private static bool IsMemoryPackSerializable(System.Reflection.MemberInfo member)
|
||
{
|
||
// 检查是否有 MemoryPackIgnore 特性
|
||
if (member.GetCustomAttribute<MemoryPackIgnoreAttribute>() != null)
|
||
return false;
|
||
|
||
// 检查是否有 MemoryPackInclude 特性(明确包含)
|
||
if (member.GetCustomAttribute<MemoryPackIncludeAttribute>() != null)
|
||
return true;
|
||
|
||
// 对于属性:必须有 public getter 和 setter
|
||
if (member is System.Reflection.PropertyInfo prop)
|
||
{
|
||
return prop.CanRead && prop.CanWrite &&
|
||
prop.GetMethod?.IsPublic == true &&
|
||
prop.SetMethod?.IsPublic == true;
|
||
}
|
||
|
||
// 对于字段:必须是 public 且非 readonly
|
||
if (member is System.Reflection.FieldInfo field)
|
||
{
|
||
return field.IsPublic && !field.IsInitOnly && !field.IsLiteral;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// 简化的使用方法
|
||
public static bool CompareWithDetails<T>(T obj1, T obj2)
|
||
{
|
||
var differences = FindDifferences(obj1, obj2);
|
||
|
||
if (differences.Count > 0)
|
||
{
|
||
LogSystem.LogWarning($"Found {differences.Count} differences:");
|
||
foreach (var diff in differences.Take(10)) // 只显示前10个差异
|
||
{
|
||
LogSystem.LogWarning($" - {diff}");
|
||
}
|
||
if (differences.Count > 10)
|
||
LogSystem.LogWarning($" ... and {differences.Count - 10} more differences");
|
||
}
|
||
|
||
return differences.Count == 0;
|
||
}
|
||
|
||
public void FindDifferences(MapData target)
|
||
{
|
||
if (target == null) return;
|
||
var allStr = "";
|
||
var diff = GetCompareEqual(target);
|
||
foreach (var str in diff) allStr += str + "\n";
|
||
diff = FindDifferences(this, target);
|
||
foreach (var str in diff) allStr += str + "\n";
|
||
LogSystem.LogError($"{allStr}");
|
||
|
||
for (int i = 0; i < Net.Actions.Count; i++)
|
||
{
|
||
if (i >= target.Net.Actions.Count) continue;
|
||
if (Net.Actions[i].MapHash == target.Net.Actions[i].MapHash) continue;
|
||
if (i == 0)
|
||
{
|
||
LogSystem.LogError($"Map不一致前后Action : 前:空" +
|
||
$"后:{Main.MapData.Net.Actions[i].ActionId.GetStringLog()}");
|
||
}
|
||
else
|
||
{
|
||
LogSystem.LogError($"Map不一致前后Action : 前:{Main.MapData.Net.Actions[i - 1].ActionId.GetStringLog()}" +
|
||
$"后:{Main.MapData.Net.Actions[i].ActionId.GetStringLog()}");
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// 地图 ID 生成器
|
||
[MemoryPackable]
|
||
public partial class MapIdGenerator
|
||
{
|
||
[MemoryPackInclude]
|
||
private uint _idGenerator;
|
||
|
||
|
||
[MemoryPackConstructor]
|
||
public MapIdGenerator()
|
||
{
|
||
_idGenerator = 0;
|
||
}
|
||
|
||
public uint GeneratorId()
|
||
{
|
||
return ++_idGenerator;
|
||
}
|
||
|
||
public MapIdGenerator DeepCopy()
|
||
{
|
||
var copy = new MapIdGenerator();
|
||
copy._idGenerator = _idGenerator;
|
||
return copy;
|
||
}
|
||
|
||
public void DeepCopy(MapIdGenerator copy)
|
||
{
|
||
_idGenerator = copy._idGenerator;
|
||
}
|
||
}
|
||
}
|
||
|