546 lines
22 KiB
C#
546 lines
22 KiB
C#
/*
|
||
* @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());
|
||
}
|
||
}
|
||
}
|