Aetheria/Assets/Scripts/Editor/CultCacheDrawers.cs
Meta Vens 67cbf34b25 Switch engine asset references from Resources paths to Addressables GUIDs
Cut 2 of docs/addressables-cut.md. The asset move (git mv
Resources/{Prefabs,Schematics,Sprites/Icons} -> Content/...) and the
UnityHelpers.cs deletion already landed in 1fdf8151, picked up by a
concurrent session sharing this working tree; this commit is the rest
of Cut 2 landing on top of that shared state, atomically:

- Bump CultLib to cultlib-unity-v1.0.60 / caching-unity-v1.4.0 (both
  45c2f40) in Packages/manifest.json, packages-lock.json and
  Directory.Build.props, together with the 8 ServerShared
  [CultInspectorAssetPath] usages becoming [CultInspectorAssetGuid] -
  the .NET and Unity sides move together or nothing compiles.
- Assets/Scripts/EngineAssets.cs replaces UnityHelpers: loads an
  Addressables key (bare GUID or guid[subAsset]) synchronously,
  loading the GameObject and taking the component for the four sites
  that load a component. ActionBarSlot, EntityInstance, ShipInstance
  and ZoneRenderer call it instead of Resources.Load/UnityHelpers; the
  inline "Assets/Resources/" prefix strips are gone with them.
  CultCacheDrawers' schematic preview goes through
  AssetDatabase.GUIDToAssetPath first.
- Assets/Scripts/Editor/EngineAssetCheck.cs validates every stored
  [CultInspectorAssetGuid] value: 32 lowercase hex, resolves to a file,
  is addressable, and satisfies its member's consumer-type contract
  (Texture2D for icons/logos/schematics, a GameObject with the right
  component for prefabs) - table-driven, so an attributed member with
  no row fails loudly instead of being skipped.
- GameData/Aetheria.cc: the scratch migration (Aetheria.Shared + the
  pinned CultLib revision, a throwaway console app, never landed)
  walked all 138 attributed field slots across 180 documents,
  rewriting the 30 non-empty Resources paths to their .meta GUIDs -
  108 stayed empty, 0 unresolved, matching the map's census exactly.

Verification: dotnet test tests/Aetheria.Shared.Tests 61/61 green
against CultLib 45c2f40; Unity 6000.3.24f1 batchmode compiles clean;
EngineAssetCheck.Run passes all 30 stored references; the map's
negative greps are clean; tests/mutation_tests.py's 5 mutations (wrong
type, non-addressable, random guid, an old path, and an untabled
attributed member) are all caught and named, the control case and the
post-revert recompile stay clean, and the catalog/source are restored
byte-identical afterward.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 05:04:50 +02:00

147 lines
7.3 KiB
C#

