Ship S2: the game composes and loads mod ships
Boot opens the derived catalog when GameData/Mods holds packages (ShipModCatalog.ResolveCatalog), preloads each hull's GLB into an assembled inactive prototype (ShipModShips), and LoadEntity clones it when the hull names a Visual. ShipModPlan decides which anchor fills which ShipInstance slot; a ShipModTemplate prefab carries the shared effects. Continue refuses a run that names a design the catalog no longer holds (RunSave.RequireDesigns). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f470c1b775
commit
460bb85ed0
16 changed files with 702 additions and 13 deletions
|
|
@ -1,10 +1,16 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
public static class ShipModPreview
|
||||
{
|
||||
// Batch proof: -executeMethod ShipModPreview.Smoke -shipModPath <package/ship.cc>
|
||||
private const string TemplatePath = "Assets/Content/Prefabs/ShipModTemplate.prefab";
|
||||
|
||||
// Batch proof: -executeMethod ShipModPreview.Smoke -shipModPath <package/ship.cc> [-assemble]
|
||||
// -assemble also builds the game's ShipInstance from the package (ShipModShips.Assemble) and checks it against the
|
||||
// plan and against a clone, the way Instantiate makes one. That needs a GLB with real meshes on the map-icon,
|
||||
// hull-collider and emitter anchors; a nodes-only fixture fails it on purpose.
|
||||
public static async void Smoke()
|
||||
{
|
||||
var args = Environment.GetCommandLineArgs();
|
||||
|
|
@ -18,10 +24,12 @@ public static class ShipModPreview
|
|||
ShipModVisual.Instance visual = null;
|
||||
try
|
||||
{
|
||||
visual = await ShipModVisual.LoadAsync(args[option + 1], null);
|
||||
var package = ShipModCatalog.ReadPackage(args[option + 1]);
|
||||
visual = await ShipModVisual.LoadAsync(package, null);
|
||||
if (visual.Anchors.Count == 0 || visual.LineMesh == null || visual.LineMesh.vertexCount == 0)
|
||||
throw new InvalidOperationException("Mod visual did not import anchors and line points.");
|
||||
Debug.Log($"SHIP_MOD_VISUAL_SMOKE anchors={visual.Anchors.Count} points={visual.LineMesh.vertexCount} segments={visual.LineMesh.GetIndexCount(0) / 2}");
|
||||
if (Array.IndexOf(args, "-assemble") >= 0) CheckAssembly(visual, package);
|
||||
visual.Destroy();
|
||||
EditorApplication.Exit(0);
|
||||
}
|
||||
|
|
@ -33,6 +41,41 @@ public static class ShipModPreview
|
|||
}
|
||||
}
|
||||
|
||||
private static void CheckAssembly(ShipModVisual.Instance visual, ShipModCatalog.Package package)
|
||||
{
|
||||
var template = AssetDatabase.LoadAssetAtPath<ShipModTemplate>(TemplatePath);
|
||||
if (template == null) throw new InvalidOperationException($"{TemplatePath} is missing or has no ShipModTemplate.");
|
||||
var plan = ShipModPlan.Build(package.Hull, package.Visual);
|
||||
var ship = ShipModShips.Assemble(visual, package.Hull, package.Visual, template);
|
||||
CheckShip(ship, plan, "assembled");
|
||||
|
||||
// Instantiate clones the assembled root, and Unity must remap every array reference into the clone.
|
||||
var clone = UnityEngine.Object.Instantiate(ship.gameObject).GetComponent<ShipInstance>();
|
||||
try { CheckShip(clone, plan, "clone"); }
|
||||
finally { UnityEngine.Object.DestroyImmediate(clone.gameObject); }
|
||||
Debug.Log($"SHIP_MOD_ASSEMBLY_SMOKE equipment={plan.Equipment.Length} thrusters={plan.Thrusters.Length} " +
|
||||
$"weapons={plan.Weapons.Length} radiators={plan.Radiators.Length}");
|
||||
}
|
||||
|
||||
private static void CheckShip(ShipInstance ship, ShipModPlan plan, string what)
|
||||
{
|
||||
void Require(bool ok, string claim) { if (!ok) throw new InvalidOperationException($"{what}: {claim}"); }
|
||||
bool Inside(Component part) => part != null && part.transform.IsChildOf(ship.transform);
|
||||
Require(ship.HullColliders.Length == 1 && Inside(ship.HullColliders[0]) &&
|
||||
ship.HullColliders[0].GetComponent<MeshCollider>() != null, "one hull collider with a MeshCollider");
|
||||
Require(Inside(ship.MapIcon) && Inside(ship.Shield) && Inside(ship.TractorBeam), "map icon, shield and tractor inside the ship");
|
||||
Require(ship.EquipmentHardpoints.Select(node => node.name).SequenceEqual(plan.Equipment) &&
|
||||
ship.EquipmentHardpoints.All(Inside), "equipment hardpoints named by the plan");
|
||||
Require(ship.ThrusterHardpoints.Select(node => node.name).SequenceEqual(plan.Thrusters) &&
|
||||
ship.ThrusterHardpoints.All(node => Inside(node) && Inside(node.Emitter)), "thruster hardpoints with emitters");
|
||||
Require(ship.RadiatorHardpoints.Select(node => node.name).SequenceEqual(plan.Radiators) &&
|
||||
ship.RadiatorHardpoints.All(node => Inside(node) && Inside(node.Mesh)), "radiator hardpoints with meshes");
|
||||
Require(ship.WeaponHardpoints.Select(node => node.name).SequenceEqual(plan.Weapons.Select(weapon => weapon.Mount)) &&
|
||||
ship.WeaponHardpoints.Zip(plan.Weapons, (node, weapon) =>
|
||||
Inside(node) && node.FiringPoint.Select(point => point.name).SequenceEqual(weapon.Muzzles) &&
|
||||
node.FiringPoint.All(Inside)).All(ok => ok), "weapon hardpoints with their muzzles in order");
|
||||
}
|
||||
|
||||
[MenuItem("Aetheria/Preview Mod Ship Package")]
|
||||
private static async void Open()
|
||||
{
|
||||
|
|
@ -40,7 +83,7 @@ public static class ShipModPreview
|
|||
if (string.IsNullOrEmpty(path)) return;
|
||||
try
|
||||
{
|
||||
var visual = await ShipModVisual.LoadAsync(path, null);
|
||||
var visual = await ShipModVisual.LoadAsync(ShipModCatalog.ReadPackage(path), null);
|
||||
Undo.RegisterCreatedObjectUndo(visual.Root, "Preview mod ship");
|
||||
Selection.activeGameObject = visual.Root;
|
||||
SceneView.FrameLastActiveSceneView();
|
||||
|
|
|
|||
Loading…
Reference in a new issue