From 8060e1922f542b1bdeb6b9e968b15903a424f649 Mon Sep 17 00:00:00 2001 From: swrring Date: Tue, 8 Sep 2026 22:17:53 +0900 Subject: [PATCH] =?UTF-8?q?[WL-811a]=20=EC=A0=84=ED=88=AC=20=EC=95=A1?= =?UTF-8?q?=EC=85=98=20=EC=BD=94=EC=96=B4=20=ED=94=84=EB=A1=9C=EB=B8=8C=20?= =?UTF-8?q?(#811)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WL811a_Trace.cs — 전투 파이프라인 런타임 이벤트 순서·타이밍 트레이스(Play 전용 · 읽기 전용 · 원본/WL 코드 무수정). 설계안 = NerdNavisAi 공유/WL_병렬작업/reports/WL-811a_전투액션코어_설계안.md (A 실측 지도 · B 코어 설계 · C 웨이브 분해 · D PD 결정 표). 컴파일 게이트 oneshot PASS(재시도 1회 · 첫 오픈 재임포트로 1차 900 s 타임아웃). Co-Authored-By: Claude Fable 5.1 --- AgentScripts/WL811a_Trace.cs | 178 +++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 AgentScripts/WL811a_Trace.cs diff --git a/AgentScripts/WL811a_Trace.cs b/AgentScripts/WL811a_Trace.cs new file mode 100644 index 000000000..d3e617f91 --- /dev/null +++ b/AgentScripts/WL811a_Trace.cs @@ -0,0 +1,178 @@ +// 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); + } + } +}