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:
@@ -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;
|
||||
object inst;
|
||||
try { inst = Activator.CreateInstance(t); }
|
||||
catch { return null; }
|
||||
_components[t] = inst;
|
||||
if (inst is Component comp) comp._go = this;
|
||||
WireComponentFields(inst);
|
||||
return inst;
|
||||
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(ty); }
|
||||
catch { return null; }
|
||||
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