Squashes 146 commits from battle-engine-extraction. Net: 2,045 files changed, +11,896 / -158,687 lines. Ships engine passes 4-7 (dead-code cull, view-layer stub, receive-path shrink) plus the Phase-5 AsyncLocal ambient deletion that turns concurrent battles into a type-system property rather than a scope contract. ## What landed **Passes 4-7 (chunks 1-34):** Extended the Phase-4 const-false collapse into a cascading cull across the skill graph, view layer, and receive-path periphery. Six mode flags (IsWatchBattle/IsReplayBattle/IsAdmin/IsAdminWatch/IsPuzzleQuest/ IsAINetwork) became `const false`, every guarded block deleted. Field*.cs subclass ctors + BackGroundBase + ObjectChecker culled to no-ops. Mulligan family reworked to take a mgr param through IMulliganMgr.InitMulligan. Emotion/Recovery/Resource clusters null-stubbed. Prediction/OperationSimulator/ skill filters converted from static ambient reads to per-mgr reads via SkillPrm.ownerCard.SelfBattlePlayer.BattleMgr / ins.BattleMgr / this.BattleMgr. **Phase-5 ambient rip (chunks 35-47):** Deleted BattleAmbient / BattleAmbient- Context / TestBattleScope in full. Every per-battle mutable slot now lives on the mgr instance itself: mgr.InstanceIsForecast / InstanceIsRandomDraw / InstanceRecoveryInfo / InstanceViewerId / InstanceNetworkAgent / GameMgr BattleManagerBase.GetIns() returns null unconditionally; the residual static flags + 3 façades (Certification.ViewerId, Data.BattleRecoveryInfo, ToolboxGame.RealTimeNetworkAgent) are null-tolerant defaults kept for the handful of engine-internal readers that still reference their types. Zero BattleAmbient references anywhere in engine + node + tests. Added pre-seeded GameMgr ctor overload threaded through the mgr chain (BattleManagerBase → SingleBattleMgr / NetworkBattleManagerBase → NetworkStandard- BattleMgr → HeadlessBattleMgr / HeadlessNetworkBattleMgr). Fixtures build a GameMgr, seed it via HeadlessEngineEnv.SeedCharaIds/SeedNetUser, and pass it to the mgr's ctor — no ambient reach. Node side (SVSim.BattleNode/SessionBattleEngine): _ctx replaced with a plain GameMgr field; 34 `using var _ambient = BattleAmbient.Enter(_ctx)` scope wraps ripped from every accessor and mutator; EngineGlobalInit.WirePerSessionGameMgr takes GameMgr as a param and runs from SessionBattleEngine.SetupInternal BEFORE mgr construction. Test side: TestBattleScope deleted; 18 fixture [SetUp]s migrated to `HeadlessEngineEnv.EnsureProcessGlobals()`; MultiInstanceEngineTests rewritten around per-mgr construction (GetIns() → null is the pinned invariant). ## Regression fixes - **chunk-48** (MulliganCtrl): chunk-35's `= null` stubs on card lookups broke the live receive-driven Deal path (BattlePlayerBase.DrawCard NRE'd downstream of NetworkPlayerMulliganCtrl.StartMulliganVfx). Restored the three lookups via `_battlePlayer.BattleMgr.GetBattleCardIdx`. Engine tests were satisfied by the WireMulliganPhase seam; unit tests exposed the live-path gap. ## Ship state - SVSim.BattleEngine.Tests: 56/56 pass, 2 skip - SVSim.UnitTests: 1554/1554 pass (was 1523/31-fail before chunk 48) - Solution build: 0 source warnings (40 pre-existing NU1902 MessagePack CVEs in SVSim.EmulatedEntrypoint, unrelated) - Sequential PVP smoke: verified live (two back-to-back battles, no regression on cleanup/spinup) - Concurrent PVP smoke: verified live Adds tools/engine-port/ClosureAnalyzer/ — the Roslyn transitive-type-closure analyzer needed to make future cascade cleanup safe (per feedback memory "Engine cleanup needs closure tool" from the 2026-06-28 pass-3 failure). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
628 lines
12 KiB
C#
628 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using UnityEngine;
|
|
|
|
[Serializable]
|
|
public class EventDelegate
|
|
{
|
|
[Serializable]
|
|
public class Parameter
|
|
{
|
|
public UnityEngine.Object obj;
|
|
|
|
public string field;
|
|
|
|
[NonSerialized]
|
|
private object mValue;
|
|
|
|
[NonSerialized]
|
|
public Type expectedType = typeof(void);
|
|
|
|
[NonSerialized]
|
|
public bool cached;
|
|
|
|
[NonSerialized]
|
|
public PropertyInfo propInfo;
|
|
|
|
[NonSerialized]
|
|
public FieldInfo fieldInfo;
|
|
|
|
public object value
|
|
{
|
|
get
|
|
{
|
|
if (mValue != null)
|
|
{
|
|
return mValue;
|
|
}
|
|
if (!cached)
|
|
{
|
|
cached = true;
|
|
fieldInfo = null;
|
|
propInfo = null;
|
|
if (obj != null && !string.IsNullOrEmpty(field))
|
|
{
|
|
Type type = obj.GetType();
|
|
propInfo = type.GetProperty(field);
|
|
if (propInfo == null)
|
|
{
|
|
fieldInfo = type.GetField(field);
|
|
}
|
|
}
|
|
}
|
|
if (propInfo != null)
|
|
{
|
|
return propInfo.GetValue(obj, null);
|
|
}
|
|
if (fieldInfo != null)
|
|
{
|
|
return fieldInfo.GetValue(obj);
|
|
}
|
|
if (obj != null)
|
|
{
|
|
return obj;
|
|
}
|
|
if (expectedType != null && expectedType.IsValueType)
|
|
{
|
|
return null;
|
|
}
|
|
return Convert.ChangeType(null, expectedType);
|
|
}
|
|
set
|
|
{
|
|
mValue = value;
|
|
}
|
|
}
|
|
|
|
public Type type
|
|
{
|
|
get
|
|
{
|
|
if (mValue != null)
|
|
{
|
|
return mValue.GetType();
|
|
}
|
|
if (obj == null)
|
|
{
|
|
return typeof(void);
|
|
}
|
|
return obj.GetType();
|
|
}
|
|
}
|
|
|
|
public Parameter()
|
|
{
|
|
}
|
|
|
|
public Parameter(UnityEngine.Object obj, string field)
|
|
{
|
|
this.obj = obj;
|
|
this.field = field;
|
|
}
|
|
|
|
public Parameter(object val)
|
|
{
|
|
mValue = val;
|
|
}
|
|
}
|
|
|
|
public delegate void Callback();
|
|
|
|
[SerializeField]
|
|
private MonoBehaviour mTarget;
|
|
|
|
[SerializeField]
|
|
private string mMethodName;
|
|
|
|
[SerializeField]
|
|
private Parameter[] mParameters;
|
|
|
|
public bool oneShot;
|
|
|
|
[NonSerialized]
|
|
private Callback mCachedCallback;
|
|
|
|
[NonSerialized]
|
|
private bool mRawDelegate;
|
|
|
|
[NonSerialized]
|
|
private bool mCached;
|
|
|
|
[NonSerialized]
|
|
private MethodInfo mMethod;
|
|
|
|
[NonSerialized]
|
|
private ParameterInfo[] mParameterInfos;
|
|
|
|
[NonSerialized]
|
|
private object[] mArgs;
|
|
|
|
private static int s_Hash = "EventDelegate".GetHashCode();
|
|
|
|
public MonoBehaviour target
|
|
{
|
|
get
|
|
{
|
|
return mTarget;
|
|
}
|
|
set
|
|
{
|
|
mTarget = value;
|
|
mCachedCallback = null;
|
|
mRawDelegate = false;
|
|
mCached = false;
|
|
mMethod = null;
|
|
mParameterInfos = null;
|
|
mParameters = null;
|
|
}
|
|
}
|
|
|
|
public string methodName
|
|
{
|
|
get
|
|
{
|
|
return mMethodName;
|
|
}
|
|
set
|
|
{
|
|
mMethodName = value;
|
|
mCachedCallback = null;
|
|
mRawDelegate = false;
|
|
mCached = false;
|
|
mMethod = null;
|
|
mParameterInfos = null;
|
|
mParameters = null;
|
|
}
|
|
}
|
|
|
|
public Parameter[] parameters
|
|
{
|
|
get
|
|
{
|
|
if (!mCached)
|
|
{
|
|
Cache();
|
|
}
|
|
return mParameters;
|
|
}
|
|
}
|
|
|
|
public bool isValid
|
|
{
|
|
get
|
|
{
|
|
if (!mCached)
|
|
{
|
|
Cache();
|
|
}
|
|
if (!mRawDelegate || mCachedCallback == null)
|
|
{
|
|
if (mTarget != null)
|
|
{
|
|
return !string.IsNullOrEmpty(mMethodName);
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public EventDelegate()
|
|
{
|
|
}
|
|
|
|
public EventDelegate(Callback call)
|
|
{
|
|
Set(call);
|
|
}
|
|
|
|
public EventDelegate(MonoBehaviour target, string methodName)
|
|
{
|
|
Set(target, methodName);
|
|
}
|
|
|
|
private static string GetMethodName(Callback callback)
|
|
{
|
|
return callback.Method.Name;
|
|
}
|
|
|
|
private static bool IsValid(Callback callback)
|
|
{
|
|
if (callback != null)
|
|
{
|
|
return callback.Method != null;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public override bool Equals(object obj)
|
|
{
|
|
if (obj == null)
|
|
{
|
|
return !isValid;
|
|
}
|
|
if (obj is Callback)
|
|
{
|
|
Callback callback = obj as Callback;
|
|
if (callback.Equals(mCachedCallback))
|
|
{
|
|
return true;
|
|
}
|
|
MonoBehaviour monoBehaviour = callback.Target as MonoBehaviour;
|
|
if (mTarget == monoBehaviour)
|
|
{
|
|
return string.Equals(mMethodName, GetMethodName(callback));
|
|
}
|
|
return false;
|
|
}
|
|
if (obj is EventDelegate)
|
|
{
|
|
EventDelegate eventDelegate = obj as EventDelegate;
|
|
if (mTarget == eventDelegate.mTarget)
|
|
{
|
|
return string.Equals(mMethodName, eventDelegate.mMethodName);
|
|
}
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return s_Hash;
|
|
}
|
|
|
|
private void Set(Callback call)
|
|
{
|
|
Clear();
|
|
if (call != null && IsValid(call))
|
|
{
|
|
mTarget = call.Target as MonoBehaviour;
|
|
if (mTarget == null)
|
|
{
|
|
mRawDelegate = true;
|
|
mCachedCallback = call;
|
|
mMethodName = null;
|
|
}
|
|
else
|
|
{
|
|
mMethodName = GetMethodName(call);
|
|
mRawDelegate = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Set(MonoBehaviour target, string methodName)
|
|
{
|
|
Clear();
|
|
mTarget = target;
|
|
mMethodName = methodName;
|
|
}
|
|
|
|
private void Cache()
|
|
{
|
|
mCached = true;
|
|
if (mRawDelegate || (mCachedCallback != null && !(mCachedCallback.Target as MonoBehaviour != mTarget) && !(GetMethodName(mCachedCallback) != mMethodName)) || !(mTarget != null) || string.IsNullOrEmpty(mMethodName))
|
|
{
|
|
return;
|
|
}
|
|
Type type = mTarget.GetType();
|
|
mMethod = null;
|
|
while (type != null)
|
|
{
|
|
try
|
|
{
|
|
mMethod = type.GetMethod(mMethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
|
if (mMethod != null)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
type = type.BaseType;
|
|
}
|
|
if (mMethod == null)
|
|
{
|
|
Debug.LogError("Could not find method '" + mMethodName + "' on " + mTarget.GetType(), mTarget);
|
|
return;
|
|
}
|
|
if (mMethod.ReturnType != typeof(void))
|
|
{
|
|
Debug.LogError(mTarget.GetType()?.ToString() + "." + mMethodName + " must have a 'void' return type.", mTarget);
|
|
return;
|
|
}
|
|
mParameterInfos = mMethod.GetParameters();
|
|
if (mParameterInfos.Length == 0)
|
|
{
|
|
mCachedCallback = (Callback)Delegate.CreateDelegate(typeof(Callback), mTarget, mMethodName);
|
|
mArgs = null;
|
|
mParameters = null;
|
|
return;
|
|
}
|
|
mCachedCallback = null;
|
|
if (mParameters == null || mParameters.Length != mParameterInfos.Length)
|
|
{
|
|
mParameters = new Parameter[mParameterInfos.Length];
|
|
int i = 0;
|
|
for (int num = mParameters.Length; i < num; i++)
|
|
{
|
|
mParameters[i] = new Parameter();
|
|
}
|
|
}
|
|
int j = 0;
|
|
for (int num2 = mParameters.Length; j < num2; j++)
|
|
{
|
|
mParameters[j].expectedType = mParameterInfos[j].ParameterType;
|
|
}
|
|
}
|
|
|
|
public bool Execute()
|
|
{
|
|
if (!mCached)
|
|
{
|
|
Cache();
|
|
}
|
|
if (mCachedCallback != null)
|
|
{
|
|
mCachedCallback();
|
|
return true;
|
|
}
|
|
if (mMethod != null)
|
|
{
|
|
if (mParameters == null || mParameters.Length == 0)
|
|
{
|
|
mMethod.Invoke(mTarget, null);
|
|
}
|
|
else
|
|
{
|
|
if (mArgs == null || mArgs.Length != mParameters.Length)
|
|
{
|
|
mArgs = new object[mParameters.Length];
|
|
}
|
|
int i = 0;
|
|
for (int num = mParameters.Length; i < num; i++)
|
|
{
|
|
mArgs[i] = mParameters[i].value;
|
|
}
|
|
try
|
|
{
|
|
mMethod.Invoke(mTarget, mArgs);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
string text = "Error calling ";
|
|
text = ((!(mTarget == null)) ? (text + mTarget.GetType()?.ToString() + "." + mMethod.Name) : (text + mMethod.Name));
|
|
text = text + ": " + ex.Message;
|
|
text += "\n Expected: ";
|
|
if (mParameterInfos.Length == 0)
|
|
{
|
|
text += "no arguments";
|
|
}
|
|
else
|
|
{
|
|
text += mParameterInfos[0];
|
|
for (int j = 1; j < mParameterInfos.Length; j++)
|
|
{
|
|
text = text + ", " + mParameterInfos[j].ParameterType;
|
|
}
|
|
}
|
|
text += "\n Received: ";
|
|
if (mParameters.Length == 0)
|
|
{
|
|
text += "no arguments";
|
|
}
|
|
else
|
|
{
|
|
text += mParameters[0].type;
|
|
for (int k = 1; k < mParameters.Length; k++)
|
|
{
|
|
text = text + ", " + mParameters[k].type;
|
|
}
|
|
}
|
|
text += "\n";
|
|
Debug.LogError(text);
|
|
}
|
|
int l = 0;
|
|
for (int num2 = mArgs.Length; l < num2; l++)
|
|
{
|
|
if (mParameterInfos[l].IsIn || mParameterInfos[l].IsOut)
|
|
{
|
|
mParameters[l].value = mArgs[l];
|
|
}
|
|
mArgs[l] = null;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
mTarget = null;
|
|
mMethodName = null;
|
|
mRawDelegate = false;
|
|
mCachedCallback = null;
|
|
mParameters = null;
|
|
mCached = false;
|
|
mMethod = null;
|
|
mParameterInfos = null;
|
|
mArgs = null;
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
if (mTarget != null)
|
|
{
|
|
string text = mTarget.GetType().ToString();
|
|
int num = text.LastIndexOf('.');
|
|
if (num > 0)
|
|
{
|
|
text = text.Substring(num + 1);
|
|
}
|
|
if (!string.IsNullOrEmpty(methodName))
|
|
{
|
|
return text + "/" + methodName;
|
|
}
|
|
return text + "/[delegate]";
|
|
}
|
|
if (!mRawDelegate)
|
|
{
|
|
return null;
|
|
}
|
|
return "[delegate]";
|
|
}
|
|
|
|
public static void Execute(List<EventDelegate> list)
|
|
{
|
|
if (list == null)
|
|
{
|
|
return;
|
|
}
|
|
int num = 0;
|
|
while (num < list.Count)
|
|
{
|
|
EventDelegate eventDelegate = list[num];
|
|
if (eventDelegate != null)
|
|
{
|
|
try
|
|
{
|
|
eventDelegate.Execute();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.InnerException != null)
|
|
{
|
|
Debug.LogError(ex.InnerException.Message);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError(ex.Message);
|
|
}
|
|
}
|
|
if (num >= list.Count)
|
|
{
|
|
break;
|
|
}
|
|
if (list[num] != eventDelegate)
|
|
{
|
|
continue;
|
|
}
|
|
if (eventDelegate.oneShot)
|
|
{
|
|
list.RemoveAt(num);
|
|
continue;
|
|
}
|
|
}
|
|
num++;
|
|
}
|
|
}
|
|
|
|
public static bool IsValid(List<EventDelegate> list)
|
|
{
|
|
if (list != null)
|
|
{
|
|
int i = 0;
|
|
for (int count = list.Count; i < count; i++)
|
|
{
|
|
EventDelegate eventDelegate = list[i];
|
|
if (eventDelegate != null && eventDelegate.isValid)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static EventDelegate Set(List<EventDelegate> list, Callback callback)
|
|
{
|
|
if (list != null)
|
|
{
|
|
EventDelegate eventDelegate = new EventDelegate(callback);
|
|
list.Clear();
|
|
list.Add(eventDelegate);
|
|
return eventDelegate;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public static void Set(List<EventDelegate> list, EventDelegate del)
|
|
{
|
|
if (list != null)
|
|
{
|
|
list.Clear();
|
|
list.Add(del);
|
|
}
|
|
}
|
|
|
|
public static EventDelegate Add(List<EventDelegate> list, Callback callback)
|
|
{
|
|
return Add(list, callback, oneShot: false);
|
|
}
|
|
|
|
public static EventDelegate Add(List<EventDelegate> list, Callback callback, bool oneShot)
|
|
{
|
|
if (list != null)
|
|
{
|
|
int i = 0;
|
|
for (int count = list.Count; i < count; i++)
|
|
{
|
|
EventDelegate eventDelegate = list[i];
|
|
if (eventDelegate != null && eventDelegate.Equals(callback))
|
|
{
|
|
return eventDelegate;
|
|
}
|
|
}
|
|
EventDelegate eventDelegate2 = new EventDelegate(callback);
|
|
eventDelegate2.oneShot = oneShot;
|
|
list.Add(eventDelegate2);
|
|
return eventDelegate2;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public static void Add(List<EventDelegate> list, EventDelegate ev)
|
|
{
|
|
Add(list, ev, ev.oneShot);
|
|
}
|
|
|
|
public static void Add(List<EventDelegate> list, EventDelegate ev, bool oneShot)
|
|
{
|
|
if (ev.mRawDelegate || ev.target == null || string.IsNullOrEmpty(ev.methodName))
|
|
{
|
|
Add(list, ev.mCachedCallback, oneShot);
|
|
}
|
|
else
|
|
{
|
|
if (list == null)
|
|
{
|
|
return;
|
|
}
|
|
int i = 0;
|
|
for (int count = list.Count; i < count; i++)
|
|
{
|
|
EventDelegate eventDelegate = list[i];
|
|
if (eventDelegate != null && eventDelegate.Equals(ev))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
EventDelegate eventDelegate2 = new EventDelegate(ev.target, ev.methodName);
|
|
eventDelegate2.oneShot = oneShot;
|
|
if (ev.mParameters != null && ev.mParameters.Length != 0)
|
|
{
|
|
eventDelegate2.mParameters = new Parameter[ev.mParameters.Length];
|
|
for (int j = 0; j < ev.mParameters.Length; j++)
|
|
{
|
|
eventDelegate2.mParameters[j] = ev.mParameters[j];
|
|
}
|
|
}
|
|
list.Add(eventDelegate2);
|
|
}
|
|
}
|
|
}
|