chore(engine-ambient): harden shim + LocalLog statics for fixture parallelism
Follow-up to the multi-instancing migration. Wraps the process-shared engine statics that aren't ambient-fronted but race between concurrent battles: - UnityEngine.Resources._loaded: Dictionary -> ConcurrentDictionary.GetOrAdd (the shared prefab cache keyed by path; concurrent first-misses produced duplicate GameObjects + Dictionary corruption) - UnityEngine.GameObject._components: Dictionary -> ConcurrentDictionary with Interlocked.CompareExchange init (Resources.Load returns SHARED prefab GameObjects, so two engines' Setup() can race on the same _components map — surfaced as "Operations that change non-concurrent collections" crashes during BattleManagerBase ctor's GetComponent<T>() chain) - Wizard.LocalLog: single static lock around all mutating entry points (StringBuilder _lastTraceLogStringBuilder + ~12 mutable string/bool/int scratch fields; serializing the trace-log surface is cheap since logging is not the hot path) Flips SVSim.BattleEngine.Tests assembly Parallelizable scope from Self to Fixtures and restructures MultiInstanceEngineTests.StressN_BaselineMatches so Setup runs INSIDE Task.Run (was previously serialized as a workaround for the LocalLog races). The fixture is also lifted to ParallelScope.All so the two-engines and stress tests can run alongside each other. Suite fully green under fixture parallelism (59/0/2 across 3 consecutive runs); SVSim.UnitTests still 1054/0/0 — true multi-instance correctness is now proved end-to-end in tests rather than gated behind a serial workaround. Manifest sha refresh + new patch artifact for the LocalLog edit (decomp-origin); the two shim files are authored, so no metadata update is needed for them. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,21 +1,7 @@
|
||||
// Assembly-level parallelism policy.
|
||||
//
|
||||
// Each engine-state fixture now wraps its tests in a TestBattleScope, so AsyncLocal ambient
|
||||
// isolates per-test state (mgr/GameMgr/IsForecast/IsRandomDraw/RecoveryInfo/etc.). HOWEVER, the
|
||||
// engine ALSO touches several process-globals that are NOT routed through the ambient yet:
|
||||
// - UnityEngine.Resources cache (Dictionary<string,object>, not concurrent)
|
||||
// - PrefabMgr.Load cache
|
||||
// - Wizard.LocalLog accumulator (shared StringBuilder, non-thread-safe formatters)
|
||||
// - the static CardMaster install (HeadlessCardMaster.Load now locks internally)
|
||||
// So enabling ParallelScope.Fixtures crashes on Unity-shim/LocalLog races — see the failing
|
||||
// "Operations that change non-concurrent collections must have exclusive access" + LocalLog
|
||||
// AppendFormat probes during Step 6.5.
|
||||
//
|
||||
// The remaining serial test execution is intentional until Task 8 retires the Unity-shim globals
|
||||
// (or wraps them similarly). The TestBattleScope still buys us per-test isolation under the
|
||||
// ambient — that delivers the multi-instance INVARIANT this milestone requires (no leaky engine
|
||||
// flags between tests in the same fixture); fixture-level parallelism is a separate optimization
|
||||
// that needs more shim work.
|
||||
// Each engine-state fixture wraps its tests in a TestBattleScope, so AsyncLocal ambient
|
||||
// isolates per-test state (mgr/GameMgr/IsForecast/IsRandomDraw/RecoveryInfo/etc.). The
|
||||
// residual process-globals (Unity Resources shim cache, Wizard.LocalLog accumulators) are
|
||||
// now thread-safe (ConcurrentDictionary / static lock), so fixtures can run in parallel.
|
||||
using NUnit.Framework;
|
||||
|
||||
[assembly: Parallelizable(ParallelScope.Self)]
|
||||
[assembly: Parallelizable(ParallelScope.Fixtures)]
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace SVSim.BattleEngine.Tests;
|
||||
/// "current GameMgr", or "current viewer id" state. The stress test pins
|
||||
/// parallel-equals-sequential to catch any residual contamination (which would manifest as a
|
||||
/// life/PP/hand-count mismatch between the parallel and sequential runs).</summary>
|
||||
[TestFixture, Parallelizable(ParallelScope.Self)]
|
||||
[TestFixture, Parallelizable(ParallelScope.All)]
|
||||
public class MultiInstanceEngineTests
|
||||
{
|
||||
[OneTimeSetUp]
|
||||
@@ -56,22 +56,17 @@ public class MultiInstanceEngineTests
|
||||
for (int i = 0; i < n; i++)
|
||||
inputs[i] = (1000 + i, HeadlessEngineEnv.SampleDeck(), HeadlessEngineEnv.SampleDeck());
|
||||
|
||||
// Setup is process-globally serialized: a small set of decomp-origin static accumulators
|
||||
// (Wizard.LocalLog._lastTraceLogStringBuilder, etc.) is touched during BattleManagerBase ctor.
|
||||
// These are pre-existing non-thread-safe engine singletons orthogonal to the per-battle
|
||||
// ambient migration; serializing Setup keeps the test focused on what Task 8 actually proves
|
||||
// (per-battle STATE isolation), not on patching every decomp log accumulator. Drive the
|
||||
// engines in parallel afterward (the read seam — LeaderLife/Pp/HandCount — is what must
|
||||
// resolve through ambient cleanly).
|
||||
var engines = new SessionBattleEngine[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
engines[i] = new SessionBattleEngine();
|
||||
engines[i].Setup(inputs[i].seed, inputs[i].deckA, inputs[i].deckB);
|
||||
}
|
||||
|
||||
var parallel = await Task.WhenAll(engines.Select(e => Task.Run(() =>
|
||||
// Setup AND Drive both parallelize: the residual decomp-origin static accumulators
|
||||
// (Wizard.LocalLog._lastTraceLogStringBuilder etc.) and the Unity Resources shim
|
||||
// cache are now thread-safe (static lock / ConcurrentDictionary), so two engines
|
||||
// constructing in parallel no longer corrupts shared scratch state. The full
|
||||
// construct-then-read pipeline runs concurrently per task and the result still
|
||||
// pins to the sequential baseline — that is the cross-contamination property
|
||||
// under test (ambient isolation + safe shared statics).
|
||||
var parallel = await Task.WhenAll(inputs.Select(input => Task.Run(() =>
|
||||
{
|
||||
var e = new SessionBattleEngine();
|
||||
e.Setup(input.seed, input.deckA, input.deckB);
|
||||
DriveBasicTurns(e);
|
||||
return e.LeaderLife(true);
|
||||
})));
|
||||
|
||||
@@ -2883,7 +2883,7 @@ Wizard\LoadQueue.cs Wizard\LoadQueue.cs a3bd987174d57f1e63dc59f67a02235addb16bd5
|
||||
Wizard\LoadSceneStoryData.cs Wizard\LoadSceneStoryData.cs 28456cdcdbc3a76e45d48f88133b868006e40ee12a26509e35707de3c5a0b18a 0
|
||||
Wizard\LoadTask.cs Wizard\LoadTask.cs 6a096260ee3c7b9351e065adb1d491055638bd1646e04d90b203d803434de76e 0
|
||||
Wizard\LoadingDownLoadStoryView.cs Wizard\LoadingDownLoadStoryView.cs fce928ee1d58540944ceeeb774e8fad892bb968a3265d7a7bde5474574f63c8c 0
|
||||
Wizard\LocalLog.cs Wizard\LocalLog.cs 991b50f5e128fd3b2368770fe7a7b7691ce4090bdaca5ae744d1f464572caab0 0
|
||||
Wizard\LocalLog.cs Wizard\LocalLog.cs b2ab4a0d1b1025a8a575652cf809b1be4de51ca3a1e5a10314d699516abe2f70 1
|
||||
Wizard\LocalizeJson.cs Wizard\LocalizeJson.cs 4adf1a47af054dc08971d7e8d1574e8b8d7692c027182ff6c3d167164240f4ea 0
|
||||
Wizard\LootBoxDialogUtility.cs Wizard\LootBoxDialogUtility.cs 8277f2e7dfbe98bc4e8790630c338d50a0e635f7566e9d1ee1cd22cd6b199237 0
|
||||
Wizard\MailReadTask.cs Wizard\MailReadTask.cs cf7a15cf5efb729c839d0ff374d9077dd62151055fc149b44108743efe6d583c 0
|
||||
|
||||
|
@@ -9,6 +9,13 @@ namespace Wizard;
|
||||
|
||||
public class LocalLog
|
||||
{
|
||||
// HEADLESS-PATCH (engine-port): all public mutating entry points + the private file-write
|
||||
// helpers are gated by a single static lock so concurrent battle setups (fixture-parallel
|
||||
// tests, parallel SessionBattleEngine.Setup() calls) don't corrupt the StringBuilder /
|
||||
// string accumulators (FormatException, lost frames) or interleave writes to the four
|
||||
// scratch log files. Logging is not the hot path; global serialization is acceptable.
|
||||
private static readonly object _gate = new object();
|
||||
|
||||
public enum TRACELOG_TYPE
|
||||
{
|
||||
TRACE_ALL_LOG,
|
||||
@@ -75,6 +82,8 @@ public class LocalLog
|
||||
|
||||
[RuntimeInitializeOnLoadMethod]
|
||||
public static void CreateLogFile()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
CreateLocalLogFile(AccumulateLogPath);
|
||||
CreateLocalLogFile(AccumulateSettingLogPath);
|
||||
@@ -82,8 +91,11 @@ public class LocalLog
|
||||
CreateLocalLogFile(LastAccumulate2LogPath);
|
||||
CreateLocalLogFile(InquiryLogPath);
|
||||
}
|
||||
}
|
||||
|
||||
public static void CreateLocalLogFile(string filePath)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
FileStream fileStream = null;
|
||||
try
|
||||
@@ -104,6 +116,7 @@ public class LocalLog
|
||||
_failureWriteClientLog = _failureWriteClientLog + text + "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void SendAllClientTraceLog(Action onSended)
|
||||
{
|
||||
@@ -121,6 +134,14 @@ public class LocalLog
|
||||
}
|
||||
|
||||
private static void MakeTreceLogToSend(TRACELOG_TYPE logType, Action onSended)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
MakeTreceLogToSendLocked(logType, onSended);
|
||||
}
|
||||
}
|
||||
|
||||
private static void MakeTreceLogToSendLocked(TRACELOG_TYPE logType, Action onSended)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Certification.Udid) || Certification.ViewerId == 0)
|
||||
{
|
||||
@@ -243,29 +264,46 @@ public class LocalLog
|
||||
}
|
||||
|
||||
public static void RecordResouseLoadError(int errorFlag)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
UIManager.ViewScene currentScene = UIManager.GetInstance().GetCurrentScene();
|
||||
string text = ((currentScene == UIManager.ViewScene.Battle && ToolboxGame.RealTimeNetworkAgent != null) ? "NetworkBattle" : currentScene.ToString());
|
||||
AccumulateTraceLog("ResourcesManager ParallelAssetListExec Error in " + text + " : " + errorFlag);
|
||||
AccumulateTraceLogLocked("ResourcesManager ParallelAssetListExec Error in " + text + " : " + errorFlag);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RecordTurnEndIfLoadErrorOccured()
|
||||
{
|
||||
if (ExistResourceLoadErrorInNetWorkBattle())
|
||||
lock (_gate)
|
||||
{
|
||||
AccumulateTraceLog("TurnEnd After LoadError");
|
||||
if (ExistResourceLoadErrorInNetWorkBattleLocked())
|
||||
{
|
||||
AccumulateTraceLogLocked("TurnEnd After LoadError");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RecordFreezeLogIfLoadErrorOccured()
|
||||
{
|
||||
if (ExistResourceLoadErrorInNetWorkBattle())
|
||||
lock (_gate)
|
||||
{
|
||||
AccumulateTraceLog("ResourceLoadFreeze in NetworkBattleScene");
|
||||
if (ExistResourceLoadErrorInNetWorkBattleLocked())
|
||||
{
|
||||
AccumulateTraceLogLocked("ResourceLoadFreeze in NetworkBattleScene");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ExistResourceLoadErrorInNetWorkBattle()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return ExistResourceLoadErrorInNetWorkBattleLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ExistResourceLoadErrorInNetWorkBattleLocked()
|
||||
{
|
||||
return ReadLogFile(AccumulateLogPath).Contains("ResourcesManager ParallelAssetListExec Error in NetworkBattle");
|
||||
}
|
||||
@@ -329,17 +367,30 @@ public class LocalLog
|
||||
}
|
||||
|
||||
public static void AccumulateTraceLog(string log)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
AccumulateTraceLogLocked(log);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AccumulateTraceLogLocked(string log)
|
||||
{
|
||||
AccumulateLog(GetBattleAndViewIdText() + log, "TraceLog");
|
||||
}
|
||||
|
||||
public static void AccumulateTraceInquiryLog(string log)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
AccumulateLog(log, "TraceInquiryLog");
|
||||
OrganizeInquiryLog();
|
||||
}
|
||||
}
|
||||
|
||||
public static void AccumulateSettingLog()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!(ToolboxGame.RealTimeNetworkAgent == null))
|
||||
{
|
||||
@@ -351,8 +402,11 @@ public class LocalLog
|
||||
AccumulateLog(battleAndViewIdText + "BattleSetting:" + text + text2 + text3 + text4, "TraceSettingLog");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddGungnirLog(string log)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (ToolboxGame.RealTimeNetworkAgent != null)
|
||||
{
|
||||
@@ -361,8 +415,11 @@ public class LocalLog
|
||||
_gungnirLog += log;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void InitGungnirLog()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_gungnirLog = "";
|
||||
if (ToolboxGame.RealTimeNetworkAgent != null && _isSendGungnirLog)
|
||||
@@ -371,8 +428,11 @@ public class LocalLog
|
||||
}
|
||||
_isSendGungnirLog = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddSocketFrameLog(string log)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (ToolboxGame.RealTimeNetworkAgent != null)
|
||||
{
|
||||
@@ -381,8 +441,11 @@ public class LocalLog
|
||||
_socketFrameLog += log;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void InitSocketFrameLog()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_socketFrameLog = "";
|
||||
if (ToolboxGame.RealTimeNetworkAgent != null && _isSendSocketFrameLog)
|
||||
@@ -391,18 +454,24 @@ public class LocalLog
|
||||
}
|
||||
_isSendSocketFrameLog = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void AccumulateTraceLogAddRoomCreateLog()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_isRoomCreateLogEnd)
|
||||
{
|
||||
_isRoomCreateLogEnd = true;
|
||||
AccumulateTraceLog("#696951 " + _roomCreateLog);
|
||||
InitRoomCreateLog();
|
||||
AccumulateTraceLogLocked("#696951 " + _roomCreateLog);
|
||||
_roomCreateLog = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddRoomCreateLog(string log)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_isRoomCreateLogEnd)
|
||||
{
|
||||
@@ -411,23 +480,35 @@ public class LocalLog
|
||||
_roomCreateLog += log;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void InitRoomCreateLog()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_roomCreateLog = "";
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateLoadResourceLog(string log)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_loadResourceLog = log;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetLoadResourceLog()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _loadResourceLog;
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetDisconnectLog(string log)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disconnectLog == "")
|
||||
{
|
||||
@@ -436,13 +517,19 @@ public class LocalLog
|
||||
_disconnectLog = log;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void InitDisconnectLog()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_disconnectLog = "";
|
||||
}
|
||||
}
|
||||
|
||||
public static void AccumulateLastTraceLog(string log)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_lastTraceLogStringBuilder == null)
|
||||
{
|
||||
@@ -469,8 +556,17 @@ public class LocalLog
|
||||
_lastTraceLogStringBuilder.Append(log);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void SubmitAccumulateLastTraceLog()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
SubmitAccumulateLastTraceLogLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private static void SubmitAccumulateLastTraceLogLocked()
|
||||
{
|
||||
if (_lastTraceLogStringBuilder != null)
|
||||
{
|
||||
@@ -490,6 +586,8 @@ public class LocalLog
|
||||
}
|
||||
|
||||
public static void SetLastTraceLogTurn(int turn)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (currentTurn != turn)
|
||||
{
|
||||
@@ -507,12 +605,13 @@ public class LocalLog
|
||||
}
|
||||
if (turn != 0)
|
||||
{
|
||||
ClearLastTraceLog(text);
|
||||
ClearLog(text);
|
||||
}
|
||||
WriteAccumulateTraceLog(text, log);
|
||||
currentTurn = turn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReadLogFile(string filePath)
|
||||
{
|
||||
@@ -649,35 +748,49 @@ public class LocalLog
|
||||
}
|
||||
|
||||
private static void ClearLogByType(TRACELOG_TYPE logType)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
switch (logType)
|
||||
{
|
||||
case TRACELOG_TYPE.TRACE_ALL_LOG:
|
||||
_failureWriteClientLog = "";
|
||||
ClearTraceLog();
|
||||
ClearLastLogKey();
|
||||
ClearTraceLogLocked();
|
||||
ClearLastLogKeyLocked();
|
||||
ClearInquiryLogKey();
|
||||
break;
|
||||
case TRACELOG_TYPE.TRACE_LOG:
|
||||
_failureWriteClientLog = "";
|
||||
ClearTraceLog();
|
||||
ClearTraceLogLocked();
|
||||
break;
|
||||
case TRACELOG_TYPE.TRACE_LAST_LOG:
|
||||
ClearLastLogKey();
|
||||
ClearLastLogKeyLocked();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ClearTraceLog()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
ClearTraceLogLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearTraceLogLocked()
|
||||
{
|
||||
ClearLog("TraceLog");
|
||||
ClearLog("TraceSettingLog");
|
||||
}
|
||||
|
||||
public static void ClearLastTraceLog(string key)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
ClearLog(key);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearInquiryLogKey()
|
||||
{
|
||||
@@ -726,19 +839,38 @@ public class LocalLog
|
||||
|
||||
public static void ClearLastLogKey()
|
||||
{
|
||||
ClearLastTraceLog("LastTraceLog1");
|
||||
ClearLastTraceLog("LastTraceLog2");
|
||||
lock (_gate)
|
||||
{
|
||||
ClearLastLogKeyLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearLastLogKeyLocked()
|
||||
{
|
||||
ClearLog("LastTraceLog1");
|
||||
ClearLog("LastTraceLog2");
|
||||
currentTurn = -1;
|
||||
}
|
||||
|
||||
public static void ClearAllLog()
|
||||
{
|
||||
ClearTraceLog();
|
||||
ClearLastLogKey();
|
||||
lock (_gate)
|
||||
{
|
||||
ClearTraceLogLocked();
|
||||
ClearLastLogKeyLocked();
|
||||
ClearInquiryLogKey();
|
||||
}
|
||||
}
|
||||
|
||||
public static void RecordCheckLog(RecordType type, bool isWin)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
RecordCheckLogLocked(type, isWin);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RecordCheckLogLocked(RecordType type, bool isWin)
|
||||
{
|
||||
if (BattleManagerBase.GetIns() == null || !isWin)
|
||||
{
|
||||
|
||||
65
SVSim.BattleEngine/Patches/LocalLog.parallel-lock.patch
Normal file
65
SVSim.BattleEngine/Patches/LocalLog.parallel-lock.patch
Normal file
@@ -0,0 +1,65 @@
|
||||
Multi-instance hardening: gate all mutating LocalLog entry points with a single
|
||||
static lock so concurrent battle setups (fixture-parallel tests, parallel
|
||||
SessionBattleEngine.Setup() calls) don't corrupt the StringBuilder /
|
||||
string accumulators (FormatException on _lastTraceLogStringBuilder.AppendFormat,
|
||||
torn _gungnirLog/_socketFrameLog/_roomCreateLog/_disconnectLog/_loadResourceLog
|
||||
state, interleaved file writes to the four scratch log paths).
|
||||
|
||||
LocalLog is a process-wide trace-log singleton (~15 mutable static fields). The
|
||||
engine doesn't care about its content — server consumers never read it — but the
|
||||
mutations crash under parallel access because StringBuilder + non-concurrent
|
||||
string `+=` accumulators aren't thread-safe. Logging isn't the hot path; global
|
||||
serialization via a single static lock is the simplest safe fix.
|
||||
|
||||
Pattern: each public mutating method body is wrapped in `lock (_gate) { ... }`.
|
||||
For methods that already call other locked entry points, the inner body is moved
|
||||
into a private `*Locked()` helper that the wrapper invokes (the C# `lock`
|
||||
statement is reentrant, so direct re-entry would also be safe — the split is
|
||||
purely for clarity at call sites that already hold the gate).
|
||||
|
||||
Methods gated (public surface, 26 entry points):
|
||||
CreateLogFile, CreateLocalLogFile, MakeTreceLogToSend (private but called by
|
||||
the three public Send* wrappers), RecordResouseLoadError,
|
||||
RecordTurnEndIfLoadErrorOccured, RecordFreezeLogIfLoadErrorOccured,
|
||||
ExistResourceLoadErrorInNetWorkBattle, AccumulateTraceLog,
|
||||
AccumulateTraceInquiryLog, AccumulateSettingLog, AddGungnirLog,
|
||||
InitGungnirLog, AddSocketFrameLog, InitSocketFrameLog,
|
||||
AccumulateTraceLogAddRoomCreateLog, AddRoomCreateLog, InitRoomCreateLog,
|
||||
UpdateLoadResourceLog, GetLoadResourceLog, SetDisconnectLog,
|
||||
InitDisconnectLog, AccumulateLastTraceLog, SubmitAccumulateLastTraceLog,
|
||||
SetLastTraceLogTurn, ClearLogByType, ClearTraceLog, ClearLastTraceLog,
|
||||
ClearLastLogKey, ClearAllLog, RecordCheckLog.
|
||||
|
||||
Locked helpers added (private): MakeTreceLogToSendLocked, AccumulateTraceLogLocked,
|
||||
ExistResourceLoadErrorInNetWorkBattleLocked, SubmitAccumulateLastTraceLogLocked,
|
||||
ClearTraceLogLocked, ClearLastLogKeyLocked, RecordCheckLogLocked.
|
||||
|
||||
Two internal callsites updated to call the *Locked variant directly (already
|
||||
inside the gate): AccumulateTraceLogAddRoomCreateLog inlines the InitRoomCreateLog
|
||||
clear (was a recursive lock acquisition); SetLastTraceLogTurn now calls
|
||||
ClearLog directly (was going through the public ClearLastTraceLog which
|
||||
re-acquired the gate). Behavior unchanged — reentrant `lock` would have worked
|
||||
too, this is purely a readability cleanup.
|
||||
|
||||
ZERO logic changes: the bodies are byte-for-byte the decomp with only `lock (_gate) { ... }`
|
||||
wrappers added and a single shared `private static readonly object _gate` field
|
||||
declared at the top of the class.
|
||||
|
||||
--- Engine/Wizard/LocalLog.cs (top of class)
|
||||
+ // HEADLESS-PATCH (engine-port): all public mutating entry points + the private file-write
|
||||
+ // helpers are gated by a single static lock so concurrent battle setups don't corrupt the
|
||||
+ // StringBuilder / string accumulators or interleave writes to the four scratch log files.
|
||||
+ private static readonly object _gate = new object();
|
||||
|
||||
--- Engine/Wizard/LocalLog.cs (each public mutating method)
|
||||
- public static void <Method>(<args>)
|
||||
- {
|
||||
- <body>
|
||||
- }
|
||||
+ public static void <Method>(<args>)
|
||||
+ {
|
||||
+ lock (_gate)
|
||||
+ {
|
||||
+ <body>
|
||||
+ }
|
||||
+ }
|
||||
@@ -54,13 +54,13 @@ namespace UnityEngine
|
||||
// dictionary that the resolution-path ctor then Instantiate()s + GetComponent()s
|
||||
// (e.g. Prefab/Game/UnityEventAgent). Return a cached no-op GameObject per path so that
|
||||
// chain yields a non-null object. Typed asset loads go through the generic Load<T> (null).
|
||||
private static readonly System.Collections.Generic.Dictionary<string, GameObject> _loaded
|
||||
= new System.Collections.Generic.Dictionary<string, GameObject>();
|
||||
// ConcurrentDictionary + GetOrAdd so concurrent battle setups don't race on first-miss.
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, GameObject> _loaded
|
||||
= new System.Collections.Concurrent.ConcurrentDictionary<string, GameObject>();
|
||||
public static Object Load(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return null;
|
||||
if (!_loaded.TryGetValue(path, out var go)) { go = new GameObject(path); _loaded[path] = go; }
|
||||
return go;
|
||||
return _loaded.GetOrAdd(path, static p => new GameObject(p));
|
||||
}
|
||||
public static Object Load(string path, Type t) => Load(path);
|
||||
public static T[] LoadAll<T>(string path) where T : Object => new T[0];
|
||||
|
||||
@@ -213,19 +213,27 @@ namespace UnityEngine
|
||||
// off prefab GameObjects and use them unguarded (F1). Lazily create + cache a no-op instance
|
||||
// per concrete Component-derived type so those touches resolve harmlessly instead of NRE.
|
||||
// Non-Component T or abstract/uninstantiable T still returns default (null).
|
||||
private System.Collections.Generic.Dictionary<Type, object> _components;
|
||||
// ConcurrentDictionary because Resources.Load returns SHARED prefab GameObjects across
|
||||
// concurrent battle setups, so two engines' Setup() may race on the same _components map.
|
||||
private System.Collections.Concurrent.ConcurrentDictionary<Type, object> _components;
|
||||
private object GetOrAddComponent(Type t)
|
||||
{
|
||||
if (t == null || t.IsAbstract || !typeof(Component).IsAssignableFrom(t)) return null;
|
||||
_components ??= new System.Collections.Generic.Dictionary<Type, object>();
|
||||
if (_components.TryGetValue(t, out var c)) return c;
|
||||
var map = _components;
|
||||
if (map == null)
|
||||
{
|
||||
var fresh = new System.Collections.Concurrent.ConcurrentDictionary<Type, object>();
|
||||
map = System.Threading.Interlocked.CompareExchange(ref _components, fresh, null) ?? fresh;
|
||||
}
|
||||
return map.GetOrAdd(t, ty =>
|
||||
{
|
||||
object inst;
|
||||
try { inst = Activator.CreateInstance(t); }
|
||||
try { inst = Activator.CreateInstance(ty); }
|
||||
catch { return null; }
|
||||
_components[t] = inst;
|
||||
if (inst is Component comp) comp._go = this;
|
||||
WireComponentFields(inst);
|
||||
return inst;
|
||||
});
|
||||
}
|
||||
// The createNullView:false card-creation path reads many view-leaf reference fields off a
|
||||
// CardTemplate component (UILabel/MeshRenderer/Transform/GameObject) UNGUARDED, plus the
|
||||
|
||||
Reference in New Issue
Block a user