using System; using System.Collections.Generic; using System.IO; using System.Numerics; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.UI; using VS.Sim; using VS.View; namespace VS.EditorTools { // 화면 렌더 검사: Unity.exe -batchmode(그래픽 켬) -projectPath -executeMethod VS.EditorTools.EditorRender.Run // → C:/ViralStreamer/_re/render/*.png · 임시 세이브 사용(PD 세이브 미접촉) · 결과 "RENDER ALL PASS" 또는 "RENDER FAIL n" public static partial class EditorRender { public const string OutDir = "C:/ViralStreamer/_re/render"; static int fails; static readonly List notes = new List(); static DateTime clockBase = new DateTime(2026, 1, 1, 12, 0, 0); static double clockExtra; public static void Run() { int exit = 0; try { Application.SetStackTraceLogType(LogType.Log, StackTraceLogType.None); fails = 0; notes.Clear(); Directory.CreateDirectory(OutDir); EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); var app = NewApp(); Shot(app, "main_new"); Progress(app.G); app.RebuildStage(); app.RefreshAll(); Shot(app, "main_mid"); app.OpenHouse(); Shot(app, "pop_house"); app.ClosePopup(); app.OpenSkill(); Shot(app, "pop_skill"); app.ClosePopup(); app.OpenCloset(); Shot(app, "pop_closet"); app.ClosePopup(); if (app.G.Story != null && app.G.Story.chapters.Count > 0) { app.OpenStory(app.G.Story.chapters[0]); Shot(app, "pop_story"); app.ClosePopup(); } app.G.CoinDonate = 1234.5m; app.RefreshAll(); app.OpenClaimDonate(); Shot(app, "pop_donate"); app.ClosePopup(); app.OpenSettings(); Shot(app, "pop_settings"); app.ClosePopup(); app.G.Follower = app.T.RankFollower[3]; app.OpenRankUp(); Shot(app, "pop_rankup"); app.ClosePopup(); SystemShots(app); // 방 6종 최대 단계 그림 for (int h = 0; h < Tables.HouseCount; h++) { app.G.S.house = h; app.G.S.rooms.Clear(); foreach (var it in app.T.Rooms[h].Items) app.G.S.rooms.Add(new SaveItem { Type = it.Index, Level = it.Levels.Count - 1 }); app.RebuildStage(); app.RefreshAll(); Shot(app, "room" + (h + 1) + "_max"); } Debug.Log(fails == 0 ? "[VS] RENDER ALL PASS" : $"[VS] RENDER FAIL {fails}"); foreach (var n in notes) Debug.Log("[VS] RENDER NOTE " + n); if (fails > 0) exit = 1; } catch (Exception e) { Debug.LogError("[VS] RENDER EXCEPTION " + e); exit = 2; } EditorApplication.Exit(exit); } // 유닛 애니 재생 검증: -executeMethod VS.EditorTools.EditorRender.AnimSeq → animseq/f##.png(0.2초 간격) + frames.txt(틱마다 유닛별 상태·프레임 이름) public static void AnimSeq() { int exit = 0; try { Application.SetStackTraceLogType(LogType.Log, StackTraceLogType.None); fails = 0; notes.Clear(); string dir = Path.Combine(OutDir, "animseq"); Directory.CreateDirectory(dir); EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); var app = NewApp(); Progress(app.G); app.RebuildStage(); app.RefreshAll(); app.G.S.battleSlots = 6; app.SwitchTab(1); app.RefreshAll(); app.BattleTest = new BattleSetup { mapType = 0, obstacles = 0, castleX = 9f, shots = new List(), gimmicks = new List() }; app.NewBattleRound(11); app.B.Start(); app.B.MaxMana = 99; for (int s = 0; s < 6; s++) { app.B.Mana = 99; app.DeployCard(s); } GameApp.AnimDtOverride = 0.1f; var sb = new System.Text.StringBuilder(); int shots = 0, atkShots = 0; // 틱 400~429 = 전 유닛 기절(정지) → 대기(idle) 클립 순환 검증(2026-10-30 ③ 원화 스타일 대기 루프 · vs_animseq_check idle 판정) for (int i = 0; i < 430; i++) { if (i >= 400) foreach (var u in app.B.Units) u.stun = 1f; app.DebugTick(0.1f, 1); var imgs = new Dictionary(); foreach (var im in app.BattleRoot.GetComponentsInChildren(false)) imgs[im.name] = im; var line = new List(); foreach (var u in app.B.Units) { imgs.TryGetValue("U" + u.uid, out var im); line.Add($"{u.def.id}/{u.uid}:{(u.attacking ? "A" : "w")}cd={u.cd:F2}:{(im != null && im.sprite != null ? im.sprite.name : "null")}"); } sb.AppendLine($"t={i * 0.1f:F1} " + string.Join(" ", line)); // 촬영 = 걷기 처음 2 초(10장) + 근접 유닛(id 1·3·5)이 공격 중인 틱 40장(0.2 초 간격 · 근접은 성까지 20 초 넘게 걸림) bool meleeAtk = false; foreach (var u in app.B.Units) if (u.attacking && !u.def.flying && u.def.range < 2f) meleeAtk = true; if (i % 2 == 0 && (i < 20 || (meleeAtk && atkShots < 40))) { if (i >= 20) atkShots++; Shot(app, "animseq/f" + (shots++).ToString("D2") + "_t" + (i * 0.1f).ToString("F1")); } } File.WriteAllText(Path.Combine(dir, "frames.txt"), sb.ToString()); GameApp.AnimDtOverride = -1f; Debug.Log($"[VS] RENDER ANIMSEQ DONE shots={shots} fails={fails}"); foreach (var n in notes) Debug.Log("[VS] RENDER NOTE " + n); } catch (Exception e) { Debug.LogError("[VS] RENDER EXCEPTION " + e); exit = 2; } EditorApplication.Exit(exit); } static RenderTexture rt; static GameApp NewApp() { clockExtra = 0; GameApp.ClockOverride = () => clockBase.AddSeconds(clockExtra); GameApp.SavePathOverride = Path.Combine(Path.GetTempPath(), "vs_render_save.json"); if (File.Exists(GameApp.SavePathOverride)) File.Delete(GameApp.SavePathOverride); rt = new RenderTexture(1080, 1920, 24, RenderTextureFormat.ARGB32); GameApp.RenderTarget = rt; var go = new GameObject("Game"); var app = go.AddComponent(); app.Headless = true; app.Init(); return app; } public static void Advance(double sec) => clockExtra += sec; /// 중반 진행 상태 (방 1 아이템 일부 · 스킬 · 랭크 3 · 의상 5) static void Progress(Game g) { g.S.rooms.Clear(); int[] lv = { 30, 26, 52, 12, 74, 20, 8, 40, 60, 3, 50 }; for (int i = 0; i < lv.Length; i++) g.S.rooms.Add(new SaveItem { Type = i, Level = lv[i] }); g.S.skills.Clear(); for (int i = 0; i < 5; i++) g.S.skills.Add(new SaveItem { Type = i, Level = 10 + i * 7 }); g.Follower = BigInteger.Parse("250000"); g.S.rankIndex = 3; g.Coin = BigInteger.Parse("12345678"); g.S.ticket = 7; g.S.closetLevel = 5; g.S.closetCurrent = 3; g.CoinDonate = 456.7m; } static void Shot(GameApp app, string name) { Canvas.ForceUpdateCanvases(); app.Cam.targetTexture = rt; app.Cam.Render(); Canvas.ForceUpdateCanvases(); app.Cam.Render(); var prev = RenderTexture.active; RenderTexture.active = rt; var tex = new Texture2D(rt.width, rt.height, TextureFormat.RGB24, false); tex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); tex.Apply(); RenderTexture.active = prev; File.WriteAllBytes(Path.Combine(OutDir, name + ".png"), tex.EncodeToPNG()); UnityEngine.Object.DestroyImmediate(tex); // 검사: 빈 그림 · 글자 넘침 int missing = 0, overflow = 0; foreach (var im in app.Root.GetComponentsInChildren(false)) if (im.sprite == null && im.color.a > 0.01f && im.enabled) { missing++; if (missing <= 3) notes.Add(name + " 빈 Image: " + im.name); } foreach (var t in app.Root.GetComponentsInChildren(false)) { if (string.IsNullOrEmpty(t.text) || !t.enabled) continue; var gen = t.cachedTextGenerator; if (gen.characterCountVisible > 0 && gen.characterCountVisible < t.text.Replace("\n", "").Length - 1) overflow++; } if (missing > 0) { fails++; notes.Add($"{name}: 그림 없는 Image {missing}"); } if (overflow > 0) notes.Add($"{name}: 잘린 글자 {overflow}"); Debug.Log($"[VS] RENDER {name} missing={missing} cut={overflow}"); } } }