// WL811a_Trace.cs — #811a 전투 파이프라인 런타임 이벤트 순서·타이밍 트레이스 (Play 전용 · 읽기 전용 · 에셋/게임 상태 변경 0) // // unity command run_script --file AgentScripts/WL811a_Trace.cs --entry WL811a_Trace.Run --args '[8]' // 8 s 동안 관찰 (자동 교전이 콤보를 시작하면 기록) // unity command run_script --file AgentScripts/WL811a_Trace.cs --entry WL811a_Trace.RunForce --args '[8]' // 3 s 안에 공격이 없으면 Play_Attack(0) 1회 호출 // 결과: Screenshots_WL/WL811a/trace__.txt + Console // // 재는 것 (완료 기준 ⓒ · 프레임 폴링이라 해상도 = 1 프레임): // · Animator 상태(클립명 Attack1_S/2_S/3_S) 진입·이탈 시각(unscaled ms)·normalizedTime → 콤보 단계 // · WeaponTrailDriver.IsSwinging 상승 에지 → ShowEffect 이벤트(검기 창 시작) // · WeaponTrailDriver.MappedSpawnCount 증가 → 검기 매핑 이펙트 스폰 // · 활성 ProjectileBase 수 증가 → Projectile 이벤트 → Shoot_Projectile(히트박스 스폰) // · PC m_DamageRate 변화(리플렉션) → Hit(float) 이벤트 // · 몹 HP 감소 / IsDead 전이 → Get_Damage → Cal_Damage / Set_Die // · WLHitFeel.HitCount / FiredCount 증가 · Time.timeScale 전이 → Actor.cs:535 훅 → 히트스톱 // · DashDriver.DashCount 증가 → 대시 // 원본·WL 코드를 수정하지 않는다. 몹 HP 는 public Get_HP() · 나머지는 public 정적 카운터 · m_DamageRate 만 리플렉션 읽기. using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using System.Text; using UnityEngine; using WL.Combat; public static class WL811a_Trace { const string OutDir = "Screenshots_WL/WL811a"; public static void Run(float seconds) { Launch(seconds, false); } public static void RunForce(float seconds) { Launch(seconds, true); } static void Launch(float seconds, bool force) { var old = GameObject.Find("__wl811a_trace"); if (old != null) UnityEngine.Object.DestroyImmediate(old); var go = new GameObject("__wl811a_trace"); var h = go.AddComponent(); h.StartCoroutine(h.Co_Trace(seconds <= 0f ? 8f : seconds, force)); } public class Host : MonoBehaviour { static readonly BindingFlags BF = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; static string ClipName(Animator a) { if (a == null) return ""; var infos = a.GetCurrentAnimatorClipInfo(0); return infos.Length > 0 && infos[0].clip != null ? infos[0].clip.name : ""; } public IEnumerator Co_Trace(float seconds, bool force) { var sb = new StringBuilder(); sb.AppendLine("# WL811a 전투 파이프라인 트레이스 " + DateTime.Now.ToString("HH:mm:ss") + " 관찰 " + seconds.ToString("F1") + " s force=" + force); var me = MyValue.MyPC; if (me == null) { sb.AppendLine("!! MyValue.MyPC 없음 — Title→인게임 진입 후 실행"); Done(sb); yield break; } var anim = me.m_animation; var fiRate = typeof(Actor).GetField("m_DamageRate", BF); var feelSt = WL.Feel.WLHitFeel.Settings; sb.AppendLine("class=" + me.Get_ID() + " feel=" + (feelSt != null ? (feelSt.enabled ? "on" : "off") : "null") + " hitStop=" + (feelSt != null ? feelSt.hitStopDuration.ToString("F3") + "/" + feelSt.hitStopKillDuration.ToString("F3") : "-") + " timeScale0=" + Time.timeScale.ToString("F2")); sb.AppendLine("# 열: t(ms unscaled) | clip | nt | 사건"); float t0 = Time.unscaledTime; string lastClip = ClipName(anim); bool lastSwing = false; int lastSpawn = WeaponTrailDriver.MappedSpawnCount; int lastHit = WL.Feel.WLHitFeel.HitCount, lastFired = WL.Feel.WLHitFeel.FiredCount; int lastDash = DashDriver.DashCount; float lastScale = Time.timeScale; float lastRate = fiRate != null ? (float)fiRate.GetValue(me) : -1f; int lastProj = CountProjectiles(); var mobHp = new Dictionary(); var mobDead = new Dictionary(); RefreshMobs(mobHp, mobDead); int frame = 0, attackFrames = 0; bool sawAttack = false; float attackEndAt = -1f; while (Time.unscaledTime - t0 < seconds) { yield return null; frame++; float ms = (Time.unscaledTime - t0) * 1000f; string clip = ClipName(anim); float nt = anim != null ? anim.GetCurrentAnimatorStateInfo(0).normalizedTime % 1f : 0f; string pre = string.Format("{0,7:F0} | {1,-22} | {2,5:F3} | ", ms, clip, nt); if (clip != lastClip) { sb.AppendLine(pre + "상태 전이 " + lastClip + " → " + clip + (anim != null && anim.IsInTransition(0) ? " (transition)" : "")); lastClip = clip; } bool isAttack = clip.Contains("Attack"); if (isAttack) { sawAttack = true; attackFrames++; attackEndAt = -1f; } else if (sawAttack && attackEndAt < 0f) attackEndAt = Time.unscaledTime; var drv = me.GetComponent(); bool swing = drv != null && drv.IsSwinging; if (swing != lastSwing) { sb.AppendLine(pre + (swing ? "ShowEffect → 검기 창 시작 (IsSwinging=1)" : "검기 창 종료")); lastSwing = swing; } int spawn = WeaponTrailDriver.MappedSpawnCount; if (spawn != lastSpawn) { sb.AppendLine(pre + "검기 매핑 이펙트 스폰 +" + (spawn - lastSpawn)); lastSpawn = spawn; } if (fiRate != null) { float rate = (float)fiRate.GetValue(me); if (!Mathf.Approximately(rate, lastRate)) { sb.AppendLine(pre + "Hit(float) 이벤트 → m_DamageRate " + lastRate.ToString("F2") + " → " + rate.ToString("F2")); lastRate = rate; } } int proj = CountProjectiles(); if (proj > lastProj) sb.AppendLine(pre + "Projectile 이벤트 → 활성 ProjectileBase +" + (proj - lastProj) + " (총 " + proj + ")"); lastProj = proj; if (frame % 30 == 0) RefreshMobs(mobHp, mobDead); foreach (var m in UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None)) { int id = m.GetInstanceID(); double hp; if (!mobHp.TryGetValue(id, out hp)) { mobHp[id] = m.Get_HP(); mobDead[id] = m.IsDead(); continue; } double cur = m.Get_HP(); if (cur < hp) { sb.AppendLine(pre + "Get_Damage/Cal_Damage → " + m.name + " HP " + hp.ToString("F0") + " → " + cur.ToString("F0")); } mobHp[id] = cur; bool dead = m.IsDead(); if (dead && !mobDead[id]) sb.AppendLine(pre + "Set_Die → " + m.name + " 사망"); mobDead[id] = dead; } int hit = WL.Feel.WLHitFeel.HitCount, fired = WL.Feel.WLHitFeel.FiredCount; if (hit != lastHit || fired != lastFired) { sb.AppendLine(pre + "WLHitFeel.OnHit (Actor.cs:535) hit+" + (hit - lastHit) + " fired+" + (fired - lastFired)); lastHit = hit; lastFired = fired; } if (!Mathf.Approximately(Time.timeScale, lastScale)) { sb.AppendLine(pre + "timeScale " + lastScale.ToString("F2") + " → " + Time.timeScale.ToString("F2") + (Time.timeScale < 0.1f ? " (히트스톱 시작)" : " (복원)")); lastScale = Time.timeScale; } int dash = DashDriver.DashCount; if (dash != lastDash) { sb.AppendLine(pre + "DashDriver 대시 +" + (dash - lastDash)); lastDash = dash; } if (force && !sawAttack && Time.unscaledTime - t0 > 3f) { force = false; float aspd = (float)me.Get_StatInfo().Get_Stat(eStat.FinalAttackSpeed); sb.AppendLine(pre + "!! 3 s 동안 공격 없음 → Play_Attack(0, " + aspd.ToString("F2") + ") 강제 1회"); me.Play_Attack(0, aspd); } // 콤보가 끝난 뒤 0.6 s 여유를 두고 종료 (히트스톱 복원까지 포함) if (sawAttack && attackEndAt > 0f && Time.unscaledTime - attackEndAt > 0.6f) break; } sb.AppendLine("# 프레임 " + frame + " · 공격 상태 프레임 " + attackFrames + " · 공격 관측=" + sawAttack + " · timeScale 종료값 " + Time.timeScale.ToString("F2")); Done(sb); } static int CountProjectiles() { return UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None).Length; } static void RefreshMobs(Dictionary hp, Dictionary dead) { foreach (var m in UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None)) { int id = m.GetInstanceID(); if (!hp.ContainsKey(id)) { hp[id] = m.Get_HP(); dead[id] = m.IsDead(); } } } void Done(StringBuilder sb) { try { System.IO.Directory.CreateDirectory(OutDir); var me = MyValue.MyPC; string path = System.IO.Path.Combine(OutDir, "trace_" + (me != null ? me.Get_ID().ToString() : "none") + "_" + DateTime.Now.ToString("HHmmss") + ".txt"); System.IO.File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false)); Debug.Log("[WL811a] 저장 " + path); } catch (Exception e) { Debug.LogWarning("[WL811a] 저장 실패 " + e.Message); } Debug.Log(sb.ToString()); UnityEngine.Object.Destroy(gameObject); } } }