Files
AIGame/AIClient/AgentScripts/BuildHomeCamp.cs
T

422 lines
25 KiB
C#

#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.SceneManagement;
using Object = UnityEngine.Object;
/// <summary>Authors HomeCamp through Pipeline; never constructs the scene at runtime.</summary>
public static class BuildHomeCamp
{
const string Root = "Assets/Bundles/Scenes/Town/HomeCamp";
const string Town = "Assets/Bundles/Scenes/Town/TownSquare";
const string PrefabPath = Root + "/HomeCamp.prefab";
const string ScenePath = Root + "/HomeCamp.unity";
const int Layer = 29;
static readonly string[] Parts = { "ground", "mainhouse", "cottage", "fountain" };
static string Repo => Directory.GetParent(Application.dataPath).Parent.FullName;
static string Review => Path.Combine(Repo, "Tools/output/homecamp_review");
[Serializable] class Receipt { public string status = ""; public int triangles = 0; public int targetTriangles = 0; }
public static string Inspect()
{
return string.Join("\n", Enumerable.Range(0, SceneManager.sceneCount).Select(i =>
{ var s = SceneManager.GetSceneAt(i); return s.path + " dirty=" + s.isDirty; }));
}
static void Folder(string path)
{
if (AssetDatabase.IsValidFolder(path)) return;
var parent = path.Substring(0, path.LastIndexOf('/'));
Folder(parent); AssetDatabase.CreateFolder(parent, Path.GetFileName(path));
}
static Bounds BoundsOf(GameObject go)
{
var renderers = go.GetComponentsInChildren<Renderer>(true);
if (renderers.Length == 0) throw new InvalidOperationException("No renderer: " + go.name);
var b = renderers[0].bounds;
foreach (var r in renderers.Skip(1)) b.Encapsulate(r.bounds);
return b;
}
static void SetLayer(GameObject go)
{
foreach (var t in go.GetComponentsInChildren<Transform>(true)) t.gameObject.layer = Layer;
}
static void Source(string source, string destination)
{
if (!File.Exists(source)) throw new FileNotFoundException(source);
Folder(Path.GetDirectoryName(destination).Replace('\\', '/'));
File.Copy(source, destination, true);
AssetDatabase.ImportAsset(destination, ImportAssetOptions.ForceSynchronousImport);
}
static void TextureSettings(string path, bool srgb, bool backdrop = false)
{
var importer = (TextureImporter)AssetImporter.GetAtPath(path);
importer.textureType = TextureImporterType.Default;
importer.sRGBTexture = srgb; importer.alphaIsTransparency = false;
importer.mipmapEnabled = !backdrop; importer.maxTextureSize = 2048;
importer.textureCompression = TextureImporterCompression.Uncompressed;
importer.wrapMode = TextureWrapMode.Clamp;
importer.SaveAndReimport();
}
static Material SaveMaterial(string path, string shaderName)
{
var shader = Shader.Find(shaderName);
if (!shader) throw new InvalidOperationException("Missing shader " + shaderName);
var mat = AssetDatabase.LoadAssetAtPath<Material>(path);
if (!mat) { mat = new Material(shader); AssetDatabase.CreateAsset(mat, path); }
mat.shader = shader;
return mat;
}
public static string BuildAssets()
{
if (EditorApplication.isPlaying) throw new InvalidOperationException("Stop Play before authoring.");
AssetDatabase.ImportAsset("Assets/Bundles/Shaders/XShader/HomeCampXPbr.shader", ImportAssetOptions.ForceSynchronousImport);
// Validate all four receipts before importing any model.
foreach (var part in Parts)
{
var path = Path.Combine(Repo, "Tools/output/homecamp_studio", part, "production.json");
var receipt = JsonUtility.FromJson<Receipt>(File.ReadAllText(path));
if (receipt.status != "ready_for_unity" || receipt.triangles * 10 > receipt.targetTriangles * 13)
throw new InvalidOperationException("Unfinished model " + part);
}
foreach (var part in Parts)
{
var dir = Root + "/" + part;
var source = Path.Combine(Repo, "Tools/output/homecamp_studio", part, "xpbr");
foreach (var suffix in new[] { ".fbx", "_base.png", "_mix.png" })
Source(Path.Combine(source, part + suffix), dir + "/" + part + suffix);
var fbx = dir + "/" + part + ".fbx";
var importer = (ModelImporter)AssetImporter.GetAtPath(fbx);
importer.globalScale = 1; importer.useFileScale = false; importer.bakeAxisConversion = true;
importer.materialImportMode = ModelImporterMaterialImportMode.None;
importer.meshCompression = ModelImporterMeshCompression.Off;
importer.isReadable = part == "ground"; importer.addCollider = false;
importer.SaveAndReimport();
TextureSettings(dir + "/" + part + "_base.png", true);
TextureSettings(dir + "/" + part + "_mix.png", false);
var mat = SaveMaterial(dir + "/" + part + ".mat", "Test/HomeCampXPbr");
mat.SetTexture("_MainTex", AssetDatabase.LoadAssetAtPath<Texture2D>(dir + "/" + part + "_base.png"));
mat.SetTexture("_MixTex", AssetDatabase.LoadAssetAtPath<Texture2D>(dir + "/" + part + "_mix.png"));
mat.SetColor("_BaseColor", Color.white); mat.SetFloat("_Metallic", 1); mat.SetFloat("_Roughness", 1); mat.SetFloat("_AO", 1);
EditorUtility.SetDirty(mat); AssetDatabase.SaveAssetIfDirty(mat);
var scene = EditorSceneManager.NewPreviewScene();
try
{
var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(fbx), scene);
go.name = part;
go.transform.localPosition = Vector3.zero; go.transform.localRotation = Quaternion.identity; go.transform.localScale = Vector3.one;
foreach (var r in go.GetComponentsInChildren<Renderer>()) r.sharedMaterial = mat;
if (part == "ground")
foreach (var f in go.GetComponentsInChildren<MeshFilter>())
{ var c = f.gameObject.AddComponent<MeshCollider>(); c.sharedMesh = f.sharedMesh; c.convex = false; }
PrefabUtility.SaveAsPrefabAsset(go, dir + "/" + part + ".prefab");
}
finally { EditorSceneManager.ClosePreviewScene(scene); }
}
Source(Path.Combine(Repo, "Doc/Design/Scene/HomeCamp/backdrop.png"), Root + "/backdrop.png");
TextureSettings(Root + "/backdrop.png", true, true);
var background = SaveMaterial(Root + "/backdrop.mat", "Universal Render Pipeline/Unlit");
background.SetTexture("_BaseMap", AssetDatabase.LoadAssetAtPath<Texture2D>(Root + "/backdrop.png"));
background.SetColor("_BaseColor", Color.white);
EditorUtility.SetDirty(background); AssetDatabase.SaveAssetIfDirty(background);
foreach (var name in new[] { "tree_card", "flower_card" })
{
Source(Path.Combine(Repo, "Doc/Design/Scene/HomeCamp", name + ".png"), Root + "/" + name + ".png");
TextureSettings(Root + "/" + name + ".png", true);
var ti = (TextureImporter)AssetImporter.GetAtPath(Root + "/" + name + ".png");
ti.alphaIsTransparency = true; ti.SaveAndReimport();
var card = SaveMaterial(Root + "/" + name + ".mat", "Universal Render Pipeline/Unlit");
card.SetTexture("_BaseMap", AssetDatabase.LoadAssetAtPath<Texture2D>(Root + "/" + name + ".png"));
card.SetColor("_BaseColor", Color.white); card.SetFloat("_AlphaClip", 1); card.SetFloat("_Cutoff", .55f);
card.EnableKeyword("_ALPHATEST_ON"); card.SetFloat("_Cull", 0); card.renderQueue = 2450;
card.SetOverrideTag("RenderType", "TransparentCutout");
EditorUtility.SetDirty(card); AssetDatabase.SaveAssetIfDirty(card);
}
BuildGroundSurface();
return "Imported four Studio assets + XPbr, authored four component Prefabs and unlit background material.";
}
static void BuildGroundSurface()
{
Source(Path.Combine(Repo, "Doc/Design/Scene/HomeCamp/ground_surface.png"), Root + "/ground_surface.png");
TextureSettings(Root + "/ground_surface.png", true);
var mat = SaveMaterial(Root + "/ground_surface.mat", "Universal Render Pipeline/Unlit");
mat.SetTexture("_BaseMap", AssetDatabase.LoadAssetAtPath<Texture2D>(Root + "/ground_surface.png"));
mat.SetColor("_BaseColor", Color.white);
EditorUtility.SetDirty(mat); AssetDatabase.SaveAssetIfDirty(mat);
var mesh = AssetDatabase.LoadAssetAtPath<Mesh>(Root + "/GroundSurface.asset");
if (!mesh) { mesh = new Mesh(); AssetDatabase.CreateAsset(mesh, Root + "/GroundSurface.asset"); }
mesh.Clear(); mesh.name = "HomeCamp continuous ground surface";
mesh.vertices = new[] {new Vector3(-18,0,-21), new Vector3(-18,0,21), new Vector3(18,0,21), new Vector3(18,0,-21)};
mesh.uv = new[] {new Vector2(0,0),new Vector2(0,1),new Vector2(1,1),new Vector2(1,0)};
mesh.triangles = new[] {0,1,2,0,2,3}; mesh.RecalculateNormals(); mesh.RecalculateTangents(); mesh.RecalculateBounds();
EditorUtility.SetDirty(mesh); AssetDatabase.SaveAssetIfDirty(mesh);
var scene = EditorSceneManager.NewPreviewScene();
try
{
var go = new GameObject("GroundSurface"); SceneManager.MoveGameObjectToScene(go, scene);
go.AddComponent<MeshFilter>().sharedMesh = mesh; go.AddComponent<MeshRenderer>().sharedMaterial = mat;
go.AddComponent<MeshCollider>().sharedMesh = mesh;
PrefabUtility.SaveAsPrefabAsset(go, Root + "/GroundSurface.prefab");
}
finally { EditorSceneManager.ClosePreviewScene(scene); }
}
static GameObject Place(Transform parent, string path, string name, Vector3 pos, float size, bool width = false, bool faceCenter = false)
{
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (!prefab) throw new FileNotFoundException(path);
var go = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
go.name = name; go.transform.localPosition = Vector3.zero; go.transform.localRotation = Quaternion.identity;
go.transform.localScale = Vector3.one;
var b = BoundsOf(go);
var current = width ? Mathf.Max(b.size.x, b.size.z) : b.size.y;
go.transform.localScale = Vector3.one * (size / current);
if (faceCenter)
{ var away = new Vector3(pos.x, 0, pos.z - .8f); go.transform.localRotation = Quaternion.LookRotation(away, Vector3.up); }
go.transform.position = pos;
SetLayer(go);
return go;
}
static string PartPath(string part) => Root + "/" + part + "/" + part + ".prefab";
static string ReusePath(string part)
{
if (part == "tree" || part == "bush")
{
var name = part == "bush" ? "shrub" : "tree";
return "Assets/Bundles/Scenes/Prototype/Battlefield/A/Components/" + name + "/" + name + ".prefab";
}
return Town + "/" + part + "/" + part + ".prefab";
}
static float GroundY(GameObject ground, float x, float z)
{
var ray = new Ray(new Vector3(x, 60, z), Vector3.down);
var hits = ground.GetComponentsInChildren<MeshCollider>().Select(c =>
{ RaycastHit hit; return c.Raycast(ray, out hit, 120) ? hit.point.y : float.NegativeInfinity; }).ToArray();
var y = hits.Max();
if (float.IsNegativeInfinity(y)) throw new InvalidOperationException("Ground ray missed at " + x + "," + z);
return y;
}
static GameObject FoliageCard(Transform parent, string name, string image, Vector3 bottom, float height)
{
var go = GameObject.CreatePrimitive(PrimitiveType.Quad); go.name = name;
go.transform.SetParent(parent, false); Object.DestroyImmediate(go.GetComponent<Collider>());
var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(Root + "/" + image + ".png");
var rotation = Quaternion.LookRotation(new Vector3(0, 1.8f, 3) - new Vector3(0, 15, -27), Vector3.up);
go.transform.rotation = rotation;
go.transform.position = bottom + (rotation * Vector3.up) * height * .5f;
go.transform.localScale = new Vector3(height * texture.width / texture.height, height, 1);
var renderer = go.GetComponent<Renderer>(); renderer.sharedMaterial = AssetDatabase.LoadAssetAtPath<Material>(Root + "/" + image + ".mat");
renderer.shadowCastingMode = ShadowCastingMode.Off; renderer.receiveShadows = false;
SetLayer(go); return go;
}
static Camera MakeCamera(Scene scene, Transform parent)
{
var go = new GameObject("HomeCampCamera"); SceneManager.MoveGameObjectToScene(go, scene);
if (parent) go.transform.SetParent(parent, false);
var camera = go.AddComponent<Camera>();
go.transform.position = new Vector3(0, 15, -27);
go.transform.LookAt(new Vector3(0, 1.8f, 3));
camera.fieldOfView = 48; camera.nearClipPlane = .1f; camera.farClipPlane = 180;
camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(.46f, .72f, .88f);
camera.cullingMask = 1 << Layer; camera.scene = scene;
camera.allowHDR = false; camera.allowMSAA = true;
return camera;
}
static void Light(Scene scene, Transform parent)
{
var go = new GameObject("HomeCampSun"); SceneManager.MoveGameObjectToScene(go, scene);
if (parent) go.transform.SetParent(parent, false);
var light = go.AddComponent<Light>(); light.type = LightType.Directional;
light.color = new Color(1, .95f, .84f); light.intensity = 1.05f; light.shadows = LightShadows.Soft;
light.cullingMask = 1 << Layer; go.transform.rotation = Quaternion.Euler(43, -32, 0);
}
static void Background(Transform parent, Camera camera)
{
var go = GameObject.CreatePrimitive(PrimitiveType.Quad); go.name = "DistantLakeWaterfallCastle";
go.transform.SetParent(parent, false); Object.DestroyImmediate(go.GetComponent<Collider>());
var renderer = go.GetComponent<MeshRenderer>();
renderer.sharedMaterial = AssetDatabase.LoadAssetAtPath<Material>(Root + "/backdrop.mat");
renderer.shadowCastingMode = ShadowCastingMode.Off; renderer.receiveShadows = false;
const float distance = 75;
var halfHeight = distance * Mathf.Tan(camera.fieldOfView * Mathf.Deg2Rad * .5f);
var width = halfHeight * 2 * 1080f / 1920;
var height = width / 1.5f;
go.transform.position = camera.transform.position + camera.transform.forward * distance + camera.transform.up * (halfHeight - height * .5f);
go.transform.rotation = camera.transform.rotation;
go.transform.localScale = new Vector3(width * 1.04f, height * 1.04f, 1);
SetLayer(go);
}
public static string Build()
{
if (EditorApplication.isPlaying) throw new InvalidOperationException("Author in Edit Mode.");
var previous = SceneManager.GetActiveScene();
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive);
try
{
SceneManager.SetActiveScene(scene);
var root = new GameObject("HomeCamp");
var ground = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(Root + "/GroundSurface.prefab"), root.transform);
ground.name = "Ground"; SetLayer(ground);
// Final source ground must be horizontal; reject a vertical generated plate.
var gb = BoundsOf(ground);
if (gb.size.y > Mathf.Min(gb.size.x, gb.size.z) * .3f)
throw new InvalidOperationException("Ground requires offline orientation correction before assembly: " + gb.size);
Physics.SyncTransforms();
var placements = new List<string>();
void Put(string part, string name, float x, float z, float size, bool width = false, bool facing = false, bool reused = false)
{
var y = GroundY(ground, x, z);
if (reused && (part == "tree" || part == "bush"))
{
FoliageCard(root.transform, name, part == "tree" ? "tree_card" : "flower_card", new Vector3(x,y,z), size);
placements.Add(name + " card groundY=" + y.ToString("F3")); return;
}
var path = reused ? ReusePath(part) : PartPath(part);
var go = Place(root.transform, path, name, new Vector3(x, y, z), size, width, facing);
var bottom = BoundsOf(go).min.y;
go.transform.position += Vector3.up * (y - bottom);
placements.Add(name + " groundY=" + y.ToString("F3"));
}
Put("mainhouse", "MainHouse", 0, 7.2f, 6.5f, false, true);
Put("mainhouse", "HouseLeft", -7.5f, 4, 4, false, true);
Put("mainhouse", "HouseRight", 7.5f, 4, 4, false, true);
Put("fountain", "Fountain", 0, .8f, 4.4f, true);
Put("tree", "TreeBackLeft", -9, 8, 7, false, false, true);
Put("tree", "TreeBackRight", 9, 9, 6.5f, false, false, true);
Put("tree", "TreeRearLeft", -5.6f, 14, 4.8f, false, false, true);
Put("tree", "TreeRearRight", 5.6f, 14, 4.8f, false, false, true);
Put("tree", "TreeFrontLeft", -10, -4, 5, false, false, true);
Put("lamp", "LampLeft", -5.2f, -.5f, 2.6f, false, false, true);
Put("lamp", "LampRight", 5.2f, 1, 2.6f, false, false, true);
Put("lamp", "LampForeground", 5.5f, -6, 2.7f, false, false, true);
Put("bench", "BenchFrontLeft", -4.4f, -4, 2.2f, true, true, true);
for (int i = 0; i < 12; i++)
{
var angle = (i * 30f) * Mathf.Deg2Rad;
Put("bush", "FountainFlowers" + i, Mathf.Sin(angle) * 2.7f, .8f + Mathf.Cos(angle) * 2.7f, .55f, false, false, true);
}
foreach (var p in new[] { new Vector2(-7,-5), new Vector2(-5,-7), new Vector2(6,-6), new Vector2(8,-3), new Vector2(-9,2), new Vector2(9,2) })
Put("bush", "BorderFlowers", p.x, p.y, .9f, false, false, true);
var camera = MakeCamera(scene, root.transform);
Light(scene, root.transform); Background(root.transform, camera); SetLayer(root);
RenderSettings.ambientMode = AmbientMode.Trilight;
RenderSettings.ambientSkyColor = new Color(.68f, .75f, .82f);
RenderSettings.ambientEquatorColor = new Color(.55f, .57f, .52f);
RenderSettings.ambientGroundColor = new Color(.35f, .34f, .29f);
PrefabUtility.SaveAsPrefabAsset(root, PrefabPath);
EditorSceneManager.SaveScene(scene, ScenePath);
Directory.CreateDirectory(Review);
File.WriteAllText(Path.Combine(Review, "assembly.txt"), string.Join("\n", placements));
CaptureCamera(camera, 1080, 1920, "homecamp_portrait.png");
CaptureCamera(camera, 1080, 2400, "homecamp_tall.png");
return "Saved " + PrefabPath + " and " + ScenePath + "; " + placements.Count + " placements; portrait/tall rendered.";
}
finally
{
if (previous.IsValid() && previous.isLoaded) SceneManager.SetActiveScene(previous);
EditorSceneManager.CloseScene(scene, true);
}
}
static void CaptureCamera(Camera camera, int width, int height, string name)
{
Directory.CreateDirectory(Review);
var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
var old = RenderTexture.active; var oldTarget = camera.targetTexture; var oldAspect = camera.aspect;
var oldAsync = ShaderUtil.allowAsyncCompilation;
var texture = new Texture2D(width, height, TextureFormat.RGB24, false);
try
{
ShaderUtil.allowAsyncCompilation = false;
camera.aspect = (float)width / height; camera.targetTexture = rt; camera.Render(); camera.Render();
RenderTexture.active = rt; texture.ReadPixels(new Rect(0, 0, width, height), 0, 0); texture.Apply();
File.WriteAllBytes(Path.Combine(Review, name), texture.EncodeToPNG());
}
finally { ShaderUtil.allowAsyncCompilation = oldAsync; camera.targetTexture = oldTarget; camera.aspect = oldAspect; RenderTexture.active = old; RenderTexture.ReleaseTemporary(rt); Object.DestroyImmediate(texture); }
}
public static string CaptureParts() => CaptureSet(Parts);
public static string CaptureReuse() => CaptureSet(new[] { "tree", "lamp", "bench", "bush" });
static string CaptureSet(IEnumerable<string> parts)
{
foreach (var part in parts)
{
var scene = EditorSceneManager.NewPreviewScene();
try
{
var path = Parts.Contains(part) ? PartPath(part) : ReusePath(part);
var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(path), scene);
SetLayer(go); Light(scene, null);
var b = BoundsOf(go); var extent = Mathf.Max(b.size.x, Mathf.Max(b.size.y, b.size.z));
var camera = MakeCamera(scene, null); camera.orthographic = true; camera.orthographicSize = extent * .7f;
camera.nearClipPlane = Mathf.Max(.00001f, extent * .01f); camera.farClipPlane = Mathf.Max(10, extent * 10);
foreach (var direction in new[] { "front", "side", "back", "perspective" })
{
var offset = direction == "front" ? new Vector3(0,.15f,-3) : direction == "side" ? new Vector3(3,.15f,0) : direction == "back" ? new Vector3(0,.15f,3) : new Vector3(2,1.8f,-3);
camera.transform.position = b.center + offset * extent;
camera.transform.LookAt(b.center);
CaptureCamera(camera, 640, 640, part + "_" + direction + ".png");
}
}
finally { EditorSceneManager.ClosePreviewScene(scene); }
}
return "Rendered four views per component.";
}
public static string Verify()
{
var report = new List<string>();
foreach (var part in Parts)
{
var p = AssetDatabase.LoadAssetAtPath<GameObject>(PartPath(part));
if (!p) throw new InvalidOperationException("Missing component " + part);
var triangles = p.GetComponentsInChildren<MeshFilter>().Sum(f => (int)f.sharedMesh.GetIndexCount(0) / 3);
var receipt = JsonUtility.FromJson<Receipt>(File.ReadAllText(Path.Combine(Repo, "Tools/output/homecamp_studio", part, "production.json")));
if (triangles > receipt.targetTriangles * 1.3f) throw new InvalidOperationException("Triangle budget " + part);
foreach (var r in p.GetComponentsInChildren<Renderer>())
if (!r.sharedMaterial || r.sharedMaterial.shader.name != "Test/HomeCampXPbr") throw new InvalidOperationException("XPbr " + part);
report.Add(part + "=" + triangles);
}
var root = AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath);
if (!root || !File.Exists(ScenePath)) throw new InvalidOperationException("Missing assembled scene/prefab");
if (root.GetComponentsInChildren<MeshCollider>().Length == 0) throw new InvalidOperationException("Missing ground collider");
var preview = EditorSceneManager.NewPreviewScene();
var triangleCount = 0;
try
{
var instance = (GameObject)PrefabUtility.InstantiatePrefab(root, preview);
var ground = instance.transform.Find("Ground").gameObject;
Physics.SyncTransforms();
for (int x=-8; x<=8; x+=4) for (int z=-8; z<=8; z+=4) GroundY(ground,x,z);
triangleCount = instance.GetComponentsInChildren<MeshFilter>().Sum(f => (int)f.sharedMesh.GetIndexCount(0)/3);
}
finally { EditorSceneManager.ClosePreviewScene(preview); }
var backdrop = root.transform.Find("DistantLakeWaterfallCastle");
if (!backdrop || backdrop.GetComponent<Collider>()) throw new InvalidOperationException("Backdrop collider");
var bg = backdrop.GetComponent<Renderer>();
if (bg.sharedMaterial.shader.name != "Universal Render Pipeline/Unlit" || bg.shadowCastingMode != ShadowCastingMode.Off)
throw new InvalidOperationException("Backdrop must be unlit without shadows");
if (EditorBuildSettings.scenes.Any(s => s.path == ScenePath)) throw new InvalidOperationException("Review scene unexpectedly added to build");
Directory.CreateDirectory(Review);
File.WriteAllText(Path.Combine(Review, "verification.txt"), "PASS " + string.Join("; ", report) + "; assembledTriangles=" + triangleCount + "; ground collision 25/25; unlit quad backdrop; independent scene; cottage/Studio ground retained as review candidates and not placed");
return File.ReadAllText(Path.Combine(Review, "verification.txt"));
}
public static string OpenForReview()
{
var scene = SceneManager.GetSceneByPath(ScenePath);
if (!scene.IsValid() || !scene.isLoaded) scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Additive);
SceneManager.SetActiveScene(scene);
var root = scene.GetRootGameObjects().Single(); Selection.activeGameObject = root;
var camera = root.GetComponentInChildren<Camera>();
if (SceneView.lastActiveSceneView != null) SceneView.lastActiveSceneView.AlignViewToObject(camera.transform);
return "Opened HomeCamp additively; existing scenes preserved.";
}
}
#endif