using System;
using System.Linq;
using System.Reflection;
using CultMath;
using CultMath.UnityBridge;
using GameCult.Caching;
using GameCult.Unity.Caching.Editor;
using MessagePack;
using UnityEditor;
using UnityEngine;
using static CultMath.math;
// Aetheria's Studio drawers: the inspection meaning CultLib's CultInspector attributes do not carry. Each draws only
// the value types its attribute sits on and hands anything else back to built-in drawing.
// The simple name of one of the attribute type's [Union] members, as a string
[CultInspectorDrawer(typeof(InspectableTypeAttribute))]
public sealed class InspectableTypeDrawer : ICultInspectorDrawer
{
public object Draw(CultInspector inspector, string label, Type type, object value, MemberInfo member)
{
var attribute = member?.GetCustomAttribute<InspectableTypeAttribute>();
if (type != typeof(string) || attribute == null) return inspector.DrawDefault(label, type, value, member);
var names = attribute.Type.GetCustomAttributes<UnionAttribute>(false).Select(union => union.SubType.Name).ToArray();
var index = Array.IndexOf(names, value as string) + 1;
var picked = EditorGUILayout.Popup(label, index, names.Prepend("None").ToArray());
return picked == index ? value : picked == 0 ? null : names[picked - 1];
}
}
[CultInspectorDrawer(typeof(InspectableColorAttribute))]
public sealed class InspectableColorDrawer : ICultInspectorDrawer
{
public object Draw(CultInspector inspector, string label, Type type, object value, MemberInfo member)
{
if (type == typeof(float3)) return EditorGUILayout.ColorField(label, ((float3) value).ToColor()).ToFloat3();
if (type == typeof(float4)) return EditorGUILayout.ColorField(label, ((float4) value).ToColor()).ToCultMath();
return inspector.DrawDefault(label, type, value, member);
}
}
// Keyframes as (time, value, in tangent, out tangent), edited as a curve over the unit square
[CultInspectorDrawer(typeof(InspectableAnimationCurveAttribute))]
public sealed class InspectableAnimationCurveDrawer : ICultInspectorDrawer
{
public object Draw(CultInspector inspector, string label, Type type, object value, MemberInfo member)
{
float4[] keys;
if (type == typeof(float4[])) keys = value as float4[];
else if (type == typeof(BezierCurve)) keys = (value as BezierCurve)?.Keys;
else return inspector.DrawDefault(label, type, value, member);
EditorGUI.BeginChangeCheck();
var curve = EditorGUILayout.CurveField(label, keys?.Length > 0 ? keys.ToCurve() : new AnimationCurve(), Color.yellow, new Rect(0, 0, 1, 1));
if (!EditorGUI.EndChangeCheck()) return value;
var edited = curve.keys.Select(k => float4(k.time, k.value, k.inTangent, k.outTangent)).ToArray();
if (type == typeof(float4[])) return edited;
var bezier = value as BezierCurve ?? new BezierCurve();
bezier.Keys = edited;
return bezier;
}
}
// One Kelvin value shown and edited as Kelvin, Celsius and Fahrenheit
[CultInspectorDrawer(typeof(InspectableTemperatureAttribute))]
public sealed class InspectableTemperatureDrawer : ICultInspectorDrawer
{
public object Draw(CultInspector inspector, string label, Type type, object value, MemberInfo member)
{
if (type != typeof(float)) return inspector.DrawDefault(label, type, value, member);
var kelvin = (float) value;
EditorGUI.BeginChangeCheck();
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.PrefixLabel(label);
var indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
GUILayout.Label("°K", EditorStyles.miniLabel, GUILayout.Width(20));
kelvin = EditorGUILayout.DelayedFloatField(kelvin);
GUILayout.Label("°C", EditorStyles.miniLabel, GUILayout.Width(20));
kelvin = EditorGUILayout.DelayedFloatField(kelvin - 273.15f) + 273.15f;
GUILayout.Label("°F", EditorStyles.miniLabel, GUILayout.Width(20));
kelvin = (EditorGUILayout.DelayedFloatField((kelvin - 273.15f) * 1.8f + 32) - 32) / 1.8f + 273.15f;
EditorGUI.indentLevel = indent;
}
return EditorGUI.EndChangeCheck() ? kelvin : value;
}
}
// A size and a toggle per cell. When the owning item has a schematic texture, the texture lies under the grid and the
// height follows its aspect; a hull's hardpoint cells are tinted. Both read the owning record through
// inspector.Record, which is a read-only copy: nothing here writes to it.
[CultInspectorDrawer(typeof(InspectableSchematicShapeAttribute))]
public sealed class InspectableSchematicShapeDrawer : ICultInspectorDrawer
{
private static Material _schematicMaterial;
public object Draw(CultInspector inspector, string label, Type type, object value, MemberInfo member)
{
if (type != typeof(Shape)) return inspector.DrawDefault(label, type, value, member);
var shape = value as Shape ?? new Shape();
var item = inspector.Record as EquippableItemData;
var hull = item as HullData;
var schematic = string.IsNullOrEmpty(item?.Schematic) ? null : AssetDatabase.LoadAssetAtPath<Texture2D>(AssetDatabase.GUIDToAssetPath(item.Schematic));
EditorGUILayout.LabelField(label);
using (new EditorGUILayout.HorizontalScope())
{
var width = max(EditorGUILayout.DelayedIntField("Width", shape.Width), 1);
int height;
if (schematic == null) height = max(EditorGUILayout.DelayedIntField("Height", shape.Height), 1);
else
{
height = max(Mathf.RoundToInt(width * ((float) schematic.height / schematic.width)), 1);
EditorGUILayout.LabelField("Height", height.ToString());
}
if (width != shape.Width || height != shape.Height) shape.Resize(width, height);
}
// The flexible space shrinks the grid group to its toggles, so the texture fits the grid, not the inspector width.
using (new EditorGUILayout.HorizontalScope())
{
using (var grid = new EditorGUILayout.VerticalScope())
{
if (schematic != null && Event.current.type == EventType.Repaint)
{
_schematicMaterial ??= new Material(Shader.Find("Legacy Shaders/Particles/Additive"));
EditorGUI.DrawPreviewTexture(grid.rect, schematic, _schematicMaterial);
}
for (var y = shape.Height - 1; y >= 0; y--)
using (new EditorGUILayout.HorizontalScope())
for (var x = 0; x < shape.Width; x++)
{
var hardpoint = hull?.Hardpoints.FirstOrDefault(hp => hp.Shape.Coordinates
.Any(v => v.x + hp.Position.x == x && v.y + hp.Position.y == y));
var previous = GUI.backgroundColor;
if (hardpoint != null) GUI.backgroundColor = hardpoint.TintColor.ToColor();
shape[int2(x, y)] = GUILayout.Toggle(shape[int2(x, y)], GUIContent.none, GUILayout.Width(16));
GUI.backgroundColor = previous;
}
}
GUILayout.FlexibleSpace();
}
return shape;
}
}