221 lines
10 KiB
C#
221 lines
10 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
|
// NullMaterialGuard.cs — 빈 머티리얼 슬롯 렌더러 비활성(D-40 마젠타 폴리곤)
|
|
//
|
|
// PD 지시 #813 · 발주서 WL-813m3 §1-4 · 근거 = Q9 `qa/W2-9_재채점_v7.md` §6
|
|
//
|
|
// ■ Q9 가 특정한 5종(전부 `InGameInfo/Effects/**` · `slot=0` 이 null)
|
|
// Effect_Hit_10501(Clone)/Pos
|
|
// Effect_Hit_AnubisSkill2(Clone)/vfx_Hit_Orb08_Orange/Light
|
|
// Effect_Poison(Clone)/GameObject
|
|
// Select_WindFairy(Clone)/PowerOrbSciFi05 (+ /OrbGlow · /Sparks)
|
|
// Hit_P_Batty_Meele(Clone)/FX_Yellow_Hit/Pos
|
|
// 🔴 다섯 개 **전부 원본(힘민지) 이펙트 풀**이 스폰한 것이다 — WL 이 만든 것이 아니다.
|
|
// 그래서 이 가드는 WL 스폰 지점에 훅을 넣는 대신 **풀 루트를 스윕**한다
|
|
// (그 스폰 파일들 = SkillSpectacle · LootBurst · LevelUpBurst 는 이번 발주의 수정 대상이 아니다).
|
|
// WL 스폰 지점에서 부를 공개 API `Inspect(GameObject)` 도 함께 둔다(후속 1줄 연결용).
|
|
//
|
|
// ■ 어떻게
|
|
// 루트(`InGameInfo.tf_Effects` · `tf_Objs` · 811i Live/Pool · 메인 PC)의 **직속 자식**을 주기적으로 본다.
|
|
// 처음 보는 인스턴스만 하위 렌더러를 전수 검사하고(인스턴스 ID 캐시 = 스폰 1회 검사),
|
|
// `sharedMaterials` 에 null 슬롯이 있으면 `Renderer.enabled = false`(SO 로 변경 가능).
|
|
// 풀은 같은 인스턴스를 재사용하므로 한 번 끈 렌더러는 계속 꺼져 있다.
|
|
//
|
|
// ■ GC 0
|
|
// `GetComponentsInChildren<Renderer>(true, List)` · `Renderer.GetSharedMaterials(List)` 둘 다 비할당 오버로드.
|
|
// 캐시는 `HashSet<int>`(성장 때만 할당) · 문자열은 **증거 표본 pathSampleMax 개까지만** 만든다.
|
|
//
|
|
// ■ C8 롤백: `WLNullMaterialSettings.enabled = 0` → 러너도 스윕도 없다(마젠타 그대로 = 원본 100 %).
|
|
// 🔴 원본 프리팹 수정 0 — 근본 수리는 아트 발주(완료보고 「아트 수정 목록」).
|
|
// 🔴 Debug.LogError/LogException 금지(813z).
|
|
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
|
|
namespace WL.Combat.Reaction
|
|
{
|
|
/// <summary>빈 머티리얼 슬롯 렌더러를 찾아 끈다. 정적 · 스폰 1회 검사 · GC 0.</summary>
|
|
public static class NullMaterialGuard
|
|
{
|
|
// ── 진단(프로브가 읽는다)
|
|
/// <summary>스윕 횟수 · 검사한 오브젝트 수 · 검사한 렌더러 수.</summary>
|
|
public static int SweepCount, InspectedObjects, InspectedRenderers;
|
|
/// <summary>빈 슬롯을 가진 렌더러 수 · 실제로 끈 수.</summary>
|
|
public static int NullMaterialRenderers, DisabledRenderers;
|
|
/// <summary>결함이 있던 오브젝트 수(렌더러가 아니라 스폰 단위).</summary>
|
|
public static int OffendingObjects;
|
|
/// <summary>마지막으로 끈 경로 · 마지막 스윕 시각(unscaled).</summary>
|
|
public static string LastPath = "";
|
|
public static float LastSweepTime;
|
|
/// <summary>증거 표본(경로 문자열 · pathSampleMax 개까지).</summary>
|
|
public static readonly List<string> Samples = new List<string>(16);
|
|
|
|
static readonly HashSet<int> s_inspected = new HashSet<int>();
|
|
static readonly List<Renderer> s_rends = new List<Renderer>(32);
|
|
static readonly List<Material> s_mats = new List<Material>(8);
|
|
static StringBuilder s_sb;
|
|
static float s_nextSweep;
|
|
|
|
static WLNullMaterialSettings St { get { return WLNullMaterialSettings.Instance; } }
|
|
|
|
/// <summary>캐시에 담긴 인스턴스 수(프로브).</summary>
|
|
public static int CacheCount { get { return s_inspected.Count; } }
|
|
|
|
// ───────────────────────────────────────── 스윕
|
|
|
|
/// <summary>러너가 매 프레임 부른다. 주기·비활성 판정은 여기서 한다(C8 = 즉시 반환).</summary>
|
|
public static void Tick(float unscaledNow)
|
|
{
|
|
var st = St;
|
|
if (st == null || !WLNullMaterialSettings.Enabled) return;
|
|
if (unscaledNow < s_nextSweep) return;
|
|
s_nextSweep = unscaledNow + Mathf.Max(0f, st.sweepIntervalSeconds);
|
|
Sweep(st, unscaledNow);
|
|
}
|
|
|
|
static void Sweep(WLNullMaterialSettings st, float unscaledNow)
|
|
{
|
|
SweepCount++; LastSweepTime = unscaledNow;
|
|
if (s_inspected.Count > Mathf.Max(64, st.inspectCacheMax)) s_inspected.Clear();
|
|
|
|
int budget = Mathf.Max(1, st.maxInspectPerSweep);
|
|
|
|
var info = InGameInfo.Ins;
|
|
if (info != null)
|
|
{
|
|
if (st.sweepIngameEffects) budget = SweepRoot(info.tf_Effects, st, budget);
|
|
if (st.sweepIngameObjs) budget = SweepRoot(info.tf_Objs, st, budget);
|
|
}
|
|
|
|
if (st.sweepSkillSpectacle && SkillSpectacleRunner.Exists)
|
|
{
|
|
var runner = SkillSpectacleRunner.Ensure(); // Exists 가 true 라 새로 만들지 않는다
|
|
if (runner != null)
|
|
{
|
|
budget = SweepRoot(runner.LiveRoot, st, budget);
|
|
budget = SweepRoot(runner.PoolRoot, st, budget);
|
|
}
|
|
}
|
|
|
|
if (st.sweepMainPc)
|
|
{
|
|
var pc = MyValue.MyPC;
|
|
if (pc != null) budget = SweepRoot(pc.transform, st, budget);
|
|
}
|
|
}
|
|
|
|
/// <summary>루트의 직속 자식 중 처음 보는 것만 검사한다. 남은 예산을 돌려준다.</summary>
|
|
static int SweepRoot(Transform root, WLNullMaterialSettings st, int budget)
|
|
{
|
|
if (root == null || budget <= 0) return budget;
|
|
int n = root.childCount;
|
|
for (int i = 0; i < n && budget > 0; i++)
|
|
{
|
|
var child = root.GetChild(i);
|
|
if (child == null) continue;
|
|
int id = child.GetInstanceID();
|
|
if (s_inspected.Contains(id)) continue;
|
|
s_inspected.Add(id);
|
|
budget--;
|
|
InspectTransform(child, st);
|
|
}
|
|
return budget;
|
|
}
|
|
|
|
// ───────────────────────────────────────── 검사
|
|
|
|
/// <summary>WL 스폰 지점용 공개 API — 스폰 직후 1회 부르면 된다(캐시 · 중복 호출 안전).</summary>
|
|
public static void Inspect(GameObject go)
|
|
{
|
|
var st = St;
|
|
if (st == null || !WLNullMaterialSettings.Enabled || go == null) return;
|
|
int id = go.transform.GetInstanceID();
|
|
if (s_inspected.Contains(id)) return;
|
|
s_inspected.Add(id);
|
|
InspectTransform(go.transform, st);
|
|
}
|
|
|
|
static void InspectTransform(Transform t, WLNullMaterialSettings st)
|
|
{
|
|
InspectedObjects++;
|
|
s_rends.Clear();
|
|
try { t.GetComponentsInChildren<Renderer>(true, s_rends); }
|
|
catch (Exception) { return; }
|
|
|
|
bool offending = false;
|
|
for (int i = 0; i < s_rends.Count; i++)
|
|
{
|
|
var r = s_rends[i];
|
|
if (r == null) continue;
|
|
InspectedRenderers++;
|
|
if (!HasNullSlot(r, st)) continue;
|
|
|
|
NullMaterialRenderers++;
|
|
offending = true;
|
|
|
|
if (Samples.Count < Mathf.Max(0, st.pathSampleMax))
|
|
{
|
|
string path = PathOf(r.transform);
|
|
Samples.Add(path);
|
|
LastPath = path;
|
|
}
|
|
|
|
switch (st.action)
|
|
{
|
|
case NullMaterialAction.DisableRenderer:
|
|
if (r.enabled) { r.enabled = false; DisabledRenderers++; }
|
|
break;
|
|
case NullMaterialAction.DeactivateObject:
|
|
if (r.gameObject.activeSelf) { r.gameObject.SetActive(false); DisabledRenderers++; }
|
|
break;
|
|
default:
|
|
break; // MeasureOnly — 계측만
|
|
}
|
|
|
|
if (st.verboseLog)
|
|
Debug.Log("[WL813m3] nullMaterial " + (st.action == NullMaterialAction.MeasureOnly ? "measured " : "disabled ")
|
|
+ (Samples.Count > 0 ? LastPath : r.name)); // 🔴 Log 만(813z)
|
|
}
|
|
if (offending) OffendingObjects++;
|
|
}
|
|
|
|
static bool HasNullSlot(Renderer r, WLNullMaterialSettings st)
|
|
{
|
|
s_mats.Clear();
|
|
try { r.GetSharedMaterials(s_mats); }
|
|
catch (Exception) { return false; }
|
|
if (s_mats.Count == 0) return st.treatEmptyArrayAsNull;
|
|
for (int i = 0; i < s_mats.Count; i++)
|
|
if (s_mats[i] == null) return true;
|
|
return false;
|
|
}
|
|
|
|
static string PathOf(Transform t)
|
|
{
|
|
if (s_sb == null) s_sb = new StringBuilder(160); else s_sb.Length = 0;
|
|
s_sb.Append(t.name);
|
|
var p = t.parent;
|
|
int guard = 0;
|
|
while (p != null && guard++ < 12)
|
|
{
|
|
s_sb.Insert(0, '/');
|
|
s_sb.Insert(0, p.name);
|
|
p = p.parent;
|
|
}
|
|
return s_sb.ToString();
|
|
}
|
|
|
|
/// <summary>프로브용: 카운터·캐시 초기화.</summary>
|
|
public static void ResetDiagnostics()
|
|
{
|
|
SweepCount = InspectedObjects = InspectedRenderers = 0;
|
|
NullMaterialRenderers = DisabledRenderers = OffendingObjects = 0;
|
|
LastPath = ""; LastSweepTime = 0f; s_nextSweep = 0f;
|
|
Samples.Clear(); s_inspected.Clear();
|
|
}
|
|
}
|
|
}
|