Project_WL/AgentScripts/WL811a_Trace.cs

179 lines
10 KiB
C#

// 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_<class>_<HHmmss>.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<Host>();
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<int, double>();
var mobDead = new Dictionary<int, bool>();
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<WeaponTrailDriver>();
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<MobActor>(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<ProjectileBase>(FindObjectsInactive.Exclude, FindObjectsSortMode.None).Length;
}
static void RefreshMobs(Dictionary<int, double> hp, Dictionary<int, bool> dead)
{
foreach (var m in UnityEngine.Object.FindObjectsByType<MobActor>(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);
}
}
}