TH1/Unity/Assets/Scripts/TH1_Logic/MatchConfig/MatchSettlementInfo.cs
2026-05-30 02:13:24 +08:00

546 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* @Author: 白哉
* @Description:
* @Date: 2025年09月05日 星期五 15:09:09
* @Modify:
*/
using System.Collections.Generic;
using Logic.CrashSight;
using MemoryPack;
using RuntimeData;
using TH1_Core.Events;
using TH1_Core.Managers;
using TH1_Logic.Collect;
namespace TH1_Logic.MatchConfig
{
public enum MatchSettlementType
{
None = 0,
Normal = 1,
Tutor = 3,
Story = 4,
}
[MemoryPackable]
public partial class MatchSettlementInfo
{
public MatchSettlementType SettlementType;
public bool IsFinished;
public Dictionary<uint, PlayerSettlementGroup> PlayerSettlements;
public static MatchSettlementType NormalizeType(MatchSettlementType matchType)
{
return (int)matchType switch
{
1 => MatchSettlementType.Normal,
2 => MatchSettlementType.Normal,
5 => MatchSettlementType.Normal,
3 => MatchSettlementType.Tutor,
4 => MatchSettlementType.Story,
_ => MatchSettlementType.None
};
}
[MemoryPackConstructor]
public MatchSettlementInfo()
{
IsFinished = false;
PlayerSettlements = new Dictionary<uint, PlayerSettlementGroup>();
}
public void Init(MapData map, MapConfig config)
{
SettlementType = NormalizeType(config?.MatchSettlement ?? MatchSettlementType.None);
IsFinished = false;
PlayerSettlements ??= new Dictionary<uint, PlayerSettlementGroup>();
PlayerSettlements.Clear();
var protectedSettlements = BuildProtectedSettlements(config);
foreach (var player in map.PlayerMap.PlayerDataList)
{
PlayerSettlements[player.Id] = new PlayerSettlementGroup();
PlayerSettlements[player.Id].Init(protectedSettlements);
}
}
public bool IsWin(uint id)
{
if (!IsFinished) return false;
var settlementGroup = PlayerSettlements.GetValueOrDefault(id);
if (settlementGroup == null) return false;
return settlementGroup.IsWin;
}
public MatchSettlementInfo(MatchSettlementInfo copyData)
{
SettlementType = NormalizeType(copyData.SettlementType);
IsFinished = copyData.IsFinished;
PlayerSettlements = new Dictionary<uint, PlayerSettlementGroup>();
foreach (var kv in copyData.PlayerSettlements)
{
PlayerSettlements[kv.Key] = new PlayerSettlementGroup(kv.Value);
}
}
public void DeepCopy(MatchSettlementInfo copyData)
{
SettlementType = NormalizeType(copyData.SettlementType);
IsFinished = copyData.IsFinished;
PlayerSettlements ??= new Dictionary<uint, PlayerSettlementGroup>();
PlayerSettlements.Clear();
foreach (var kv in copyData.PlayerSettlements)
{
PlayerSettlements[kv.Key] = new PlayerSettlementGroup(kv.Value);
}
}
private static List<PlayerSettlementInfo> BuildProtectedSettlements(MapConfig config)
{
var protectedSettlements = new List<PlayerSettlementInfo>();
var matchType = NormalizeType(config?.MatchSettlement ?? MatchSettlementType.None);
var sourceSettlements = config?.PlayerSettlements;
if (sourceSettlements != null)
{
foreach (var sourceSettlement in sourceSettlements)
{
if (sourceSettlement == null)
{
LogSystem.LogWarning("MatchSettlement.Init: 检测到空的 PlayerSettlementInfo已跳过。");
continue;
}
protectedSettlements.Add(BuildProtectedSettlement(
sourceSettlement,
matchType));
}
}
if (protectedSettlements.Count > 0) return protectedSettlements;
LogSystem.LogWarning(
$"MatchSettlement.Init: 结算配置为空或无效已注入默认结算任务。MatchType={matchType}");
protectedSettlements.Add(CreateFallbackSettlement(matchType));
return protectedSettlements;
}
private static PlayerSettlementInfo BuildProtectedSettlement(
PlayerSettlementInfo sourceSettlement,
MatchSettlementType matchType)
{
var settlementType = sourceSettlement.SettlementType;
if (settlementType == PlayerSettlementType.None)
{
LogSystem.LogWarning("MatchSettlement.Init: SettlementType=None已自动修正为 AllSuccessOrFailure。");
settlementType = PlayerSettlementType.AllSuccessOrFailure;
}
var settlement = new PlayerSettlementInfo
{
SettlementType = settlementType,
IsSettlement = false,
IsWin = false,
Tasks = new List<PlayerTaskInfo>()
};
if (sourceSettlement.Tasks != null)
{
foreach (var sourceTask in sourceSettlement.Tasks)
{
if (sourceTask == null)
{
LogSystem.LogWarning("MatchSettlement.Init: 检测到空的 PlayerTaskInfo已跳过。");
continue;
}
if (sourceTask.TaskType == PlayerTaskType.None)
{
LogSystem.LogWarning("MatchSettlement.Init: TaskType=None已跳过该任务。");
continue;
}
settlement.Tasks.Add(new PlayerTaskInfo
{
TaskType = sourceTask.TaskType,
IsSettlement = false,
IsSuccess = false,
Param1 = sourceTask.Param1,
Param2 = sourceTask.Param2,
Param3 = sourceTask.Param3,
Param4 = sourceTask.Param4,
Param1Cur = 0,
Param2Cur = 0,
Param3Cur = 0,
Order = sourceTask.Order,
CustomDesc = sourceTask.CustomDesc,
CustomHint = sourceTask.CustomHint,
UnlockLimits = sourceTask.UnlockLimits == null
? null
: new List<MatchLimitType>(sourceTask.UnlockLimits)
});
}
}
if (settlement.Tasks.Count > 0) return settlement;
LogSystem.LogWarning("MatchSettlement.Init: 结算器任务为空或无效,已注入默认任务。");
settlement.Tasks.Add(CreateFallbackTask(matchType));
return settlement;
}
private static PlayerSettlementInfo CreateFallbackSettlement(MatchSettlementType matchType)
{
return new PlayerSettlementInfo
{
SettlementType = PlayerSettlementType.AllSuccessOrFailure,
IsSettlement = false,
IsWin = false,
Tasks = new List<PlayerTaskInfo> { CreateFallbackTask(matchType) }
};
}
private static PlayerTaskInfo CreateFallbackTask(MatchSettlementType matchType)
{
// 教程/剧情默认给可收敛的生存任务其他模式默认给“其他玩家死亡”任务N-1
if (matchType == MatchSettlementType.Tutor || matchType == MatchSettlementType.Story)
{
return new PlayerTaskInfo
{
TaskType = PlayerTaskType.SurviveOrLoseMatch,
IsSettlement = false,
IsSuccess = false,
Param1 = 1,
Param2 = 0,
Param3 = 0,
Param4 = UnitType.None,
Param1Cur = 0,
Param2Cur = 0,
Param3Cur = 0
};
}
return new PlayerTaskInfo
{
TaskType = PlayerTaskType.OtherDie,
IsSettlement = false,
IsSuccess = false,
Param1 = -1,
Param2 = 0,
Param3 = 0,
Param4 = UnitType.None,
Param1Cur = 0,
Param2Cur = 0,
Param3Cur = 0
};
}
}
public class MatchSettlementLogicFactory
{
private static Dictionary<MatchSettlementType, IMatchSettlement> _logicDict = new Dictionary<MatchSettlementType, IMatchSettlement>()
{
{ MatchSettlementType.Normal, new NormalMatchSettlement() },
{ MatchSettlementType.Tutor, new TutorMatchSettlement() },
{ MatchSettlementType.Story, new StoryMatchSettlement() },
};
public static void RefreshMatchSettlementInfo(MapData map)
{
map.MatchSettlement.SettlementType = MatchSettlementInfo.NormalizeType(map.MatchSettlement.SettlementType);
if (!_logicDict.TryGetValue(map.MatchSettlement.SettlementType, out var logic))
{
LogSystem.LogError($"RefreshPlayerSettlementInfo Error TaskType:{map.MatchSettlement.SettlementType} No Logic");
return;
}
foreach (var kv in map.MatchSettlement.PlayerSettlements)
{
var player = map.PlayerMap.GetPlayerData(kv.Key);
if (player == null)
{
LogSystem.LogError($"RefreshMatchSettlementInfo Error Player is null");
continue;
}
PlayerSettlementGroup.RefreshPlayerSettlementGroup(map, player, kv.Value);
}
logic.Refresh(map, map.MatchSettlement);
// DEBUG: 输出结算状态
/*if (!map.MatchSettlement.IsFinished)
{
var debugStr = $"[Settlement] Type={map.MatchSettlement.SettlementType} IsFinished={map.MatchSettlement.IsFinished} PlayerCount={map.MatchSettlement.PlayerSettlements.Count}";
foreach (var kv in map.MatchSettlement.PlayerSettlements)
{
var p = map.PlayerMap.GetPlayerData(kv.Key);
var isAI = map.CheckIsAI(kv.Key);
var taskCount = 0;
foreach (var s in kv.Value.Settlements) taskCount += s.Tasks?.Count ?? 0;
debugStr += $"\n Player{kv.Key}(AI={isAI},Alive={p?.IsSurvival}): Group.IsSettlement={kv.Value.IsSettlement} IsWin={kv.Value.IsWin} Settlements={kv.Value.Settlements.Count} Tasks={taskCount}";
foreach (var s in kv.Value.Settlements)
{
debugStr += $"\n Settlement(Type={s.SettlementType}): IsSettled={s.IsSettlement} IsWin={s.IsWin}";
if (s.Tasks != null)
foreach (var t in s.Tasks)
debugStr += $"\n Task(Type={t.TaskType} P1={t.Param1}): IsSettled={t.IsSettlement} IsSuccess={t.IsSuccess} Cur={t.Param1Cur}";
}
}
UnityEngine.Debug.Log(debugStr);
}*/
}
}
public abstract class IMatchSettlement
{
public abstract MatchSettlementType GetSettlementType();
public abstract void Refresh(MapData map, MatchSettlementInfo info);
}
// NORMAL 任意玩家结算且为胜,游戏结束 所有非AI玩家结算且均为败游戏结束
public class NormalMatchSettlement : IMatchSettlement
{
public override MatchSettlementType GetSettlementType()
{
return MatchSettlementType.Normal;
}
public override void Refresh(MapData map, MatchSettlementInfo info)
{
if (info.IsFinished) return;
foreach (var kv in info.PlayerSettlements)
{
if (kv.Value.IsSettlement && kv.Value.IsWin)
{
info.IsFinished = true;
return;
}
}
foreach (var kv in info.PlayerSettlements)
{
if (map.CheckIsAI(kv.Key)) continue;
if (kv.Value.IsSettlement && !kv.Value.IsWin) continue;
MatchSettlementStuckGuard.CheckAndRecover(map, info, MatchSettlementType.Normal, kv.Key);
return;
}
info.IsFinished = true;
}
}
// SideTaskConfig 不影响 不参与
public class TutorMatchSettlement : IMatchSettlement
{
public override MatchSettlementType GetSettlementType()
{
return MatchSettlementType.Tutor;
}
public override void Refresh(MapData map, MatchSettlementInfo info)
{
if (info.IsFinished) return;
// Tutor 专属:本地玩家任务完成后解除关卡限制。
// 仅在 Tutor 模式生效,其他结算类型不会进入此分支。
ProcessUnlockLimits(map, info);
foreach (var kv in info.PlayerSettlements)
{
if (map.CheckIsAI(kv.Key)) continue;
if (kv.Value.IsSettlement) continue;
return;
}
info.IsFinished = true;
}
/// <summary>
/// 扫描本地玩家的任务,把已完成任务声明的 UnlockLimits 从 MapConfig.MatchLimits 移除。
/// 处理完后清空 UnlockLimits保证幂等再次进入函数不会重复触发重复 Remove 也不会出错)。
/// </summary>
private static void ProcessUnlockLimits(MapData map, MatchSettlementInfo info)
{
if (map?.MapConfig == null) return;
var selfId = map.PlayerMap.SelfPlayerId;
if (!info.PlayerSettlements.TryGetValue(selfId, out var selfGroup) || selfGroup == null) return;
if (selfGroup.Settlements == null) return;
var limits = map.MapConfig.MatchLimits;
if (limits == null) return;
// 追踪是否实际有限制被移除,仅在真正变动时广播 OnMatchLimitsChanged
// 避免每次 Refresh 都发空事件让 UI 做无意义刷新。
bool anyRemoved = false;
foreach (var settlement in selfGroup.Settlements)
{
if (settlement?.Tasks == null) continue;
foreach (var task in settlement.Tasks)
{
if (task == null) continue;
if (!task.IsSuccess) continue;
if (task.UnlockLimits == null || task.UnlockLimits.Count == 0) continue;
foreach (var limit in task.UnlockLimits)
{
if (limits.Remove(limit)) anyRemoved = true;
}
task.UnlockLimits.Clear();
}
}
if (anyRemoved) EventManager.Publish(new OnMatchLimitsChanged());
}
}
// TutorTaskConfig 若所有List<Task>完成,当前玩家结算 不参与
public class StoryMatchSettlement : IMatchSettlement
{
public override MatchSettlementType GetSettlementType()
{
return MatchSettlementType.Story;
}
public override void Refresh(MapData map, MatchSettlementInfo info)
{
if (info.IsFinished) return;
foreach (var kv in info.PlayerSettlements)
{
if (map.CheckIsAI(kv.Key)) continue;
if (kv.Value.IsSettlement) continue;
return;
}
info.IsFinished = true;
}
}
/// <summary>
/// 偶现 bug 兜底:玩家明明已经满足"应当结束"的客观条件,但 task 链路因为某种运行时累积态没把
/// PlayerSettlementGroup 翻成 IsSettlement导致游戏一直能下一回合。已知"退出再读档"可恢复。
/// 这里在原结算逻辑准备 return 之前做一次兜底判定,并通过 LogError 取证。
/// </summary>
public static class MatchSettlementStuckGuard
{
// 记录已经报过的 (matchType, blockingPlayerId) 组合,避免每回合刷屏
private static readonly HashSet<long> _reportedKeys = new HashSet<long>();
public static void Reset()
{
_reportedKeys.Clear();
}
public static void CheckAndRecover(
MapData map,
MatchSettlementInfo info,
MatchSettlementType matchType,
uint blockingPlayerId)
{
if (map == null || info == null) return;
if (info.IsFinished) return;
// TODO: id=10 is the current endless creative win-condition config. This is a temporary
// guard; later the stuck recovery should decide by settlement/task semantics instead of level id.
if (map.MapConfig?.Id == 10) return;
// 客观胜利条件blockingPlayerId 是非 AI 玩家自己,且仍存活,且非本队对手都已不存活
var self = map.PlayerMap.GetPlayerData(blockingPlayerId);
if (self == null || !self.IsSurvival) return;
foreach (var p in map.PlayerMap.PlayerDataList)
{
if (p == null) continue;
if (p.Id == blockingPlayerId) continue;
if (map.MapConfig != null
&& map.MapConfig.ArePlayersInSameTeam(blockingPlayerId, p.Id))
{
continue;
}
if (p.IsSurvival) return; // 还有非本队对手活着,不触发兜底
}
// 命中:取证 + 强制结算
ReportStuck(map, info, matchType, blockingPlayerId, self);
if (info.PlayerSettlements.TryGetValue(blockingPlayerId, out var selfGroup) && selfGroup != null)
{
selfGroup.IsSettlement = true;
selfGroup.IsWin = true;
}
info.IsFinished = true;
}
private static void ReportStuck(
MapData map,
MatchSettlementInfo info,
MatchSettlementType matchType,
uint blockingPlayerId,
PlayerData self)
{
var key = ((long)matchType << 32) | blockingPlayerId;
if (!_reportedKeys.Add(key)) return;
var sb = new System.Text.StringBuilder();
sb.Append("[MatchSettlementStuck] 触发兜底:");
sb.Append("MatchType=").Append(matchType);
sb.Append(" BlockingPlayerId=").Append(blockingPlayerId);
sb.Append(" Turn=").Append(self.Turn);
sb.Append(" NetMode=").Append(map.Net?.Mode.ToString() ?? "null");
sb.Append(" PlayerCount=").Append(map.PlayerMap.PlayerDataList.Count);
sb.Append('\n');
foreach (var p in map.PlayerMap.PlayerDataList)
{
if (p == null) continue;
sb.Append(" Player Id=").Append(p.Id);
sb.Append(" IsAI=").Append(map.CheckIsAI(p.Id));
sb.Append(" Alive=").Append(p.Alive);
sb.Append(" IsSurrender=").Append(p.IsSurrender);
sb.Append(" IsSurvival=").Append(p.IsSurvival);
sb.Append(" DieMark=").Append(p.DieMark);
sb.Append(" Turn=").Append(p.Turn);
sb.Append(" CityCount=").Append(map.GetCityCount(p.Id));
if (info.PlayerSettlements.TryGetValue(p.Id, out var group) && group != null)
{
sb.Append(" Group(IsSettlement=").Append(group.IsSettlement);
sb.Append(",IsWin=").Append(group.IsWin).Append(")");
if (group.Settlements != null)
{
for (int si = 0; si < group.Settlements.Count; si++)
{
var s = group.Settlements[si];
if (s == null) continue;
sb.Append("\n Settlement[").Append(si).Append("] Type=").Append(s.SettlementType);
sb.Append(" IsSettlement=").Append(s.IsSettlement);
sb.Append(" IsWin=").Append(s.IsWin);
if (s.Tasks != null)
{
for (int ti = 0; ti < s.Tasks.Count; ti++)
{
var t = s.Tasks[ti];
if (t == null) continue;
sb.Append("\n Task[").Append(ti).Append("] Type=").Append(t.TaskType);
sb.Append(" IsSettlement=").Append(t.IsSettlement);
sb.Append(" IsSuccess=").Append(t.IsSuccess);
sb.Append(" P1=").Append(t.Param1).Append("/Cur=").Append(t.Param1Cur);
sb.Append(" P2=").Append(t.Param2).Append("/Cur=").Append(t.Param2Cur);
sb.Append(" P3=").Append(t.Param3).Append("/Cur=").Append(t.Param3Cur);
sb.Append(" P4=").Append(t.Param4);
}
}
}
}
}
else
{
sb.Append(" Group=null");
}
sb.Append('\n');
}
LogSystem.LogError(sb.ToString());
}
}
}