Project_WL/Assets/WL/Combat/Diagnostics/WLProjectileStart.cs

82 lines
4.9 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLProjectileStart.cs — 투사체 시작 위치 null 타깃 가드 (Q7 D-35 원인 지점)
//
// PD 지시 #813 · 발주서 WL-813x2 §1-2 (2026-09-09)
//
// ■ 실측 원인 (메인 에디터 Editor.log · Q7 R1 15:08 구간 · R1_13/R1_14 캡처 사이)
// NullReferenceException: Object reference not set to an instance of an object
// at ProjectileBase.Set (ProjectileData _data) [0x0026d] in Assets/Script/Character/Projectile/ProjectileBase.cs:233
// at Select_BodyonFire.Set (ProjectileData _data) in .../SelectSkill/Select_BodyonFire.cs:12
// at InGameInfo+<>c__DisplayClass31_0.<Load_Skill>b__0 (AsyncOperationHandle`1) in Assets/Script/Info/InGameInfo.cs:339
// at DelegateList`1[T].Invoke (Addressables · DelayedActionManager.LateUpdate)
// 2회째(15:23 R2b 구간)는 같은 줄에서 `SelectFairyBase.Set` 경유.
//
// ■ 왜 null 인가
// `InGameInfo.Load_Skill`(:333-340) 은 선택스킬 프리팹이 풀에 없을 때만 도는 **최초 1회 비동기 로드**다.
// Addressables 콜백이 다음 LateUpdate 로 밀리는 사이에 `_ptd.target`(=발사 시점의 몹)이 사라지면
// 콜백이 부르는 `ProjectileBase.Set` 안에서 target 이 null 이 된다.
// `Set` 은 :184 에서 `isNullTarget` 을 이미 계산해 :213 · :238 은 가드하지만,
// :229-236 의 시작 위치 switch 는 **`default:`(=None/Target)와 `TargetCenter` 를 가드하지 않는다**.
// → 존 클리어 카드(813o)·레벨업으로 런 도중 선택스킬이 새로 붙는 WL 런에서 특히 잘 걸린다.
//
// ■ 가드
// target 이 (Unity 오버로드 ==) null 이면 shooter 위치로 대체한다. shooter 마저 null 이면 현재 위치 유지.
// target 이 살아 있으면 **원본과 완전히 동일한 식**이다.
// SO `projectileNullTargetGuard = 0` 이면 원본 동작 그대로(=역참조)로 되돌린다(C8).
//
// 🔴 `??` · `?.` 를 쓰지 않는다 — UnityEngine.Object 의 가짜 null 을 못 거른다(813y 교훈).
// 🔴 Debug.LogError 금지(813z) — 진단 로그는 Debug.Log 1줄뿐이고 기본 off.
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Combat.Diagnostics
{
/// <summary>ProjectileBase.Set 의 target 기준 시작 위치 계산(원본 2줄 훅이 부른다).</summary>
public static class WLProjectileStart
{
/// <summary>가드가 null target 을 잡은 횟수(프로브·QA 가 읽는다).</summary>
public static int NullTargetCount;
/// <summary>그중 shooter 로도 대체하지 못해 현재 위치를 유지한 횟수.</summary>
public static int NoShooterCount;
/// <summary>마지막으로 잡은 스킬(진단용).</summary>
public static string LastSkill = "";
/// <summary>카운터 초기화(프로브 전용).</summary>
public static void ResetCounters() { NullTargetCount = NoShooterCount = 0; LastSkill = ""; }
/// <summary>
/// target 기준 시작 위치. target 이 null 이면 shooter → current 순으로 대체한다.
/// </summary>
/// <param name="data">발사 데이터(원본 `_data` 그대로).</param>
/// <param name="current">대체 실패 시 유지할 현재 위치.</param>
public static Vector3 TargetPos(ProjectileData data, Vector3 current)
{
if (data == null) return current;
var cfg = WLErrorGuardSettings.Instance;
bool guard = cfg == null || (cfg.enabled && !WLErrorGuardSettings.RuntimeDisabled && cfg.projectileNullTargetGuard);
// C8: 가드 off 면 원본 그대로 역참조한다(= 원본 동작 100%).
if (!guard) return data.target.Get_CenterPositionFoward();
if (!(data.target == null)) return data.target.Get_CenterPositionFoward(); // Unity 오버로드 == (가짜 null 포함)
NullTargetCount++;
LastSkill = data.m_Skill.ToString();
if (!(data.shooter == null))
{
if (cfg != null && cfg.projectileGuardLog)
Debug.Log("[WL813x2] 투사체 타깃 소실 — shooter 위치로 대체 · skill=" + LastSkill + " · 누적 " + NullTargetCount);
return data.shooter.Get_CenterPositionFoward();
}
NoShooterCount++;
if (cfg != null && cfg.projectileGuardLog)
Debug.Log("[WL813x2] 투사체 타깃·발사자 모두 소실 — 현재 위치 유지 · skill=" + LastSkill);
return current;
}
}
}