Project_WL/AgentScripts/WL764_Probe.cs

383 lines
22 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// PD 지시 #764 — 몬스터 스폰/이동 지형 실측 프로브 (읽기 전용)
//
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기)
// 에디트 모드 스폰 도달성 + NavMesh 섬 통계
// unity command run_script --file AgentScripts/WL764_Probe.cs --entry WL764_Probe.Edit --timeout 300
// Play 중 몹 이탈 감시 시작 / 상태 / 결과
// unity command run_script --file AgentScripts/WL764_Probe.cs --entry WL764_Probe.Watch
// unity command run_script --file AgentScripts/WL764_Probe.cs --entry WL764_Probe.Status
// unity command run_script --file AgentScripts/WL764_Probe.cs --entry WL764_Probe.Dump
//
// 수치는 전부 프로젝트 데이터에서 읽는다 (C45):
// 스포너 위치·MobGenRange = WL_Start.prefab / 플레이어 시작 지점 = MapData.m_MapZoneData[].go_portal (+Vector3.forward)
// NavMesh = Assets/WL/Settings/WL_Start_NavMesh.asset
// 프로브 자체의 격자 간격·표집 수는 "측정 해상도"이지 게임 데이터가 아니다.
public static class WL764_Probe
{
const string kTargetPrefab = "Assets/Res_Addr/Map/WL_Start.prefab";
const string kNavAsset = "Assets/WL/Settings/WL_Start_NavMesh.asset";
const string kEditLog = "AgentScripts/staging/WL_Spawn/EDIT_PROBE.txt";
const string kRunLog = "AgentScripts/staging/WL_Spawn/RUNTIME_PROBE.txt";
// 측정 해상도 (게임 데이터 아님)
const float kGridStep = 4f; // 섬 통계 격자
const int kSamplesPerSpawner = 30;
const int kSeed = 764;
const float kOffMeshRadius = 0.3f; // 발주서 지정 — 이 반경 안에 NavMesh 가 없으면 "이탈"
static System.Text.StringBuilder s;
// ─────────────────────────────────────────────────────────────────────
// 1. 에디트 모드: 스포너별 도달률 + 섬 통계
// ─────────────────────────────────────────────────────────────────────
public static string Edit()
{
s = new System.Text.StringBuilder();
L("===== WL764 에디트 모드 실측 =====");
L("playMode=" + UnityEngine.Application.isPlaying);
var navData = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AI.NavMeshData>(kNavAsset);
if (navData == null) { L("[에러] NavMeshData 없음: " + kNavAsset); return Flush(kEditLog); }
var root = UnityEditor.PrefabUtility.LoadPrefabContents(kTargetPrefab);
var navInst = UnityEngine.AI.NavMesh.AddNavMeshData(navData);
try
{
// 플레이어 시작 지점 — MapData 의 포탈 (런타임 LoadMapMgr.Get_PortalPos 와 동일 식)
var md = root.GetComponent<MapData>();
UnityEngine.Vector3 pcSpawn;
if (md != null && md.m_MapZoneData != null && md.m_MapZoneData.Count > 0 && md.m_MapZoneData[0].go_portal != null)
{
pcSpawn = md.m_MapZoneData[0].go_portal.transform.position + UnityEngine.Vector3.forward;
L("플레이어 시작 = MapData.m_MapZoneData[0].go_portal(" + md.m_MapZoneData[0].go_portal.name + ") + forward = " + pcSpawn.ToString("F2"));
}
else
{
var p = root.transform.Find("Portal_Start");
if (p == null) { L("[에러] 포탈을 찾지 못했다"); return Flush(kEditLog); }
pcSpawn = p.position + UnityEngine.Vector3.forward;
L("플레이어 시작 = Portal_Start + forward = " + pcSpawn.ToString("F2") + " (MapData 경로 실패 → 폴백)");
}
UnityEngine.AI.NavMeshHit pcHit;
if (!UnityEngine.AI.NavMesh.SamplePosition(pcSpawn, out pcHit, 5f, UnityEngine.AI.NavMesh.AllAreas))
{ L("[에러] 시작 지점이 NavMesh 위가 아니다"); return Flush(kEditLog); }
var pcOnNav = pcHit.position;
L("시작 지점 NavMesh 스냅 = " + pcOnNav.ToString("F2"));
// ── (d) 섬 통계 ──────────────────────────────────────────────
IslandStats(pcOnNav);
// ── (a) 스포너별 도달률 ──────────────────────────────────────
var mgrs = root.GetComponentsInChildren<MobControlMgr>(true);
L("");
L("── (a) 스포너 " + mgrs.Length + "개 · 스포너당 " + kSamplesPerSpawner + "점 표집 ──");
L(" ID | 스포너위치 | range | 스포너도달 | 현행표집 도달 | 신방식 도달 | 표집반경초과");
System.Array.Sort(mgrs, delegate (MobControlMgr a, MobControlMgr b) { return a.SpawnerID.CompareTo(b.SpawnerID); });
int totBadOld = 0, totBadNew = 0, badSpawners = 0, overRange = 0, totFallback = 0; float sumSpread = 0f;
for (int i = 0; i < mgrs.Length; i++)
{
var m = mgrs[i];
var org = m.transform.position;
bool spawnerReach = Reachable(pcOnNav, org);
if (!spawnerReach) badSpawners++;
UnityEngine.Random.InitState(kSeed + m.SpawnerID);
int okOld = 0, over = 0;
for (int k = 0; k < kSamplesPerSpawner; k++)
{
var p = DSUtil.Get_RandomPos_onNavMesh(org, 0f, m.MobGenRange);
if (Reachable(pcOnNav, p)) okOld++;
if (UnityEngine.Vector3.Distance(org, p) > m.MobGenRange + 0.01f) over++;
}
overRange += over;
UnityEngine.Random.InitState(kSeed + m.SpawnerID);
int okNew = 0, fallback = 0; float sumD = 0f;
for (int k = 0; k < kSamplesPerSpawner; k++)
{
s_fellBack = false;
var p = SampleStrict(org, m.MobGenRange, pcOnNav);
if (s_fellBack) fallback++;
sumD += UnityEngine.Vector3.Distance(org, p);
if (Reachable(pcOnNav, p)) okNew++;
}
totFallback += fallback; sumSpread += sumD / kSamplesPerSpawner;
totBadOld += (kSamplesPerSpawner - okOld);
totBadNew += (kSamplesPerSpawner - okNew);
L(string.Format(" {0,2} | ({1,8:F1},{2,6:F1},{3,8:F1}) | {4,5:F1} | {5,-6} | {6,3}/{7} ({8,3}%) | {9,3}/{10} ({11,3}%) | {12,2}",
m.SpawnerID, org.x, org.y, org.z, m.MobGenRange, spawnerReach ? "OK" : "🔴불가",
okOld, kSamplesPerSpawner, okOld * 100 / kSamplesPerSpawner,
okNew, kSamplesPerSpawner, okNew * 100 / kSamplesPerSpawner, over));
}
L("");
L("합계: 스포너 원점 도달불가 " + badSpawners + "/" + mgrs.Length
+ " · 현행 표집 도달불가 " + totBadOld + "/" + (mgrs.Length * kSamplesPerSpawner)
+ " (" + Pct(totBadOld, mgrs.Length * kSamplesPerSpawner) + "%)"
+ " · 신방식 도달불가 " + totBadNew + "/" + (mgrs.Length * kSamplesPerSpawner)
+ " (" + Pct(totBadNew, mgrs.Length * kSamplesPerSpawner) + "%)");
L("표집점이 MobGenRange 를 벗어난 횟수(현행) = " + overRange
+ " — SamplePosition 의 스냅 반경이 MobGenRange 라 최대 2×range 까지 튄다");
L("신방식 산포: 원점 폴백 " + totFallback + "/" + (mgrs.Length * kSamplesPerSpawner)
+ " · 스포너별 평균 이격 " + (sumSpread / mgrs.Length).ToString("F2") + "m (MobGenRange 10 기준 · 몹이 원점에 뭉치지 않는지 확인)");
}
finally
{
UnityEngine.AI.NavMesh.RemoveNavMeshData(navInst);
UnityEditor.PrefabUtility.UnloadPrefabContents(root);
}
return Flush(kEditLog);
}
static void IslandStats(UnityEngine.Vector3 pcOnNav)
{
var tri = UnityEngine.AI.NavMesh.CalculateTriangulation();
if (tri.vertices.Length == 0) { L("[에러] NavMesh 가 비어 있다"); return; }
var min = tri.vertices[0]; var max = tri.vertices[0];
for (int i = 1; i < tri.vertices.Length; i++)
{ min = UnityEngine.Vector3.Min(min, tri.vertices[i]); max = UnityEngine.Vector3.Max(max, tri.vertices[i]); }
int nGrid = 0, nOnNav = 0, nReach = 0;
for (float x = min.x; x <= max.x; x += kGridStep)
for (float z = min.z; z <= max.z; z += kGridStep)
{
nGrid++;
UnityEngine.AI.NavMeshHit h;
var probe = new UnityEngine.Vector3(x, (min.y + max.y) * 0.5f, z);
if (!UnityEngine.AI.NavMesh.SamplePosition(probe, out h, (max.y - min.y) * 0.5f + kGridStep, UnityEngine.AI.NavMesh.AllAreas)) continue;
if (UnityEngine.Mathf.Abs(h.position.x - x) > kGridStep || UnityEngine.Mathf.Abs(h.position.z - z) > kGridStep) continue;
nOnNav++;
if (Reachable(pcOnNav, h.position)) nReach++;
}
L("");
L("── (d) NavMesh 섬 통계 (" + kGridStep + "m 격자) ──");
L(" 삼각형 " + (tri.indices.Length / 3) + "개 · 범위 min=" + min.ToString("F0") + " max=" + max.ToString("F0"));
L(" 격자 " + nGrid + "점 → NavMesh 위 " + nOnNav + "점 → 시작 지점에서 도달 가능 " + nReach
+ "점 (" + Pct(nReach, nOnNav) + "%) · 도달 불가 " + (nOnNav - nReach) + "점 (" + Pct(nOnNav - nReach, nOnNav) + "%)");
}
static bool s_fellBack;
/// <summary>신방식 표집 — 스냅 반경을 좁히고 완전 경로(PathComplete)를 요구한다. MobControlMgr 에 넣을 로직과 동일.</summary>
public static UnityEngine.Vector3 SampleStrict(UnityEngine.Vector3 origin, float range, UnityEngine.Vector3 reachFrom)
{
float snap = range * 0.2f; // Edit 프로브 기본 — 실제 값은 MapData.spawnSnapRadius
for (int i = 0; i < 50; i++)
{
var dir = UnityEngine.Random.insideUnitSphere * range + origin;
UnityEngine.AI.NavMeshHit hit;
if (!UnityEngine.AI.NavMesh.SamplePosition(dir, out hit, snap, UnityEngine.AI.NavMesh.AllAreas)) continue;
if (UnityEngine.Vector3.Distance(origin, hit.position) > range) continue;
if (!Reachable(origin, hit.position)) continue;
if (!Reachable(reachFrom, hit.position)) continue;
return hit.position;
}
s_fellBack = true;
return origin;
}
static bool Reachable(UnityEngine.Vector3 from, UnityEngine.Vector3 to)
{
var path = new UnityEngine.AI.NavMeshPath();
if (!UnityEngine.AI.NavMesh.CalculatePath(from, to, UnityEngine.AI.NavMesh.AllAreas, path)) return false;
return path.status == UnityEngine.AI.NavMeshPathStatus.PathComplete;
}
// ─────────────────────────────────────────────────────────────────────
// 2. Play 모드: 몹 이탈 감시
// ─────────────────────────────────────────────────────────────────────
static bool s_watching;
static float s_next, s_t0, s_dur;
static int s_ticks;
static System.Text.StringBuilder s_run;
static System.Collections.Generic.Dictionary<int, int> s_offCount; // instanceID → 이탈 틱 수
static System.Collections.Generic.Dictionary<int, string> s_offName;
static int s_peakMobs, s_offEvents;
const float kWatchSeconds = 60f; // 발주서 지정 감시 시간(측정 조건)
public static string Watch()
{
float seconds = kWatchSeconds;
if (!UnityEngine.Application.isPlaying) return "[에러] Play 중이 아니다";
if (s_watching) return "이미 감시 중 — 남은 " + (s_dur - (UnityEngine.Time.realtimeSinceStartup - s_t0)).ToString("F0") + "s";
s_run = new System.Text.StringBuilder();
s_offCount = new System.Collections.Generic.Dictionary<int, int>();
s_offName = new System.Collections.Generic.Dictionary<int, string>();
s_peakMobs = 0; s_offEvents = 0; s_ticks = 0;
s_t0 = UnityEngine.Time.realtimeSinceStartup; s_dur = seconds; s_next = 0f;
s_watching = true;
UnityEditor.EditorApplication.update -= Tick;
UnityEditor.EditorApplication.update += Tick;
R("===== 런타임 이탈 감시 시작 (" + seconds + "s · 1s 간격 · SamplePosition 반경 " + kOffMeshRadius + ") =====");
ReachSweep("시작");
return "감시 시작 " + seconds + "s";
}
static void Tick()
{
if (!s_watching) { UnityEditor.EditorApplication.update -= Tick; return; }
if (!UnityEngine.Application.isPlaying) { R("[중단] Play 가 멈췄다"); StopWatch(); return; }
float el = UnityEngine.Time.realtimeSinceStartup - s_t0;
if (el < s_next) return;
s_next = el + 1f;
s_ticks++;
var mobs = UnityEngine.Object.FindObjectsByType<MobActor>(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None);
if (mobs.Length > s_peakMobs) s_peakMobs = mobs.Length;
int off = 0;
for (int i = 0; i < mobs.Length; i++)
{
var m = mobs[i];
var p = m.transform.position;
UnityEngine.AI.NavMeshHit h;
if (UnityEngine.AI.NavMesh.SamplePosition(p, out h, kOffMeshRadius, UnityEngine.AI.NavMesh.AllAreas)) continue;
off++; s_offEvents++;
int id = m.GetInstanceID();
if (!s_offCount.ContainsKey(id)) { s_offCount[id] = 0; s_offName[id] = m.name; }
s_offCount[id]++;
if (s_offCount[id] <= 3) // 몹당 3회까지만 상세 기록
{
var ag = m.GetComponent<UnityEngine.AI.NavMeshAgent>();
R(string.Format(" [이탈] t={0,5:F1}s {1} pos=({2:F1},{3:F1},{4:F1}) agent.enabled={5} isOnNavMesh={6} CC={7}",
el, m.name, p.x, p.y, p.z,
ag == null ? "null" : ag.enabled.ToString(),
ag == null ? "null" : (ag.enabled ? ag.isOnNavMesh.ToString() : "-"),
m.IsCC()));
}
}
if (s_ticks % 10 == 0) R(" t=" + el.ToString("F0") + "s 활성몹=" + mobs.Length + " 이탈=" + off);
if (el >= s_dur) { ReachSweep("종료"); StopWatch(); }
}
static void ReachSweep(string tag)
{
var pc = MyValue.MyPC;
if (pc == null) { R("[" + tag + "] PC 없음 — 도달성 스윕 생략"); return; }
var pcPos = pc.transform.position;
UnityEngine.AI.NavMeshHit ph;
if (!UnityEngine.AI.NavMesh.SamplePosition(pcPos, out ph, 3f, UnityEngine.AI.NavMesh.AllAreas))
{ R("[" + tag + "] PC 가 NavMesh 위가 아니다 pos=" + pcPos.ToString("F1")); return; }
var mobs = UnityEngine.Object.FindObjectsByType<MobActor>(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None);
int bad = 0; var names = new System.Text.StringBuilder();
for (int i = 0; i < mobs.Length; i++)
{
if (Reachable(ph.position, mobs[i].transform.position)) continue;
bad++;
if (bad <= 15) { if (names.Length > 0) names.Append(", "); names.Append(mobs[i].name + mobs[i].transform.position.ToString("F0")); }
}
R("[" + tag + "] PC=" + ph.position.ToString("F1") + " 활성몹 " + mobs.Length
+ " 중 도달 불가 " + bad + " (" + Pct(bad, mobs.Length) + "%)" + (bad > 0 ? " 예: " + names : ""));
}
static void StopWatch()
{
s_watching = false;
UnityEditor.EditorApplication.update -= Tick;
R("");
R("===== 감시 종료 · 틱 " + s_ticks + " · 최대 활성몹 " + s_peakMobs
+ " · 이탈 이벤트 " + s_offEvents + " · 이탈 몹 " + (s_offCount == null ? 0 : s_offCount.Count) + "종 =====");
if (s_offCount != null)
foreach (var kv in s_offCount) R(" 이탈몹 " + s_offName[kv.Key] + " ×" + kv.Value + "틱");
WriteFile(kRunLog, s_run.ToString());
}
public static string Stop() { if (s_watching) StopWatch(); return "중지"; }
/// <summary>즉시 1회 측정 — 활성 몹 전원의 (a) PC 로부터의 도달성 (b) NavMesh 이탈 여부.</summary>
public static string Sweep()
{
if (!UnityEngine.Application.isPlaying) return "[에러] Play 중이 아니다";
var pc = MyValue.MyPC;
if (pc == null) return "[에러] PC 없음";
UnityEngine.AI.NavMeshHit ph;
if (!UnityEngine.AI.NavMesh.SamplePosition(pc.transform.position, out ph, 3f, UnityEngine.AI.NavMesh.AllAreas))
return "[에러] PC 가 NavMesh 위가 아니다 " + pc.transform.position.ToString("F1");
var mobs = UnityEngine.Object.FindObjectsByType<MobActor>(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None);
int unreach = 0, off = 0;
var bad = new System.Text.StringBuilder();
for (int i = 0; i < mobs.Length; i++)
{
var p = mobs[i].transform.position;
UnityEngine.AI.NavMeshHit h;
if (!UnityEngine.AI.NavMesh.SamplePosition(p, out h, kOffMeshRadius, UnityEngine.AI.NavMesh.AllAreas)) off++;
if (Reachable(ph.position, p)) continue;
unreach++;
if (unreach <= 12) { if (bad.Length > 0) bad.Append(", "); bad.Append(mobs[i].name + p.ToString("F0")); }
}
return "활성몹 " + mobs.Length + " · PC(" + ph.position.ToString("F1") + ") 에서 도달 불가 " + unreach
+ " (" + Pct(unreach, mobs.Length) + "%) · NavMesh 이탈(" + kOffMeshRadius + "m) " + off
+ (unreach > 0 ? " · 예: " + bad : "");
}
public static string Status()
{
if (!s_watching) return "감시 중 아님 (틱 " + s_ticks + " · 이탈 " + s_offEvents + ")";
float el = UnityEngine.Time.realtimeSinceStartup - s_t0;
var mobs = UnityEngine.Object.FindObjectsByType<MobActor>(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None);
return "감시 중 " + el.ToString("F0") + "/" + s_dur + "s · 틱 " + s_ticks + " · 현재 활성몹 " + mobs.Length
+ " · 누적 이탈 " + s_offEvents + " (" + (s_offCount == null ? 0 : s_offCount.Count) + "종)";
}
public static string Dump()
{
if (s_run == null) return "기록 없음";
WriteFile(kRunLog, s_run.ToString());
return s_run.ToString();
}
/// <summary>#763 펫 판정 — 활성 PetActor 수와 펫 카드 상태.</summary>
public static string PetCheck()
{
var sb = new System.Text.StringBuilder();
var all = UnityEngine.Object.FindObjectsByType<PetActor>(UnityEngine.FindObjectsInactive.Include, UnityEngine.FindObjectsSortMode.None);
var act = UnityEngine.Object.FindObjectsByType<PetActor>(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None);
sb.Append("PetActor 전체=" + all.Length + " 활성=" + act.Length);
for (int i = 0; i < all.Length; i++) sb.Append(" | " + all[i].name + " active=" + all[i].gameObject.activeInHierarchy);
var pets = ActorInfo.Ins != null ? ActorInfo.Ins.Get_Actors(eRole.Pet, 0, true) : null;
sb.Append(" || ActorInfo eRole.Pet(includeDie) = " + (pets == null ? "n/a" : pets.Count.ToString()));
// 계정에 실제로 펫이 장착돼 있는지 — 장착 0 이면 이 검증은 무의미하다
if (ServerInfo.Ins != null && ServerInfo.Ins.m_ServerData != null)
{
int equipped = ServerInfo.Ins.m_ServerData.Get_EqiupmentEquip(eItem.Pet);
sb.Append(" || 장착 펫ID = " + equipped + (equipped > 0 ? " (장착됨 — 차단이 실제로 동작해야 함)" : " (미장착 🔴 검증 무의미)"));
}
// 펫 카드 UI 상태
if (NewGameUI.isIns && NewGameUI.Ins.arr_PetCard != null && NewGameUI.Ins.arr_PetCard.Length > 0)
{
var card = NewGameUI.Ins.arr_PetCard[0];
sb.Append(" || arr_PetCard[0] = " + (card == null ? "null" : card.name + " active=" + card.gameObject.activeInHierarchy));
}
else sb.Append(" || NewGameUI/arr_PetCard 없음");
sb.Append(" || MyPC.Get_MyPet() = " + (MyValue.MyPC == null ? "PC없음" : (MyValue.MyPC.Get_MyPet() == null ? "null(OK)" : MyValue.MyPC.Get_MyPet().name)));
// 설정 에셋은 리플렉션으로 읽는다 — 이 프로브가 WLGameplaySettings 존재 여부와 무관하게 컴파일되도록.
var t = System.Type.GetType("WL.Settings.WLGameplaySettings, Assembly-CSharp");
if (t == null) sb.Append(" || WLGameplaySettings 타입 없음");
else
{
var inst = t.GetProperty("Instance", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static).GetValue(null, null);
if (inst == null) sb.Append(" || WLGameplaySettings 에셋 없음");
else sb.Append(" || WLGameplaySettings.spawnPlayerPet=" + t.GetField("spawnPlayerPet").GetValue(inst));
}
return sb.ToString();
}
// ─────────────────────────────────────────────────────────────────────
static string Pct(int a, int b) { return b == 0 ? "0" : (a * 100 / b).ToString(); }
static void L(string t) { s.Append(t); s.Append("\n"); UnityEngine.Debug.Log("[WL764] " + t); }
static void R(string t) { s_run.Append(t); s_run.Append("\n"); }
static string Flush(string path) { WriteFile(path, s.ToString()); return s.ToString(); }
static void WriteFile(string path, string body)
{
var full = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), path);
var dir = System.IO.Path.GetDirectoryName(full);
if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir);
System.IO.File.WriteAllText(full, body, new System.Text.UTF8Encoding(true));
}
}