177 lines
8.9 KiB
C#
177 lines
8.9 KiB
C#
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
// Hit1LookAtTarget.cs — 1타 클립에 LookAtTarget 이벤트 런타임 주입 (WL-816v 이슈 ① · Lead 결정 (b))
|
||
|
|
//
|
||
|
|
// ■ 관측된 결함 (실측 2026-09-15 · AnimationUtility.GetAnimationEvents)
|
||
|
|
// Attack1_S_10101 : ShowEffect(0.2071) · Move(0.2807) · Projectile(0.3117) ← LookAtTarget 없음
|
||
|
|
// Attack2_S_10101 : **LookAtTarget(0.0000)** · ShowEffect · Move · Projectile
|
||
|
|
// Attack3_S_10101 : **LookAtTarget(0.0000)** · ShowEffect · Move · Projectile
|
||
|
|
// → 1타만 대상 방향으로 돌지 않은 채 나간다. 2·3타는 클립 시작에 정렬된다.
|
||
|
|
//
|
||
|
|
// ■ 이 파일이 하는 것 — 원본 훅 0 · FBX/meta 0줄
|
||
|
|
// Play 중에만, 같은 컨트롤러 안의 2·3타 클립에서 LookAtTarget 이벤트를 **템플릿으로 읽어**
|
||
|
|
// (함수명·시간·파라미터를 코드에 넣지 않는다 · C45) 1타 클립 인스턴스에 AddEvent 로 1회 주입하고
|
||
|
|
// Animator.Rebind() 로 바인딩을 갱신한다. 임포트된 FBX 서브클립은 .meta 가 SOT 이고 런타임
|
||
|
|
// AddEvent 는 메모리 인스턴스만 바꾸므로 에셋 파일은 변하지 않는다(게이트에서 git status 로 실측).
|
||
|
|
//
|
||
|
|
// ■ 값(C45) 전부 WLCombatCoreSettings. hit1LookAtTarget = 0 이면 러너 자체가 뜨지 않는다(C8 롤백).
|
||
|
|
//
|
||
|
|
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(Actor/PCActor 가 Assembly-CSharp 에 있다).
|
||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
using UnityEngine;
|
||
|
|
using WL.Combat.Core;
|
||
|
|
|
||
|
|
namespace WL.Combat.Auto
|
||
|
|
{
|
||
|
|
/// <summary>1타 클립에 LookAtTarget 이벤트를 런타임 주입한다. 클래스당 1회.</summary>
|
||
|
|
public static class Hit1LookAtTarget
|
||
|
|
{
|
||
|
|
static WLCombatCoreSettings Cfg { get { return WLCombatCoreSettings.Instance; } }
|
||
|
|
|
||
|
|
static bool Active
|
||
|
|
{
|
||
|
|
get
|
||
|
|
{
|
||
|
|
var c = Cfg;
|
||
|
|
return c != null && c.hit1LookAtTarget;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static float s_nextPoll;
|
||
|
|
static int s_doneClassId = -1;
|
||
|
|
static bool s_faulted;
|
||
|
|
|
||
|
|
/// <summary>마지막 주입 결과(검증 읽기 전용 · 게임 로직은 쓰지 않는다).</summary>
|
||
|
|
public static string LastResult = "(미실행)";
|
||
|
|
|
||
|
|
/// <summary>주입한 클립 수 누계(검증용).</summary>
|
||
|
|
public static int InjectedCount;
|
||
|
|
|
||
|
|
internal static void Tick()
|
||
|
|
{
|
||
|
|
if (s_faulted || !Active) return;
|
||
|
|
var cfg = Cfg;
|
||
|
|
float now = Time.unscaledTime;
|
||
|
|
if (now < s_nextPoll) return;
|
||
|
|
s_nextPoll = now + Mathf.Max(0.02f, cfg.hit1PollSeconds);
|
||
|
|
|
||
|
|
try { Poll(cfg); }
|
||
|
|
catch (System.Exception ex) { s_faulted = true; LastResult = "중단(예외) — " + ex.Message; }
|
||
|
|
}
|
||
|
|
|
||
|
|
static void Poll(WLCombatCoreSettings cfg)
|
||
|
|
{
|
||
|
|
var pc = MyValue.MyPC;
|
||
|
|
if (DSUtil.CheckNull(pc)) { s_doneClassId = -1; return; } // PC 없음(타이틀·로딩)
|
||
|
|
|
||
|
|
int classId = pc.Get_ID();
|
||
|
|
if (classId == s_doneClassId) return; // 이 클래스는 이미 처리
|
||
|
|
|
||
|
|
var animator = pc.m_animation;
|
||
|
|
if (animator == null || animator.runtimeAnimatorController == null) return;
|
||
|
|
|
||
|
|
int n = Inject(animator, classId, cfg.hit1ClipNamePattern);
|
||
|
|
if (n > 0) animator.Rebind(); // 새 이벤트를 바인딩에 반영
|
||
|
|
s_doneClassId = classId; // 0건이어도 재시도하지 않는다(이미 있는 경우)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 컨트롤러가 물고 있는 클립 중 이름이 <paramref name="pattern"/>({CID} 치환)과 같은 것에
|
||
|
|
/// LookAtTarget 이벤트를 주입한다. 템플릿(함수명·시간·파라미터)은 **같은 컨트롤러의 다른 클립**에서
|
||
|
|
/// 읽는다 — 코드 상수 0개(C45). 이미 같은 함수명 이벤트가 있으면 건드리지 않는다.
|
||
|
|
/// </summary>
|
||
|
|
/// <returns>주입한 클립 수</returns>
|
||
|
|
public static int Inject(Animator animator, int classId, string pattern)
|
||
|
|
{
|
||
|
|
if (animator == null || animator.runtimeAnimatorController == null) return 0;
|
||
|
|
return Inject(animator.runtimeAnimatorController.animationClips, classId, pattern);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>클립 배열 직접 주입(에디트 모드 프로브가 같은 경로를 태울 수 있게 공개).</summary>
|
||
|
|
public static int Inject(AnimationClip[] clips, int classId, string pattern)
|
||
|
|
{
|
||
|
|
if (clips == null || clips.Length == 0 || string.IsNullOrEmpty(pattern))
|
||
|
|
{ LastResult = "클립 없음 또는 패턴 비어 있음"; return 0; }
|
||
|
|
|
||
|
|
string wanted = pattern.Replace("{CID}", classId.ToString());
|
||
|
|
|
||
|
|
// ── ① 템플릿 — 같은 컨트롤러의 다른 클립에 이미 있는 LookAtTarget 이벤트 ──
|
||
|
|
AnimationEvent template = null;
|
||
|
|
string templateFrom = null;
|
||
|
|
var fn = Cfg != null ? Cfg.hit1EventFunctionName : null;
|
||
|
|
for (int i = 0; i < clips.Length && template == null; i++)
|
||
|
|
{
|
||
|
|
var c = clips[i];
|
||
|
|
if (c == null || c.name == wanted) continue;
|
||
|
|
var evs = c.events;
|
||
|
|
if (evs == null) continue;
|
||
|
|
for (int e = 0; e < evs.Length; e++)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrEmpty(fn) || evs[e].functionName != fn) continue;
|
||
|
|
template = evs[e]; templateFrom = c.name; break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (template == null)
|
||
|
|
{ LastResult = string.Format("템플릿 없음 — '{0}' 이벤트를 가진 클립이 컨트롤러에 없다(주입 안 함)", fn); return 0; }
|
||
|
|
|
||
|
|
// ── ② 대상 클립에 주입 ──
|
||
|
|
int added = 0, already = 0, found = 0;
|
||
|
|
for (int i = 0; i < clips.Length; i++)
|
||
|
|
{
|
||
|
|
var c = clips[i];
|
||
|
|
if (c == null || c.name != wanted) continue;
|
||
|
|
found++;
|
||
|
|
|
||
|
|
bool has = false;
|
||
|
|
var evs = c.events;
|
||
|
|
if (evs != null)
|
||
|
|
for (int e = 0; e < evs.Length && !has; e++) has = evs[e].functionName == template.functionName;
|
||
|
|
if (has) { already++; continue; }
|
||
|
|
|
||
|
|
c.AddEvent(new AnimationEvent
|
||
|
|
{
|
||
|
|
functionName = template.functionName,
|
||
|
|
time = Mathf.Clamp(template.time, 0f, Mathf.Max(c.length - 0.0001f, 0f)),
|
||
|
|
intParameter = template.intParameter,
|
||
|
|
stringParameter = template.stringParameter,
|
||
|
|
floatParameter = template.floatParameter,
|
||
|
|
messageOptions = template.messageOptions
|
||
|
|
});
|
||
|
|
added++;
|
||
|
|
}
|
||
|
|
|
||
|
|
InjectedCount += added;
|
||
|
|
LastResult = string.Format("class {0} · 대상 '{1}' {2}건(주입 {3} · 기존보유 {4}) · 템플릿 '{5}' from {6} t={7:F4}s int={8} str='{9}'",
|
||
|
|
classId, wanted, found, added, already, template.functionName, templateFrom, template.time,
|
||
|
|
template.intParameter, template.stringParameter);
|
||
|
|
return added;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────────────
|
||
|
|
// 런너 (숨김 GameObject 1개 · 플레이 모드 전용)
|
||
|
|
// ─────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
static Hit1LookAtTargetRunner s_runner;
|
||
|
|
|
||
|
|
internal static void EnsureRunner()
|
||
|
|
{
|
||
|
|
if (!Application.isPlaying || !DSUtil.CheckNull(s_runner)) return;
|
||
|
|
var go = new GameObject("[WL816v] Hit1LookAtTargetRunner");
|
||
|
|
go.hideFlags = HideFlags.HideAndDontSave;
|
||
|
|
s_runner = go.AddComponent<Hit1LookAtTargetRunner>();
|
||
|
|
}
|
||
|
|
|
||
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||
|
|
static void Boot()
|
||
|
|
{
|
||
|
|
if (Active) EnsureRunner(); // 꺼져 있으면 오브젝트 0(C8)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Hit1LookAtTarget 의 시간 축. 숨김 GameObject 1개 · 코루틴 0.</summary>
|
||
|
|
internal sealed class Hit1LookAtTargetRunner : MonoBehaviour
|
||
|
|
{
|
||
|
|
void Update() { Hit1LookAtTarget.Tick(); }
|
||
|
|
}
|
||
|
|
}
|