// ───────────────────────────────────────────────────────────────────────────── // WLStaleStaticReset.cs — Play 시작마다 「지난 Play 에서 파괴된 객체」를 가리키는 static 참조를 비운다 (PD #957 · 2026-10-21) // // PD 원문(#957): 「스킬 테스트맵을 실행할 때 에러 때문에 진행이 안되는거 같아 왜 그런지 실측해보고 수정해줘.」 // ■ 실측(2026-10-21): 스킬 테스트 씬 Play → stage=pc-fail · MissingReferenceException 'MoneyForm' destroyed @ MoneyForm.Set // ← TopMoney.Set ← ServerData 아이템 지급(`if (TopMoney.isIns) TopMoney.Ins.Set()`) = PC 생성(스타터 킷 지급) 단계에서 멈춤. // TestMap Play → LoadingUI.End_Loading(go 파괴) ← MobControlMgrList.Start(`if (LoadingUI.isIns)`). // 편집 모드 실측: TopMoney · LoadingUI · NewGameUI 의 isIns = True 인데 Ins = 파괴됨 · MoneyForm 목록 2개 모두 파괴됨. // ■ 원인: #947-2 가 Enter Play Mode 의 도메인 리로드를 껐다(EditorSettings · 테스트 씬 빠른 재시작). 원본 싱글턴(MonoBehaviourSingletonTemplate · // LoadingUI 등)은 Awake 에서 static Ins · isIns 를 세우고 지우지 않는다 → 앞 Play(다른 씬)의 객체가 Play 종료 때 파괴돼도 static 이 남아 // 다음 Play 의 `if (X.isIns) X.Ins…` 가 파괴된 객체를 건드린다(도메인 리로드가 켜져 있으면 static 이 초기화돼 생기지 않던 오류). // ■ 처리: Play 시작(SubsystemRegistration = 씬 Awake 전)에 Assembly-CSharp 형식(닫힌 제네릭 부모 포함)의 static 필드 중 // ① UnityEngine.Object 형인데 값이 파괴된 것 → null ② List 의 파괴된 항목 제거 // ③ 같은 형식의 static bool isIns 는 그 형식의 Ins 가 비었으면 false. // 살아 있는 객체(편집 모드 객체 · 에셋)는 건드리지 않는다 · 에디터 전용(빌드는 매번 새로 시작해 해당 없음) · readonly/const 제외 · // 실패는 조용히 건너뜀(LogError 금지 — 원본 ErrorLogHook 팝업) · 결과 = LastLog(정리한 것이 있으면 콘솔 Log 1줄). // ───────────────────────────────────────────────────────────────────────────── using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using UnityEngine; namespace WL.Start { public static class WLStaleStaticReset { public static int Cleared, ListItems, Flags, TypesScanned; public static float LastMs; public static string LastLog = ""; #if UNITY_EDITOR const BindingFlags SF = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void Run() { Cleared = ListItems = Flags = TypesScanned = 0; string err = ""; var sw = System.Diagnostics.Stopwatch.StartNew(); var names = new List(); try { var seen = new HashSet(); foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) { if (asm.GetName().Name != "Assembly-CSharp") continue; Type[] types; try { types = asm.GetTypes(); } catch (ReflectionTypeLoadException e) { types = e.Types; } foreach (var t0 in types) for (var t = t0; t != null && t.Assembly == asm; t = t.BaseType) // 닫힌 제네릭 부모(MonoBehaviourSingletonTemplate)의 static 은 형식마다 따로 있다 { if (t.ContainsGenericParameters || !seen.Add(t)) break; // 이미 본 형식이면 그 부모 사슬도 이미 봤다 Clean(t, names); } } } catch (Exception ex) { err = " · 실패 " + ex.Message; } LastMs = (float)sw.Elapsed.TotalMilliseconds; LastLog = "형식 " + TypesScanned + " · 참조 " + Cleared + " · 목록 항목 " + ListItems + " · isIns " + Flags + " · " + LastMs.ToString("F0") + " ms" + (names.Count > 0 ? " · " + string.Join(", ", names.GetRange(0, Mathf.Min(12, names.Count))) + (names.Count > 12 ? " 외 " + (names.Count - 12) : "") : "") + err; if (Cleared + ListItems + Flags > 0) Debug.Log("[WL957] 지난 Play 에서 파괴된 static 참조 정리 — " + LastLog); } static void Clean(Type t, List names) { FieldInfo[] fs; try { fs = t.GetFields(SF); } catch { return; } if (fs.Length == 0) return; TypesScanned++; FieldInfo isIns = null; bool hasIns = false, insGone = false; foreach (var f in fs) { if (f.IsLiteral || f.IsInitOnly) continue; var ft = f.FieldType; try { if (ft == typeof(bool)) { if (f.Name == "isIns") isIns = f; continue; } if (typeof(UnityEngine.Object).IsAssignableFrom(ft)) { var v = f.GetValue(null) as UnityEngine.Object; bool gone = (object)v != null && v == null; // 참조는 남았는데 Unity 객체는 파괴됨 if (gone) { f.SetValue(null, null); Cleared++; names.Add(Name(t) + "." + f.Name); } if (f.Name == "Ins") { hasIns = true; insGone = gone || (object)v == null; } continue; } if (ft.IsGenericType && ft.GetGenericTypeDefinition() == typeof(List<>) && typeof(UnityEngine.Object).IsAssignableFrom(ft.GetGenericArguments()[0])) { var list = f.GetValue(null) as IList; if (list == null) continue; int before = ListItems; for (int i = list.Count - 1; i >= 0; i--) { var o = list[i] as UnityEngine.Object; if ((object)o != null && o == null) { list.RemoveAt(i); ListItems++; } } if (ListItems > before) names.Add(Name(t) + "." + f.Name + "[" + (ListItems - before) + "]"); } } catch { } } if (isIns != null && hasIns && insGone) { try { if ((bool)isIns.GetValue(null)) { isIns.SetValue(null, false); Flags++; names.Add(Name(t) + ".isIns"); } } catch { } } } /// 닫힌 제네릭은 「MonoBehaviourSingletonTemplate<TopMoney>」 꼴로. static string Name(Type t) { if (!t.IsGenericType) return t.Name; var n = t.Name; int k = n.IndexOf('`'); if (k > 0) n = n.Substring(0, k); return n + "<" + string.Join(",", Array.ConvertAll(t.GetGenericArguments(), a => a.Name)) + ">"; } #endif } }