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 () - { - - } + public static void () + { + lock (_gate) + { + + } + }