Project_WL/AgentScripts/WL813t2_Guard.cs

260 lines
16 KiB
C#
Raw 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.

// 발주서 WL-813t2 — 지형 가드 · 스폰 위치 검증 프로브 (에디트 모드 · Play 0 · 로그인 0)
//
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기)
// SO 생성 : unity command --project-path E:\NerdNavis\WL_wt\systems run_script --file AgentScripts/WL813t2_Guard.cs --entry WL813t2_Guard.MakeSettings --timeout 300
// 프로브 : … --entry WL813t2_Guard.Probe --timeout 900
//
// 프로브가 재는 것 — 전부 읽기 전용(에셋 수정 0):
// ① SO 실값(에셋에서 읽는다 · 코드 상수 대조 0)
// ② 언덕 격자 1000점 — 재베이크 뒤 walkable 0 인지(SamplePosition)
// ③ 되돌릴 지점 찾기 — D-48 실좌표(아레나 139/20 위 y 3.66 · 5.48)에서 FindGround → y ≤ 2.96
// ④ 스폰 보정 경로 — 언덕 위 가짜 스폰점 200개에 §1-4 규약(재표집 3회 → 스포너 노드) 적용
// ⑤ C8 — enabled=0 · terrainGuardEnabled=0 · spawnGuardEnabled=0 · RuntimeDisabled 진리표
// ⑥ GC — 정상 상태 틱 1000회의 GC 증가분
public static class WL813t2_Guard
{
const string kSoPath = "Assets/WL/Combat/Settings/Resources/WL/WLTerrainGuardSettings.asset";
const string kNavAsset = "Assets/WL/Settings/WL_Nature_NavMesh.asset";
const string kMapPrefab= "Assets/Res_Addr/Map/WL_Nature.prefab";
const string kOutDir = "AgentScripts/staging/WL813t2";
static System.Text.StringBuilder s;
static void L(string t) { s.AppendLine(t); UnityEngine.Debug.Log("[WL813t2] " + t); }
static string Abs(string rel)
{
return System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(),
rel.Replace('/', System.IO.Path.DirectorySeparatorChar));
}
static string Flush(string path)
{
try
{
System.IO.Directory.CreateDirectory(Abs(kOutDir));
System.IO.File.WriteAllText(Abs(path), s.ToString(), new System.Text.UTF8Encoding(true));
}
catch (System.Exception e) { UnityEngine.Debug.LogWarning("[WL813t2] 로그 저장 실패: " + e.Message); }
return s.ToString();
}
// ─────────────────────────────────────────────────────────────────────────
public static string MakeSettings()
{
s = new System.Text.StringBuilder();
var existing = UnityEditor.AssetDatabase.LoadAssetAtPath<WL.Combat.Nav.WLTerrainGuardSettings>(kSoPath);
if (existing != null) { L("이미 있음: " + kSoPath); return Flush(kOutDir + "/SO.txt"); }
var so = UnityEngine.ScriptableObject.CreateInstance<WL.Combat.Nav.WLTerrainGuardSettings>();
// 값은 클래스 초기값(= 발주서 §1-3 · §1-4 값)을 그대로 굳힌다 — 여기서 새 수치를 만들지 않는다.
UnityEditor.AssetDatabase.CreateAsset(so, kSoPath);
UnityEditor.AssetDatabase.SaveAssets();
UnityEditor.AssetDatabase.ImportAsset(kSoPath, UnityEditor.ImportAssetOptions.ForceUpdate);
L("생성: " + kSoPath);
return Flush(kOutDir + "/SO.txt");
}
// ─────────────────────────────────────────────────────────────────────────
public static string Probe()
{
s = new System.Text.StringBuilder();
L("===== WL-813t2 가드 프로브 (" + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ") =====");
WL.Combat.Nav.WLTerrainGuardSettings.ClearCache();
var cfg = UnityEngine.Resources.Load<WL.Combat.Nav.WLTerrainGuardSettings>(
WL.Combat.Nav.WLTerrainGuardSettings.ResourcesPath);
if (cfg == null) { L("[에러] SO 로드 실패 (" + kSoPath + ")"); return Flush(kOutDir + "/PROBE.txt"); }
// ① SO 실값
L("");
L("── ① SO 실값 (에셋에서 읽음) ──");
L(" enabled=" + B(cfg.enabled) + " verboseLog=" + B(cfg.verboseLog)
+ " check=" + cfg.checkSeconds + "s rescan=" + cfg.rescanSeconds + "s");
L(" [지형] terrainGuardEnabled=" + B(cfg.terrainGuardEnabled) + " maxWalkableY=" + cfg.maxWalkableY
+ " offNavMeshTolerance=" + cfg.offNavMeshTolerance + " dwell=" + cfg.dwellSeconds + "s cool=" + cfg.warpCooldownSeconds + "s"
+ " near=" + cfg.warpNearRadius + " far=" + cfg.warpFarRadius);
L(" [지형] guardPlayer=" + B(cfg.guardPlayer) + " guardMobs=" + B(cfg.guardMobs) + " guardBoss=" + B(cfg.guardBoss)
+ " warpSnapsCameraLead=" + B(cfg.warpSnapsCameraLead));
L(" [범위] heightGuardMapIds=[" + Join(cfg.heightGuardMapIds) + "] allMaps=" + B(cfg.heightGuardAllMaps)
+ " whenUnknown=" + B(cfg.heightGuardWhenMapUnknown) + " (WL_Nature = BattleMapConfig n_MapID -1)");
L(" [스폰] spawnGuardEnabled=" + B(cfg.spawnGuardEnabled) + " check=" + cfg.spawnCheckSeconds + "s"
+ " sampleRadius=" + cfg.spawnSampleRadius + " retries=" + cfg.spawnRetries
+ " fallbackToNode=" + B(cfg.spawnFallbackToSpawnerNode));
var navData = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AI.NavMeshData>(kNavAsset);
var root = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(kMapPrefab);
if (navData == null || root == null) { L("[에러] NavMesh/프리팹 로드 실패"); return Flush(kOutDir + "/PROBE.txt"); }
var inst = UnityEngine.AI.NavMesh.AddNavMeshData(navData);
try
{
// ② 언덕 격자 1000점
L("");
L("── ② 언덕 격자 — 재베이크 뒤 walkable 0 인지 ──");
// 813t2 실측 고지대 클러스터 C1(X 96~160 · Z 100~16 · 최고 y 20.46)이 D-48 현장(아레나 139/20 옆)이다.
UnityEngine.Random.InitState(8132);
int n = 1000, walkable = 0, aboveMax = 0;
float hiY = -1e9f;
for (int i = 0; i < n; i++)
{
float x = UnityEngine.Random.Range(96f, 160f);
float z = UnityEngine.Random.Range(-100f, -16f);
float y = UnityEngine.Random.Range(3.0f, 20.0f); // 언덕 높이대
UnityEngine.AI.NavMeshHit h;
if (!UnityEngine.AI.NavMesh.SamplePosition(new UnityEngine.Vector3(x, y, z), out h, 1.0f, UnityEngine.AI.NavMesh.AllAreas)) continue;
walkable++;
if (h.position.y > hiY) hiY = h.position.y;
if (h.position.y > cfg.maxWalkableY) aboveMax++;
}
L(" 클러스터 C1(X96~160 · Z100~16 · y 3~20) 1000점 · 반경 1 m 스냅 성공 " + walkable
+ " · maxWalkableY(" + cfg.maxWalkableY + ") 초과 " + aboveMax
+ (walkable > 0 ? " · 최고 스냅 y " + hiY.ToString("F2") : ""));
// 맵 전역 y>3 walkable 이 정말 0 인지 한 번 더(삼각형 기준은 Bake 프로브가 이미 0.0 을 실측)
var tri = UnityEngine.AI.NavMesh.CalculateTriangulation();
float top = -1e9f;
for (int i = 0; i < tri.vertices.Length; i++) if (tri.vertices[i].y > top) top = tri.vertices[i].y;
L(" NavMesh 정점 최고 y = " + top.ToString("F2") + " (maxWalkableY " + cfg.maxWalkableY + " 아래여야 한다)");
// ③ 되돌릴 지점 찾기 — D-48 실좌표
L("");
L("── ③ FindGround — D-48 실좌표에서 되돌릴 지점 ──");
UnityEngine.Vector3[] cases = {
new UnityEngine.Vector3(139f, 3.66f, -20f), // D-48 R2 t=295.46
new UnityEngine.Vector3(139f, 5.48f, -20f), // D-48 R2 t=309.33
new UnityEngine.Vector3(139f, 5.27f, -20f), // D-48 결과창 방치 74 s
new UnityEngine.Vector3(128f, 8.00f, -58f), // 클러스터 C1 중심 상공
new UnityEngine.Vector3(147f, 6.00f, 30f), // 존2 상공
};
for (int i = 0; i < cases.Length; i++)
{
UnityEngine.Vector3 got;
bool ok = WL.Combat.Nav.TerrainGuard.FindGround(cfg, cases[i], out got);
L(" " + cases[i].ToString("F2") + " → " + (ok ? got.ToString("F2") + " · Δy " + (cases[i].y - got.y).ToString("F2")
+ " · 수평 " + UnityEngine.Vector2.Distance(new UnityEngine.Vector2(cases[i].x, cases[i].z), new UnityEngine.Vector2(got.x, got.z)).ToString("F2") + " m"
+ (got.y <= cfg.maxWalkableY ? " · PASS" : " · **FAIL(언덕 위)**")
: "실패(반경 " + cfg.warpFarRadius + " m 안에 NavMesh 없음)"));
}
// ④ 스폰 보정 경로 — 실제 SpawnPositionGuard.ResolveSpawnPoint 를 그대로 부른다(로직 복제 0)
L("");
L("── ④ 스폰 보정 경로 — 언덕 위 가짜 스폰점 200개 (SpawnPositionGuard.ResolveSpawnPoint 실호출) ──");
var mgrs = root.GetComponentsInChildren<MobControlMgr>(true);
for (int i = 0; i < mgrs.Length; i++) // 에디트 모드에는 씬 인스턴스가 없다 → 노드 캐시를 직접 심는다
WL.Combat.Nav.SpawnPositionGuard.SeedSpawnerNodeForProbe(mgrs[i].SpawnerID, mgrs[i].transform.position);
UnityEngine.Random.InitState(8133);
int okStep1 = 0, okRetry = 0, okNode = 0, fail = 0, resultAbove = 0;
for (int i = 0; i < 200; i++)
{
var mgr = mgrs[i % mgrs.Length];
var origin = mgr.transform.position;
// 스포너 반경 안의 랜덤 XZ 를 언덕 높이(y 6)로 들어올린 가짜 스폰점
var off = UnityEngine.Random.insideUnitCircle * mgr.MobGenRange;
var p = new UnityEngine.Vector3(origin.x + off.x, 6f, origin.z + off.y);
UnityEngine.Vector3 res;
int how = WL.Combat.Nav.SpawnPositionGuard.ResolveSpawnPoint(cfg, p, mgr.SpawnerID, out res);
if (how == WL.Combat.Nav.SpawnPositionGuard.ResultValid) okStep1++;
else if (how == WL.Combat.Nav.SpawnPositionGuard.ResultNode) okNode++;
else if (how == WL.Combat.Nav.SpawnPositionGuard.ResultFailed) { fail++; continue; }
else okRetry++;
if (res.y > cfg.maxWalkableY) resultAbove++;
}
L(" 200점 · 원위치 유효 " + okStep1 + " · 재표집 " + okRetry + " · 스포너 노드 폴백 " + okNode
+ " · 보정 실패 " + fail + " · **보정 결과가 언덕 위 " + resultAbove + "**");
// 스포너 노드 자체가 전부 walkable 이고 언덕 아래인지
int nodeOk = 0, nodeHigh = 0;
for (int i = 0; i < mgrs.Length; i++)
{
UnityEngine.AI.NavMeshHit h;
if (!UnityEngine.AI.NavMesh.SamplePosition(mgrs[i].transform.position, out h, cfg.spawnSampleRadius, UnityEngine.AI.NavMesh.AllAreas)) continue;
nodeOk++;
if (h.position.y > cfg.maxWalkableY) { nodeHigh++; L(" [경고] 스포너 " + mgrs[i].SpawnerID + " 노드가 언덕 위 y=" + h.position.y.ToString("F2")); }
}
L(" 스포너 노드 " + mgrs.Length + "기 · walkable " + nodeOk + " · 언덕 위 " + nodeHigh + " (0 이어야 폴백이 안전하다)");
}
finally { UnityEngine.AI.NavMesh.RemoveNavMeshData(inst); }
// ⑤ C8 진리표 — 임시 SO 로만 실험한다(실에셋 무수정)
L("");
L("── ⑤ C8 진리표 (임시 SO · 실에셋 무수정) ──");
var real = cfg;
try
{
L(" 실에셋 → TerrainGuard.Active=" + B(WL.Combat.Nav.TerrainGuard.Active)
+ " · SpawnPositionGuard.Active=" + B(WL.Combat.Nav.SpawnPositionGuard.Active));
var tmp = UnityEngine.ScriptableObject.CreateInstance<WL.Combat.Nav.WLTerrainGuardSettings>();
WL.Combat.Nav.WLTerrainGuardSettings.SetInstanceForProbe(tmp);
tmp.enabled = false;
L(" enabled=0 → " + B(WL.Combat.Nav.TerrainGuard.Active) + " / " + B(WL.Combat.Nav.SpawnPositionGuard.Active) + " (기대 false/false)");
tmp.enabled = true; tmp.terrainGuardEnabled = false;
L(" terrainGuard=0 → " + B(WL.Combat.Nav.TerrainGuard.Active) + " / " + B(WL.Combat.Nav.SpawnPositionGuard.Active) + " (기대 false/true)");
tmp.terrainGuardEnabled = true; tmp.spawnGuardEnabled = false;
L(" spawnGuard=0 → " + B(WL.Combat.Nav.TerrainGuard.Active) + " / " + B(WL.Combat.Nav.SpawnPositionGuard.Active) + " (기대 true/false)");
tmp.spawnGuardEnabled = true; WL.Combat.Nav.WLTerrainGuardSettings.RuntimeDisabled = true;
L(" RuntimeDisabled → " + B(WL.Combat.Nav.TerrainGuard.Active) + " / " + B(WL.Combat.Nav.SpawnPositionGuard.Active) + " (기대 false/false)");
WL.Combat.Nav.WLTerrainGuardSettings.RuntimeDisabled = false;
WL.Combat.Nav.WLTerrainGuardSettings.SetInstanceForProbe(null);
L(" 에셋 없음 → " + B(WL.Combat.Nav.TerrainGuard.Active) + " / " + B(WL.Combat.Nav.SpawnPositionGuard.Active) + " (기대 false/false)");
UnityEngine.Object.DestroyImmediate(tmp);
// ⑥ GC — 정상 상태 틱 + 실제 판정 경로(FindGround · ResolveSpawnPoint) 각 1000회
L("");
L("── ⑥ GC — 틱 1000회 + 판정 경로 1000회 ──");
var g = UnityEngine.ScriptableObject.CreateInstance<WL.Combat.Nav.WLTerrainGuardSettings>();
g.rescanSeconds = 100000f; g.spawnCheckSeconds = 100000f;
WL.Combat.Nav.WLTerrainGuardSettings.SetInstanceForProbe(g);
WL.Combat.Nav.TerrainGuard.ResetDiagnostics();
WL.Combat.Nav.SpawnPositionGuard.ResetDiagnostics();
WL.Combat.Nav.TerrainGuard.ForceTick(); // 1회는 재수집(할당) — 이후가 정상 상태
WL.Combat.Nav.SpawnPositionGuard.ForceTick();
var navInst2 = UnityEngine.AI.NavMesh.AddNavMeshData(navData);
try
{
var from = new UnityEngine.Vector3(139f, 5.48f, -20f);
UnityEngine.Vector3 t1, t2;
WL.Combat.Nav.TerrainGuard.FindGround(g, from, out t1); // JIT 워밍
WL.Combat.Nav.SpawnPositionGuard.ResolveSpawnPoint(g, from, 813001, out t2);
System.GC.Collect(); System.GC.WaitForPendingFinalizers(); System.GC.Collect();
long before = System.GC.GetTotalMemory(false);
for (int i = 0; i < 1000; i++)
{
WL.Combat.Nav.TerrainGuard.ProbeTick();
WL.Combat.Nav.SpawnPositionGuard.ProbeTick();
WL.Combat.Nav.TerrainGuard.FindGround(g, from, out t1);
WL.Combat.Nav.SpawnPositionGuard.ResolveSpawnPoint(g, from, 813001, out t2);
}
long after = System.GC.GetTotalMemory(false);
L(" 틱 1000 + FindGround 1000 + ResolveSpawnPoint 1000 · GC 총량 " + before + " → " + after
+ " (Δ " + (after - before) + " B)"
+ " · 재수집 " + WL.Combat.Nav.TerrainGuard.Scans + " · Spawn 스캔 " + WL.Combat.Nav.SpawnPositionGuard.Scans);
}
finally { UnityEngine.AI.NavMesh.RemoveNavMeshData(navInst2); }
UnityEngine.Object.DestroyImmediate(g);
}
finally
{
WL.Combat.Nav.WLTerrainGuardSettings.RuntimeDisabled = false;
WL.Combat.Nav.WLTerrainGuardSettings.ClearCache();
WL.Combat.Nav.TerrainGuard.ResetDiagnostics();
WL.Combat.Nav.SpawnPositionGuard.ResetDiagnostics();
L(" 정리: 캐시 초기화 · RuntimeDisabled=false · 실에셋 재로드 = " + (UnityEngine.Resources.Load<WL.Combat.Nav.WLTerrainGuardSettings>(WL.Combat.Nav.WLTerrainGuardSettings.ResourcesPath) != null));
}
L("");
L("===== 끝 =====");
return Flush(kOutDir + "/PROBE.txt");
}
static string B(bool b) { return b ? "true" : "false"; }
static string Join(int[] a)
{
if (a == null) return "";
var sb = new System.Text.StringBuilder();
for (int i = 0; i < a.Length; i++) { if (i > 0) sb.Append(", "); sb.Append(a[i]); }
return sb.ToString();
}
}