// ───────────────────────────────────────────────────────────────────────────── // WLLeadAutoEnter.cs — Lead 자동 검증용 타이틀 자동 진입 (원본 0줄 · 표식이 있을 때만) // // 2026-09-17 PD 「네가 Play 테스트할 때 타이틀 화면에 계속 대기하지 않도록 진입 프로세스를 개선해」. // Lead 파이프라인 스크립트가 Play 직전에 PlayerPrefs `WL_LeadAutoEnter = 1` 을 세우면, 타이틀 씬에서 이 컴포넌트가 // ① 로그인 버튼이 활성화될 때까지 기다렸다가 `OnClick_DevLogin()`(ID 비어 있으면 TestID) // ② `AllComplete` 가 되면 `OnClick_Screen()`(인게임 로드) // ③ 로그인 실패(`LoginFail`)면 3 s 뒤 재시도 · 120 s 넘으면 포기 // 를 대신 한다. 표식은 소비 즉시 지운다(PD 의 일반 Play 에는 영향 0). 원본 `TitleInfo` 의 private 필드는 리플렉션으로 읽기만 한다. // ───────────────────────────────────────────────────────────────────────────── using System.Collections; using System.Reflection; using UnityEngine; using UnityEngine.SceneManagement; namespace WL.Tools { public sealed class WLLeadAutoEnter : MonoBehaviour { public const string PrefKey = "WL_LeadAutoEnter"; public static string LastLog = ""; public static bool Running; /// 로그인 완료(AllComplete) 뒤 인게임으로 넘기기까지 기다리는 시간(s) — 사람이 화면을 터치하는 간격. /// 🔴 2026-09-17 실측: 완료 직후 바로 넘기면 원본 로그인 사슬의 **두 번째** 완료 응답이 타이틀 씬이 내려간 뒤 도착해 /// `TitleInfo.ServerComplete` 가 지워진 글자를 만져 MissingReferenceException 창이 떴다(재시도 0회에서도 재현). public static float EnterDelay = 8f; public static float AllCompleteAt = -1f; // 🔴 2026-09-17 실측: 표식(Boot)으로 만든 인스턴스와 프로브가 직접 만든 인스턴스가 **둘 다** DevLogin 을 불러 로그인 사슬이 2개 → // 두 번째 사슬의 토큰 저장이 씬 전환 뒤 실패해 「서버 에러 · 토큰 저장 실패」 창(원본 모달)이 인게임에 떴다. 인스턴스는 항상 하나만. static WLLeadAutoEnter s_instance; public static bool Exists { get { return s_instance != null; } } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] static void Boot() { if (PlayerPrefs.GetInt(PrefKey, 0) == 0) return; PlayerPrefs.SetInt(PrefKey, 0); PlayerPrefs.Save(); // 1회 소비 Create(); } /// 프로브가 직접 부를 때도 여기로 — 이미 있으면 만들지 않는다. public static bool Create() { if (s_instance != null) return false; var go = new GameObject("~WL_LeadAutoEnter"); go.hideFlags = HideFlags.DontSave; DontDestroyOnLoad(go); s_instance = go.AddComponent(); return true; } void Start() { if (s_instance != null && s_instance != this) { Destroy(gameObject); return; } s_instance = this; StartCoroutine(Run()); } void OnDestroy() { if (s_instance == this) s_instance = null; } IEnumerator Run() { Running = true; float t0 = Time.realtimeSinceStartup; var bf = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; FieldInfo fActive = typeof(TitleInfo).GetField("activeButtons", bf), fAll = typeof(TitleInfo).GetField("AllComplete", bf), fFail = typeof(TitleInfo).GetField("LoginFail", bf), fInput = typeof(TitleInfo).GetField("Input_ID", bf); bool loginCalled = false; float lastLogin = -99f; while (Time.realtimeSinceStartup - t0 < 120f) { if (SceneManager.GetActiveScene().name != "Title") { LastLog = "타이틀 아님(" + SceneManager.GetActiveScene().name + ") — 종료"; break; } var title = FindFirstObjectByType(); if (title == null) { yield return new WaitForSeconds(0.5f); continue; } bool all = fAll != null && (bool)fAll.GetValue(title); if (all) { if (AllCompleteAt < 0f) { AllCompleteAt = Time.realtimeSinceStartup; LastLog = "로그인 완료(" + (AllCompleteAt - t0).ToString("F1") + " s) → " + EnterDelay.ToString("F0") + " s 뒤 진입"; } if (Time.realtimeSinceStartup - AllCompleteAt < EnterDelay) { yield return new WaitForSeconds(0.5f); continue; } title.OnClick_Screen(); LastLog = "로그인 완료 " + (AllCompleteAt - t0).ToString("F1") + " s · 화면 터치 → 인게임 로드(" + (Time.realtimeSinceStartup - t0).ToString("F1") + " s)"; yield return new WaitForSeconds(2f); if (SceneManager.GetActiveScene().name != "Title") break; continue; } bool active = fActive != null && (bool)fActive.GetValue(title); int fail = fFail != null ? (int)fFail.GetValue(title) : 0; // 🔴 실측: 스토어 정보 대기 중에는 activeButtons 가 오래 false 인데 OnClick_DevLogin 은 그와 무관하게 동작한다(이전 수동 진입과 동일) // → 타이틀이 보이면 1 s 뒤 바로 호출한다. // 🔴 2026-09-17 실측: 8 s 마다 다시 부르면 서버 응답(ServerComplete)이 여러 번 오고, 마지막 응답이 인게임 전환 **뒤**에 도착해 // 원본 TitleInfo 가 지워진 타이틀 글자를 만져 MissingReferenceException 창이 떴다 → 재시도는 실패 표시가 있거나 90 s 무응답일 때만 // (실측: 개발 로그인 완료까지 40~45 s 걸린다 — 40 s 재시도도 같은 창을 띄웠다). bool first = !loginCalled && Time.realtimeSinceStartup - t0 > 1f; bool retry = loginCalled && (fail > 0 || Time.realtimeSinceStartup - lastLogin > 90f); if (first || retry) { var input = fInput != null ? fInput.GetValue(title) as TMPro.TMP_InputField : null; if (input != null && string.IsNullOrEmpty(input.text)) input.text = "TestID"; title.OnClick_DevLogin(); loginCalled = true; lastLogin = Time.realtimeSinceStartup; LastLog = "DevLogin 호출(" + (lastLogin - t0).ToString("F1") + " s · 버튼 " + active + " · 실패 " + fail + ")"; } yield return new WaitForSeconds(0.5f); } Running = false; Destroy(gameObject); } } }