Project_WL/AgentScripts/WL813t2_Guard.cs

260 lines
16 KiB
C#
Raw Normal View History

// 발주서 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();
}
}