From 3bab9638c9b65b9a25de0aaf6ad25229c6ed0504 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 16:19:11 +0100 Subject: [PATCH 001/292] Restore Eve input capability contract --- .../Runtime/EveInputCapabilityDocument.cs | 59 +++++++++++++++++++ .../EveInputCapabilityDocument.cs.meta | 2 + 2 files changed, 61 insertions(+) create mode 100644 packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs create mode 100644 packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs.meta diff --git a/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs b/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs new file mode 100644 index 0000000..356edee --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using GameCult.Caching; +using MessagePack; + +namespace GameCult.Eve.Surface +{ + [CultDocument("gamecult.eve.input_capability", SchemaId)] + [MessagePackObject] + public sealed class EveInputCapabilityDocument + { + public const string SchemaId = "gamecult.eve.input_capability.v1"; + [Key(0)] public string Schema { get; set; } = SchemaId; + [Key(1)] public string ProviderId { get; set; } = ""; + [Key(2)] public string CapabilityId { get; set; } = ""; + [Key(3)] public long Version { get; set; } + [Key(4)] public EveInputActionDocument[] Actions { get; set; } = Array.Empty(); + [Key(5)] public EveInputProfileDocument[] DefaultProfiles { get; set; } = Array.Empty(); + } + + [MessagePackObject] + public sealed class EveInputActionDocument + { + [Key(0)] public string ActionId { get; set; } = ""; + [Key(1)] public string Label { get; set; } = ""; + [Key(2)] public string Operation { get; set; } = ""; + [Key(3)] public string Context { get; set; } = "pilot"; + [Key(4)] public string Category { get; set; } = "ship"; + [Key(5)] public string Availability { get; set; } = "available"; + [Key(6)] public string SourceRef { get; set; } = ""; + [Key(7)] public Dictionary Payload { get; set; } = new Dictionary(StringComparer.Ordinal); + } + + [MessagePackObject] + public sealed class EveInputProfileDocument + { + [Key(0)] public string ProfileId { get; set; } = ""; + [Key(1)] public string DeviceClass { get; set; } = ""; + [Key(2)] public EveInputBindingDocument[] Bindings { get; set; } = Array.Empty(); + } + + [MessagePackObject] + public sealed class EveInputBindingDocument + { + [Key(0)] public string BindingId { get; set; } = ""; + [Key(1)] public string ActionId { get; set; } = ""; + [Key(2)] public EveInputGestureDocument Gesture { get; set; } = new EveInputGestureDocument(); + [Key(3)] public bool ActionBar { get; set; } + } + + [MessagePackObject] + public sealed class EveInputGestureDocument + { + [Key(0)] public string Kind { get; set; } = "direct"; + [Key(1)] public string[] Controls { get; set; } = Array.Empty(); + [Key(2)] public int MaxStepIntervalMs { get; set; } = 650; + [Key(3)] public string CompletionControl { get; set; } = ""; + } +} diff --git a/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs.meta b/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs.meta new file mode 100644 index 0000000..f13f766 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4c297c7dbb64a0f49a7f805e770c5e55 From 511782ca4210e304be2650320c3c990886791aec Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 16:53:21 +0100 Subject: [PATCH 002/292] Read legacy Eve surface contracts --- .../Runtime/EveSurfaceDocument.cs | 126 ++++++++++++++++- .../Runtime/GameCult.Eve.Surface.asmdef | 3 +- .../.gitignore | 1 + ...eSurfaceSerializationCompatibilityTests.cs | 129 ++++++++++++++++++ ...ult.Eve.Surface.Serialization.Tests.csproj | 20 +++ 5 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 tests/GameCult.Eve.Surface.Serialization.Tests/.gitignore create mode 100644 tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs create mode 100644 tests/GameCult.Eve.Surface.Serialization.Tests/GameCult.Eve.Surface.Serialization.Tests.csproj diff --git a/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs index c4eb051..9eed51f 100644 --- a/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs +++ b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs @@ -4,6 +4,7 @@ using System.Linq; using GameCult.Caching; using GameCult.Mesh; using MessagePack; +using MessagePack.Formatters; #nullable enable @@ -11,6 +12,7 @@ namespace GameCult.Eve.Surface { [CultDocument("gamecult.eve.surface", "gamecult.eve.surface.v1")] [MessagePackObject] + [MessagePackFormatter(typeof(EveSurfaceDocumentCompatibilityFormatter))] public sealed class EveSurfaceDocument { public const string DefaultType = "surface-state"; @@ -289,6 +291,7 @@ namespace GameCult.Eve.Surface } [MessagePackObject] + [MessagePackFormatter(typeof(EveCommandTemplateCompatibilityFormatter))] public sealed class EveCommandTemplate { public EveCommandTemplate(CultMeshOperationBindingDescriptor operation) @@ -318,7 +321,7 @@ namespace GameCult.Eve.Surface public string Transport => Operation.RouteHint.Description ?? ""; } - [CultDocument("gamecult.eve.command", SchemaId)] + [CultDocument("gamecult.eve.command_invocation", SchemaId)] [MessagePackObject] public sealed class EveSurfaceCommandRequest { @@ -411,4 +414,125 @@ namespace GameCult.Eve.Surface [IgnoreMember] public string CommandId => Operation.IdempotencyKey ?? ""; } + + /// + /// Reads the current Eve surface envelope and the original seven-field v1 envelope. + /// + public sealed class EveSurfaceDocumentCompatibilityFormatter : IMessagePackFormatter + { + private const int LegacyFieldCount = 7; + private const int CurrentFieldCount = 9; + + public void Serialize(ref MessagePackWriter writer, EveSurfaceDocument? value, MessagePackSerializerOptions options) + { + if (value == null) { writer.WriteNil(); return; } + writer.WriteArrayHeader(CurrentFieldCount); + writer.Write(value.Type); + writer.Write(value.Schema); + writer.Write(value.ProviderId); + writer.Write(value.ProviderKind); + writer.Write(value.Title); + writer.Write(value.Version); + writer.Write(value.UpdatedAtUtc); + Formatter(options).Serialize(ref writer, value.Surface, options); + Formatter>(options).Serialize(ref writer, value.Commands, options); + } + + public EveSurfaceDocument? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + if (reader.TryReadNil()) return null; + if (reader.NextMessagePackType != MessagePackType.Array) + throw new MessagePackSerializationException("Eve surface document must be an array."); + + options.Security.DepthStep(ref reader); + try + { + var fields = reader.ReadArrayHeader(); + if (fields == LegacyFieldCount) + { + return new EveSurfaceDocument( + ReadString(ref reader), ReadString(ref reader), ReadString(ref reader), + reader.ReadInt64(), ReadString(ref reader), + Formatter(options).Deserialize(ref reader, options)!, + Formatter>(options).Deserialize(ref reader, options) + ?? Array.Empty()); + } + + var type = ReadString(ref reader, fields, 0); + var schema = ReadString(ref reader, fields, 1); + var providerId = ReadString(ref reader, fields, 2); + var providerKind = ReadString(ref reader, fields, 3); + var title = ReadString(ref reader, fields, 4); + var version = fields > 5 ? reader.ReadInt64() : 0; + var updatedAtUtc = ReadString(ref reader, fields, 6); + var surface = fields > 7 ? Formatter(options).Deserialize(ref reader, options) : null; + var commands = fields > 8 ? Formatter>(options).Deserialize(ref reader, options) : null; + for (var index = CurrentFieldCount; index < fields; index++) reader.Skip(); + return new EveSurfaceDocument( + type, schema, providerId, providerKind, title, version, updatedAtUtc, + surface ?? throw new MessagePackSerializationException("Eve surface tree is required."), + commands ?? Array.Empty()); + } + finally { reader.Depth--; } + } + + private static IMessagePackFormatter Formatter(MessagePackSerializerOptions options) => + options.Resolver.GetFormatter() + ?? throw new MessagePackSerializationException($"No formatter is registered for {typeof(T).FullName}."); + + private static string ReadString(ref MessagePackReader reader) => reader.ReadString() ?? ""; + private static string ReadString(ref MessagePackReader reader, int fields, int index) => + index < fields ? ReadString(ref reader) : ""; + } + + /// + /// Reads both direct legacy command fields and the current wrapped binding record. + /// + public sealed class EveCommandTemplateCompatibilityFormatter : IMessagePackFormatter + { + public void Serialize(ref MessagePackWriter writer, EveCommandTemplate? value, MessagePackSerializerOptions options) + { + if (value == null) { writer.WriteNil(); return; } + writer.WriteArrayHeader(1); + BindingFormatter(options).Serialize(ref writer, value.OperationRecord, options); + } + + public EveCommandTemplate? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + if (reader.TryReadNil()) return null; + if (reader.NextMessagePackType != MessagePackType.Array) + throw new MessagePackSerializationException("Eve command template must be an array."); + + options.Security.DepthStep(ref reader); + try + { + var fields = reader.ReadArrayHeader(); + if (fields == 1 && reader.NextMessagePackType == MessagePackType.Array) + { + return new EveCommandTemplate( + BindingFormatter(options).Deserialize(ref reader, options) + ?? new CultMeshOperationBindingRecord()); + } + + var operationId = ReadString(ref reader, fields, 0); + var label = ReadString(ref reader, fields, 1); + var schemaId = ReadString(ref reader, fields, 2); + var routeKind = ReadString(ref reader, fields, 3); + var routeDescription = ReadString(ref reader, fields, 4); + for (var index = 5; index < fields; index++) reader.Skip(); + return new EveCommandTemplate(new CultMeshOperationBindingRecord( + operationId, label, schemaId, routeKind, routeDescription)); + } + finally { reader.Depth--; } + } + + private static IMessagePackFormatter BindingFormatter( + MessagePackSerializerOptions options) => + options.Resolver.GetFormatter() + ?? throw new MessagePackSerializationException("No CultMesh operation binding formatter is registered."); + + private static string ReadString(ref MessagePackReader reader, int fields, int index) => + index < fields ? reader.ReadString() ?? "" : ""; + } + } diff --git a/packages/org.gamecult.eve.surface/Runtime/GameCult.Eve.Surface.asmdef b/packages/org.gamecult.eve.surface/Runtime/GameCult.Eve.Surface.asmdef index 8041c88..7f19662 100644 --- a/packages/org.gamecult.eve.surface/Runtime/GameCult.Eve.Surface.asmdef +++ b/packages/org.gamecult.eve.surface/Runtime/GameCult.Eve.Surface.asmdef @@ -11,7 +11,8 @@ "precompiledReferences": [ "GameCult.Caching.dll", "GameCult.Mesh.dll", - "MessagePack.Annotations.dll" + "MessagePack.Annotations.dll", + "MessagePack.dll" ], "autoReferenced": true, "defineConstraints": [], diff --git a/tests/GameCult.Eve.Surface.Serialization.Tests/.gitignore b/tests/GameCult.Eve.Surface.Serialization.Tests/.gitignore new file mode 100644 index 0000000..296b87c --- /dev/null +++ b/tests/GameCult.Eve.Surface.Serialization.Tests/.gitignore @@ -0,0 +1 @@ +!GameCult.Eve.Surface.Serialization.Tests.csproj diff --git a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs new file mode 100644 index 0000000..b198b2e --- /dev/null +++ b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs @@ -0,0 +1,129 @@ +using System.Buffers; +using GameCult.Eve.Surface; +using GameCult.Mesh; +using MessagePack; +using NUnit.Framework; + +namespace GameCult.Eve.Surface.Tests; + +[TestFixture] +public sealed class EveSurfaceSerializationCompatibilityTests +{ + private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard; + + [Test] + public void LegacySevenFieldSurfaceDocumentDeserializes() + { + var bytes = WriteLegacySurfaceDocument(); + + var document = MessagePackSerializer.Deserialize(bytes, Options); + + Assert.Multiple(() => + { + Assert.That(document.Type, Is.EqualTo(EveSurfaceDocument.DefaultType)); + Assert.That(document.Schema, Is.EqualTo(EveSurfaceDocument.SchemaId)); + Assert.That(document.ProviderId, Is.EqualTo("legacy-provider")); + Assert.That(document.ProviderKind, Is.EqualTo("legacy-kind")); + Assert.That(document.Title, Is.EqualTo("Legacy surface")); + Assert.That(document.Version, Is.EqualTo(17)); + Assert.That(document.UpdatedAtUtc, Is.EqualTo("2026-07-13T12:00:00Z")); + Assert.That(document.Surface.Id, Is.EqualTo("legacy-surface")); + Assert.That(document.Commands, Has.Count.EqualTo(1)); + Assert.That(document.Commands[0].Command, Is.EqualTo("legacy.execute")); + }); + } + + [Test] + public void LegacyDirectFiveStringCommandTemplateDeserializes() + { + var bytes = WriteLegacyCommandTemplate(); + + var command = MessagePackSerializer.Deserialize(bytes, Options); + + Assert.Multiple(() => + { + Assert.That(command.Command, Is.EqualTo("legacy.execute")); + Assert.That(command.Label, Is.EqualTo("Execute legacy command")); + Assert.That(command.Operation.SchemaId, Is.EqualTo("gamecult.legacy.command.v1")); + Assert.That(command.OperationRecord.RouteKind, Is.EqualTo("cultmesh")); + Assert.That(command.OperationRecord.RouteDescription, Is.EqualTo("legacy.commands")); + Assert.That(command.Transport, Is.EqualTo("legacy.commands")); + }); + } + + [Test] + public void CurrentSerializationWritesNineFieldSurfaceAndWrappedStructuredCommandBinding() + { + var document = CreateCurrentDocument(); + + var bytes = MessagePackSerializer.Serialize(document, Options); + var reader = new MessagePackReader(bytes); + + Assert.That(reader.ReadArrayHeader(), Is.EqualTo(9)); + for (var field = 0; field < 8; field++) + reader.Skip(); + Assert.That(reader.ReadArrayHeader(), Is.EqualTo(1), "commands collection"); + Assert.That(reader.ReadArrayHeader(), Is.EqualTo(1), "command template wrapper"); + Assert.That(reader.NextMessagePackType, Is.EqualTo(MessagePackType.Array), "structured operation binding"); + + var roundTrip = MessagePackSerializer.Deserialize(bytes, Options); + Assert.That(roundTrip.Commands[0].Command, Is.EqualTo("current.execute")); + } + + private static EveSurfaceDocument CreateCurrentDocument() => new( + "current-provider", + "current-kind", + "Current surface", + 23, + "2026-07-13T13:00:00Z", + CreateSurfaceTree("current-surface"), + new[] + { + new EveCommandTemplate(new CultMeshOperationBindingRecord( + "current.execute", + "Execute current command", + "gamecult.current.command.v1", + "cultmesh", + "current.commands")) + }); + + private static EveSurfaceTree CreateSurfaceTree(string id) => new( + id, + new EveSurfaceComponent( + "root", + "column", + new Dictionary(), + Array.Empty()), + Array.Empty()); + + private static byte[] WriteLegacySurfaceDocument() + { + var buffer = new ArrayBufferWriter(); + var writer = new MessagePackWriter(buffer); + writer.WriteArrayHeader(7); + writer.Write("legacy-provider"); + writer.Write("legacy-kind"); + writer.Write("Legacy surface"); + writer.Write(17L); + writer.Write("2026-07-13T12:00:00Z"); + writer.WriteRaw(MessagePackSerializer.Serialize(CreateSurfaceTree("legacy-surface"), Options)); + writer.WriteArrayHeader(1); + writer.WriteRaw(WriteLegacyCommandTemplate()); + writer.Flush(); + return buffer.WrittenSpan.ToArray(); + } + + private static byte[] WriteLegacyCommandTemplate() + { + var buffer = new ArrayBufferWriter(); + var writer = new MessagePackWriter(buffer); + writer.WriteArrayHeader(5); + writer.Write("legacy.execute"); + writer.Write("Execute legacy command"); + writer.Write("gamecult.legacy.command.v1"); + writer.Write("cultmesh"); + writer.Write("legacy.commands"); + writer.Flush(); + return buffer.WrittenSpan.ToArray(); + } +} diff --git a/tests/GameCult.Eve.Surface.Serialization.Tests/GameCult.Eve.Surface.Serialization.Tests.csproj b/tests/GameCult.Eve.Surface.Serialization.Tests/GameCult.Eve.Surface.Serialization.Tests.csproj new file mode 100644 index 0000000..e3c947a --- /dev/null +++ b/tests/GameCult.Eve.Surface.Serialization.Tests/GameCult.Eve.Surface.Serialization.Tests.csproj @@ -0,0 +1,20 @@ + + + net10.0 + enable + enable + false + true + obj\bin\ + + + + + + + + + + + + From 7da47f04db9042c3ba3e1911f7c88ff9f254fc33 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 17:21:04 +0100 Subject: [PATCH 003/292] Read legacy Eve provider advertisements --- .../Runtime/EveProviderDocuments.cs | 146 ++++++++++++++++++ ...eSurfaceSerializationCompatibilityTests.cs | 77 +++++++++ 2 files changed, 223 insertions(+) diff --git a/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs b/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs index e9bb1da..874ace8 100644 --- a/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs +++ b/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using GameCult.Caching; using MessagePack; +using MessagePack.Formatters; #nullable enable @@ -9,6 +10,7 @@ namespace GameCult.Eve.Surface { [CultDocument("gamecult.eve.provider_advertisement", SchemaId)] [MessagePackObject] + [MessagePackFormatter(typeof(EveProviderAdvertisementCompatibilityFormatter))] public sealed class EveProviderAdvertisementDocument { public const string SchemaId = "gamecult.eve.provider_advertisement.v1"; @@ -76,6 +78,150 @@ namespace GameCult.Eve.Surface [Key(12)] public IReadOnlyList Commands { get; } } + /// + /// Reads canonical provider advertisements and the original Aetheria-expanded v1 layout. + /// + public sealed class EveProviderAdvertisementCompatibilityFormatter : + IMessagePackFormatter + { + private const int CurrentFieldCount = 13; + private const int LegacyFieldCount = 16; + + public void Serialize( + ref MessagePackWriter writer, + EveProviderAdvertisementDocument? value, + MessagePackSerializerOptions options) + { + if (value == null) { writer.WriteNil(); return; } + writer.WriteArrayHeader(CurrentFieldCount); + writer.Write(value.Schema); + writer.Write(value.ProviderId); + writer.Write(value.ServiceId); + writer.Write(value.VerseId); + writer.Write(value.Title); + writer.Write(value.Kind); + writer.Write(value.CultMeshAddress); + writer.Write(value.UpdatedAtUtc); + Formatter(options).Serialize(ref writer, value.Freshness, options); + Formatter>(options).Serialize(ref writer, value.Schemas, options); + Formatter>(options).Serialize(ref writer, value.Witnesses, options); + Formatter>(options).Serialize(ref writer, value.Surfaces, options); + Formatter>(options).Serialize(ref writer, value.Commands, options); + } + + public EveProviderAdvertisementDocument? Deserialize( + ref MessagePackReader reader, + MessagePackSerializerOptions options) + { + if (reader.TryReadNil()) return null; + if (reader.NextMessagePackType != MessagePackType.Array) + throw new MessagePackSerializationException("Eve provider advertisement must be an array."); + + options.Security.DepthStep(ref reader); + try + { + var fields = reader.ReadArrayHeader(); + if (fields == LegacyFieldCount) + return ReadLegacy(ref reader, options); + + var schema = ReadString(ref reader, fields, 0); + var providerId = ReadString(ref reader, fields, 1); + var serviceId = ReadString(ref reader, fields, 2); + var verseId = ReadString(ref reader, fields, 3); + var title = ReadString(ref reader, fields, 4); + var kind = ReadString(ref reader, fields, 5); + var address = ReadString(ref reader, fields, 6); + var updatedAtUtc = ReadString(ref reader, fields, 7); + var freshness = fields > 8 ? Formatter(options).Deserialize(ref reader, options) : null; + var schemas = fields > 9 ? Formatter>(options).Deserialize(ref reader, options) : null; + var witnesses = fields > 10 ? Formatter>(options).Deserialize(ref reader, options) : null; + var surfaces = fields > 11 ? Formatter>(options).Deserialize(ref reader, options) : null; + var commands = fields > 12 ? Formatter>(options).Deserialize(ref reader, options) : null; + for (var index = CurrentFieldCount; index < fields; index++) reader.Skip(); + return new EveProviderAdvertisementDocument( + schema, providerId, serviceId, verseId, title, kind, address, updatedAtUtc, + freshness ?? new EveProviderFreshness("unknown", "", 0), + schemas ?? Array.Empty(), + witnesses ?? Array.Empty(), + surfaces ?? Array.Empty(), + commands ?? Array.Empty()); + } + finally { reader.Depth--; } + } + + private static EveProviderAdvertisementDocument ReadLegacy( + ref MessagePackReader reader, + MessagePackSerializerOptions options) + { + var schema = ReadString(ref reader); + var providerId = ReadString(ref reader); + var serviceId = ReadString(ref reader); + var verseId = ReadString(ref reader); + reader.Skip(); // RootVerse remains provider-owned settings, not portable advertisement state. + reader.Skip(); // CanonicalService + reader.Skip(); // LocatedService + var address = ReadString(ref reader); + var title = ReadString(ref reader); + var kind = ReadString(ref reader); + var updatedAtUtc = ReadString(ref reader); + var freshness = Formatter(options).Deserialize(ref reader, options) + ?? new EveProviderFreshness("unknown", "", 0); + var schemas = Formatter>(options).Deserialize(ref reader, options) + ?? Array.Empty(); + var witnesses = Formatter>(options).Deserialize(ref reader, options) + ?? Array.Empty(); + var surfaces = ReadLegacySurfaces(ref reader); + var commands = ReadLegacyCommands(ref reader); + return new EveProviderAdvertisementDocument( + schema, providerId, serviceId, verseId, title, kind, address, updatedAtUtc, + freshness, schemas, witnesses, surfaces, commands); + } + + private static EveAdvertisedSurface[] ReadLegacySurfaces(ref MessagePackReader reader) + { + var count = reader.ReadArrayHeader(); + var values = new EveAdvertisedSurface[count]; + for (var index = 0; index < count; index++) + { + var fields = reader.ReadArrayHeader(); + var schema = ReadString(ref reader, fields, 0); + var surfaceId = ReadString(ref reader, fields, 1); + var recordRef = ReadString(ref reader, fields, 2); + var transport = ReadString(ref reader, fields, 3); + var status = ReadString(ref reader, fields, 4); + for (var field = 5; field < fields; field++) reader.Skip(); + values[index] = new EveAdvertisedSurface( + surfaceId, schema, recordRef, transport, status, "", null); + } + return values; + } + + private static EveAdvertisedCommand[] ReadLegacyCommands(ref MessagePackReader reader) + { + var count = reader.ReadArrayHeader(); + var values = new EveAdvertisedCommand[count]; + for (var index = 0; index < count; index++) + { + var fields = reader.ReadArrayHeader(); + var command = ReadString(ref reader, fields, 0); + var transport = ReadString(ref reader, fields, 1); + var summary = ReadString(ref reader, fields, 2); + for (var field = 3; field < fields; field++) reader.Skip(); + values[index] = new EveAdvertisedCommand(command, "", transport, summary); + } + return values; + } + + private static IMessagePackFormatter Formatter(MessagePackSerializerOptions options) => + options.Resolver.GetFormatter() + ?? throw new MessagePackSerializationException($"No formatter is registered for {typeof(T).FullName}."); + + private static string ReadString(ref MessagePackReader reader) => reader.ReadString() ?? ""; + private static string ReadString(ref MessagePackReader reader, int fields, int index) => + index < fields ? ReadString(ref reader) : ""; + } + + [MessagePackObject] public sealed class EveProviderFreshness { diff --git a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs index b198b2e..da482df 100644 --- a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs +++ b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs @@ -70,6 +70,44 @@ public sealed class EveSurfaceSerializationCompatibilityTests Assert.That(roundTrip.Commands[0].Command, Is.EqualTo("current.execute")); } + [Test] + public void LegacyExpandedProviderAdvertisementDeserializesAsCanonicalContract() + { + var document = MessagePackSerializer.Deserialize( + WriteLegacyProviderAdvertisement(), + Options); + + Assert.Multiple(() => + { + Assert.That(document.ProviderId, Is.EqualTo("aetheria")); + Assert.That(document.ServiceId, Is.EqualTo("aetheria-daemon")); + Assert.That(document.VerseId, Is.EqualTo("aetheria.local")); + Assert.That(document.Title, Is.EqualTo("Aetheria")); + Assert.That(document.CultMeshAddress, Is.EqualTo("cultmesh://aetheria.local/aetheria")); + Assert.That(document.Surfaces[0].SurfaceId, Is.EqualTo("aetheria.pilot")); + Assert.That(document.Surfaces[0].Schema, Is.EqualTo(EveSurfaceDocument.SchemaId)); + Assert.That(document.Commands[0].Command, Is.EqualTo("aetheria.pilot.move")); + Assert.That(document.Commands[0].SurfaceId, Is.Empty); + }); + } + + [Test] + public void CurrentProviderAdvertisementSerializationWritesCanonicalThirteenFields() + { + var document = new EveProviderAdvertisementDocument( + "provider", "service", "verse", "Title", "game.runtime", "cultmesh://verse/provider", + "2026-07-13T14:00:00Z", new EveProviderFreshness("fresh", "2026-07-13T14:00:00Z", 15000), + new[] { EveSurfaceDocument.SchemaId }, Array.Empty(), + Array.Empty(), Array.Empty()); + + var bytes = MessagePackSerializer.Serialize(document, Options); + var reader = new MessagePackReader(bytes); + + Assert.That(reader.ReadArrayHeader(), Is.EqualTo(13)); + Assert.That(MessagePackSerializer.Deserialize(bytes, Options).Title, + Is.EqualTo("Title")); + } + private static EveSurfaceDocument CreateCurrentDocument() => new( "current-provider", "current-kind", @@ -126,4 +164,43 @@ public sealed class EveSurfaceSerializationCompatibilityTests writer.Flush(); return buffer.WrittenSpan.ToArray(); } + + private static byte[] WriteLegacyProviderAdvertisement() + { + var buffer = new ArrayBufferWriter(); + var writer = new MessagePackWriter(buffer); + writer.WriteArrayHeader(16); + writer.Write(EveProviderAdvertisementDocument.SchemaId); + writer.Write("aetheria"); + writer.Write("aetheria-daemon"); + writer.Write("aetheria.local"); + writer.Write("asgard"); + writer.Write("aetheria"); + writer.Write("aetheria.local/aetheria"); + writer.Write("cultmesh://aetheria.local/aetheria"); + writer.Write("Aetheria"); + writer.Write("game.runtime"); + writer.Write("2026-07-13T14:00:00Z"); + writer.WriteArrayHeader(3); + writer.Write("fresh"); + writer.Write("2026-07-13T14:00:00Z"); + writer.Write(15000); + writer.WriteArrayHeader(1); + writer.Write(EveSurfaceDocument.SchemaId); + writer.WriteArrayHeader(0); + writer.WriteArrayHeader(1); + writer.WriteArrayHeader(5); + writer.Write(EveSurfaceDocument.SchemaId); + writer.Write("aetheria.pilot"); + writer.Write("eve:surface:aetheria.pilot"); + writer.Write("cultmesh"); + writer.Write("available"); + writer.WriteArrayHeader(1); + writer.WriteArrayHeader(3); + writer.Write("aetheria.pilot.move"); + writer.Write("cultmesh"); + writer.Write("Move the pilot ship"); + writer.Flush(); + return buffer.WrittenSpan.ToArray(); + } } From 03407c3165d4bbd6e36a2163175f00ee87a0e660 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 18:01:12 +0100 Subject: [PATCH 004/292] Add transport-neutral entity SoA view v2 --- .../GameCult.Eve.Surface.csproj | 5 +- .../Runtime/EveEntitySoaViewDocument.cs | 39 +++--- .../EveUnityCultMeshLiveProviderTransport.cs | 66 +++++++++- .../EveUnityCultMeshPlayableWorldProvider.cs | 35 ++++-- .../Runtime/EveUnityEntitySoaPresenter.cs | 13 +- .../Runtime/EveUnityEntitySoaView.cs | 118 ++++++++++-------- .../Runtime/EveUnityPlayableWorldRuntime.cs | 5 +- .../EveUnitySceneSurfaceLowererTests.cs | 118 +++++++++++++++--- ...eSurfaceSerializationCompatibilityTests.cs | 35 ++++++ 9 files changed, 327 insertions(+), 107 deletions(-) diff --git a/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj index 16839dd..f07f7e8 100644 --- a/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj +++ b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj @@ -5,10 +5,11 @@ latest enable Portable Eve surface and provider contracts. + $(MSBuildThisFileDirectory)..\..\..\CultLib - - + + diff --git a/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs b/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs index 222b4a1..961ea2f 100644 --- a/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs +++ b/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs @@ -10,31 +10,30 @@ namespace GameCult.Eve.Surface [MessagePackObject] public sealed class EveEntitySoaViewDocument { - public const string SchemaId = "gamecult.eve.entity_soa_view.v1"; + public const string SchemaId = "gamecult.eve.entity_soa_view.v2"; [Key(0)] public string Schema { get; set; } = SchemaId; [Key(1)] public string ProviderId { get; set; } = ""; [Key(2)] public string ViewId { get; set; } = ""; - [Key(3)] public long FrameId { get; set; } - [Key(4)] public long Generation { get; set; } - [Key(5)] public string PublishedAtUtc { get; set; } = ""; - [Key(6)] public string Backend { get; set; } = "cultcache"; - [Key(7)] public string SynchronizationMode { get; set; } = "immutable_frame"; - [Key(8)] public EveEntitySoaBuffer[] Buffers { get; set; } = Array.Empty(); - [Key(9)] public EveEntitySoaColumn[] Columns { get; set; } = Array.Empty(); - [Key(10)] public EveEntitySoaDirtyRange[] DirtyRanges { get; set; } = Array.Empty(); - [Key(11)] public EveEntityRenderGroup[] RenderGroups { get; set; } = Array.Empty(); + [Key(3)] public string PublishedAtUtc { get; set; } = ""; + [Key(4)] public string BodySchemaId { get; set; } = ""; + [Key(5)] public int LayoutVersion { get; set; } + [Key(6)] public long ProducerEpoch { get; set; } + [Key(7)] public long Sequence { get; set; } + [Key(8)] public int Capacity { get; set; } + [Key(9)] public EveEntitySoaBuffer[] Buffers { get; set; } = Array.Empty(); + [Key(10)] public EveEntitySoaColumn[] Columns { get; set; } = Array.Empty(); + [Key(11)] public EveEntitySoaDirtyRange[] DirtyRanges { get; set; } = Array.Empty(); + [Key(12)] public EveEntityRenderGroup[] RenderGroups { get; set; } = Array.Empty(); + [Key(13)] public EveEntityIdentity[] Identities { get; set; } = Array.Empty(); } [MessagePackObject] public sealed class EveEntitySoaBuffer { [Key(0)] public string BufferId { get; set; } = ""; - [Key(1)] public string Backend { get; set; } = "cultcache"; - [Key(2)] public string Location { get; set; } = ""; - [Key(3)] public long ByteOffset { get; set; } - [Key(4)] public long ByteLength { get; set; } - [Key(5)] public long Generation { get; set; } + [Key(1)] public long ByteOffset { get; set; } + [Key(2)] public long ByteLength { get; set; } } [MessagePackObject] @@ -57,7 +56,15 @@ namespace GameCult.Eve.Surface [Key(0)] public string ColumnId { get; set; } = ""; [Key(1)] public int StartIndex { get; set; } [Key(2)] public int Count { get; set; } - [Key(3)] public long Generation { get; set; } + [Key(3)] public long Sequence { get; set; } + } + + [MessagePackObject] + public sealed class EveEntityIdentity + { + [Key(0)] public int Index { get; set; } + [Key(1)] public string EntityId { get; set; } = ""; + [Key(2)] public string EntityKind { get; set; } = ""; } [MessagePackObject] diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 566b19e..7b0d8ed 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -30,6 +30,7 @@ namespace GameCult.Eve.UnityScene typeof(EveCommandReceiptDocument), typeof(EveAssetCatalogDocument), typeof(EveEntitySoaViewDocument), + typeof(CultMeshBodyPublicationDocument), typeof(CultMeshCdnArtifactManifest), typeof(CultMeshCdnArtifactChunk) }; @@ -53,13 +54,15 @@ namespace GameCult.Eve.UnityScene private CultNetDatabaseSubscriptionClient? _subscriptions; private EveProviderAdvertisementDocument? _advertisement; private EveAdvertisedSurface? _advertisedSurface; + private readonly CultMeshBodyPublicationResolver _bodyResolver; public EveUnityCultMeshLiveProviderTransport( string replicaPath, string endpoint, string providerId, string surfaceId, - string runtimeId = "eve-unity") + string runtimeId = "eve-unity", + CultMeshBodyPublicationResolver? bodyResolver = null) { _replicaPath = string.IsNullOrWhiteSpace(replicaPath) ? throw new ArgumentException("Replica path must be non-empty.", nameof(replicaPath)) @@ -74,6 +77,7 @@ namespace GameCult.Eve.UnityScene ? throw new ArgumentException("Surface id must be non-empty.", nameof(surfaceId)) : surfaceId.Trim(); _runtimeId = string.IsNullOrWhiteSpace(runtimeId) ? "eve-unity" : runtimeId.Trim(); + _bodyResolver = bodyResolver ?? CreateBodyResolver(); CurrentSurfaceDocument = EmptySurfaceDocument(); CurrentAssetManifestDocument = EmptyAssetManifest(); } @@ -94,7 +98,7 @@ namespace GameCult.Eve.UnityScene public event Action? CommandReceiptAvailable; - public event Action? EntityViewAvailable; + public event Action? EntityViewAvailable; public void Connect() { @@ -119,7 +123,7 @@ namespace GameCult.Eve.UnityScene while (_liveDocuments.TryDequeue(out var document)) { if (document is EveEntitySoaViewDocument entityView) - EntityViewAvailable?.Invoke(entityView); + PublishEntityView(entityView); else if (document is EveSurfaceDocument surface) PublishSurface(surface); else if (document is EveCommandReceiptDocument receipt) @@ -268,6 +272,62 @@ namespace GameCult.Eve.UnityScene throw new InvalidOperationException("The provider surface advertisement does not publish a record reference."); } + private CultMeshBodyPublicationResolver CreateBodyResolver() + { + var mappedRoot = Path.GetDirectoryName(_replicaPath) ?? "."; + return new CultMeshBodyPublicationResolver(new CultMeshBodyTransportService( + new ICultMeshBodyTransportAdapter[] + { + new CultMeshSharedMemoryBodyAdapter(), + new CultMeshMappedBodyAdapter(mappedRoot), + new CultMeshNetworkBodyAdapter(_ => throw new NotSupportedException( + "The connected Verse does not expose a CultMesh network body byte reader.")) + }, + (producerId, _) => string.Equals(producerId, _providerId, StringComparison.Ordinal))); + } + + private void PublishEntityView(EveEntitySoaViewDocument document) + { + ResolveAdvertisement(); + if (!string.Equals(document.ProviderId, _advertisement!.ProviderId, StringComparison.Ordinal)) + throw new UnauthorizedAccessException("Entity layout provider does not match the advertised Eve provider."); + if (document.Buffers == null || document.Buffers.Length == 0) + throw new InvalidOperationException("Entity layout does not name a primary logical buffer."); + var bodyId = document.Buffers[0].BufferId; + var publication = _snapshot! + .FetchDocumentsAsync( + recordKeys: new[] { CultMeshBodyPublicationDocument.CreateRecordKey(bodyId).Value }, + schemaIds: new[] { CultMeshBodyPublicationSchemaVersions.Publication }) + .GetAwaiter().GetResult().FirstOrDefault() + ?? throw new InvalidOperationException($"Provider did not publish CultMesh body '{bodyId}'."); + var lease = _bodyResolver.ResolveReadOnly(publication, new CultMeshBodyValidationRequest + { + BodyId = bodyId, + SchemaId = document.BodySchemaId, + LayoutVersion = document.LayoutVersion, + ProducerEpoch = document.ProducerEpoch, + Sequence = document.Sequence, + Capacity = document.Capacity, + AccessMode = CultMeshBodyAccessMode.ReadOnly, + NowUtc = DateTimeOffset.UtcNow + }); + var handler = EntityViewAvailable; + if (handler == null) + { + lease.Dispose(); + return; + } + try + { + handler(document, lease); + } + catch + { + lease.Dispose(); + throw; + } + } + private void RefreshSurface() { var surface = _snapshot! diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs index ac39266..ec1a239 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Collections.Concurrent; using GameCult.Eve.Surface; +using GameCult.Mesh; using UnityEngine; #nullable enable @@ -29,11 +30,9 @@ namespace GameCult.Eve.UnityScene private EveUnityCultMeshLiveProviderTransport? _transport; private EveUnitySceneLiveProviderBridge? _bridge; - private readonly ConcurrentQueue _pendingEntityViews = new ConcurrentQueue(); + private readonly ConcurrentQueue _pendingEntityViews = new ConcurrentQueue(); - public EveEntitySoaViewDocument? CurrentEntityView { get; private set; } - - public event Action? EntityViewAvailable; + public event Action? EntityViewAvailable; public EveUnityCultMeshProviderSelection? Selection { get; private set; } @@ -119,6 +118,7 @@ namespace GameCult.Eve.UnityScene { _bridge?.Dispose(); _transport?.Dispose(); + while (_pendingEntityViews.TryDequeue(out var pending)) pending.Lease.Dispose(); _bridge = null; _transport = null; Selection = null; @@ -127,11 +127,16 @@ namespace GameCult.Eve.UnityScene private void Update() { _transport?.PumpLiveEvents(); - EveEntitySoaViewDocument? latest = null; - while (_pendingEntityViews.TryDequeue(out var next)) latest = next; + EntityViewLease? latest = null; + while (_pendingEntityViews.TryDequeue(out var next)) + { + latest?.Lease.Dispose(); + latest = next; + } if (latest == null) return; - CurrentEntityView = latest; - EntityViewAvailable?.Invoke(latest); + var handler = EntityViewAvailable; + if (handler == null) latest.Lease.Dispose(); + else handler(latest.Document, latest.Lease); } private EveUnitySceneLiveProviderBridge Bridge @@ -165,8 +170,20 @@ namespace GameCult.Eve.UnityScene Selection.ProviderId, Selection.SurfaceId, runtimeId); - _transport.EntityViewAvailable += view => _pendingEntityViews.Enqueue(view); + _transport.EntityViewAvailable += (view, lease) => _pendingEntityViews.Enqueue(new EntityViewLease(view, lease)); _bridge = new EveUnitySceneLiveProviderBridge(_transport); } + + private sealed class EntityViewLease + { + public EntityViewLease(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease) + { + Document = document; + Lease = lease; + } + + public EveEntitySoaViewDocument Document { get; } + public ICultMeshBodyReadLease Lease { get; } + } } } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs index de83f46..17a89a0 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using GameCult.Eve.Surface; +using GameCult.Mesh; #nullable enable @@ -9,8 +10,7 @@ namespace GameCult.Eve.UnityScene { public interface IEveUnityEntitySoaViewDocumentSource { - EveEntitySoaViewDocument? CurrentEntityView { get; } - event Action EntityViewAvailable; + event Action EntityViewAvailable; } /// Projects portable entity SoA generations into the ordinary Unity scene sink. @@ -24,9 +24,9 @@ namespace GameCult.Eve.UnityScene _sink = sink ?? throw new ArgumentNullException(nameof(sink)); } - public void Apply(EveEntitySoaViewDocument document) + public void Apply(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease) { - using var view = EveUnityEntitySoaView.Open(document); + using var view = EveUnityEntitySoaView.Open(document, lease); var next = new HashSet(StringComparer.Ordinal); for (var row = 0; row < view.EntityCount; row++) { @@ -37,14 +37,15 @@ namespace GameCult.Eve.UnityScene view.TryReadFloat("render.scale", row, out var scale); view.TryReadUInt32("render.group.id", row, out var groupId); var renderGroup = view.RenderGroups.FirstOrDefault(group => group.GroupId == groupId); - var entityId = $"entity:{entityIndex}"; + var identity = document.Identities.FirstOrDefault(item => item.Index == entityIndex); + var entityId = identity?.EntityId ?? $"entity:{entityIndex}"; next.Add(entityId); var assetRef = renderGroup?.MeshAssetRef ?? ""; _sink.UpsertEntity( new EveUnityPlayableWorldEntity( entityId, entityId, - "entity", + identity?.EntityKind ?? "entity", entityId, "", assetRef, diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs index f1498ef..8aab4de 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using System.IO.MemoryMappedFiles; using System.Linq; using GameCult.Eve.Surface; +using GameCult.Mesh; using UnityEngine; #nullable enable @@ -11,104 +11,124 @@ namespace GameCult.Eve.UnityScene { public sealed class EveUnityEntitySoaView : IDisposable { - private readonly Dictionary _buffers = - new Dictionary(StringComparer.Ordinal); + private readonly ICultMeshBodyReadLease _lease; + private readonly Dictionary _buffers; private readonly Dictionary _columns; - private EveUnityEntitySoaView(EveEntitySoaViewDocument document) + private EveUnityEntitySoaView(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease) { Document = document; + _lease = lease; + _buffers = document.Buffers.ToDictionary(buffer => buffer.BufferId, StringComparer.Ordinal); _columns = document.Columns.ToDictionary(column => column.Semantic, StringComparer.Ordinal); - foreach (var buffer in document.Buffers) - { - if (!string.Equals(buffer.Backend, "memory_mapped_file", StringComparison.Ordinal)) - throw new NotSupportedException($"EveUnity does not support entity SoA backend '{buffer.Backend}'."); - var mapped = MemoryMappedFile.OpenExisting(buffer.Location, MemoryMappedFileRights.Read); - _buffers.Add(buffer.BufferId, mapped.CreateViewAccessor( - buffer.ByteOffset, - buffer.ByteLength, - MemoryMappedFileAccess.Read)); - mapped.Dispose(); - } } public EveEntitySoaViewDocument Document { get; } - public long Generation => Document.Generation; + public long Generation => Document.Sequence; public IReadOnlyList RenderGroups => Document.RenderGroups; + public int EntityCount => Document.Columns.Length == 0 ? 0 : Document.Columns.Min(column => column.ElementCount); - public static EveUnityEntitySoaView Open(EveEntitySoaViewDocument document) + public static EveUnityEntitySoaView Open(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease) { if (document == null) throw new ArgumentNullException(nameof(document)); - if (!string.Equals(document.Schema, EveEntitySoaViewDocument.SchemaId, StringComparison.Ordinal)) - throw new ArgumentException($"Expected {EveEntitySoaViewDocument.SchemaId}, got '{document.Schema}'.", nameof(document)); - return new EveUnityEntitySoaView(document); + if (lease == null) throw new ArgumentNullException(nameof(lease)); + Validate(document, lease); + return new EveUnityEntitySoaView(document, lease); } public bool TryReadVector3(string semantic, int index, out Vector3 value) { value = default; - if (!TryColumn(semantic, index, "float3", 12, out var column, out var accessor)) return false; - var offset = column.ByteOffset + (long)index * column.ElementStride; - value = new Vector3(accessor.ReadSingle(offset), accessor.ReadSingle(offset + 4), accessor.ReadSingle(offset + 8)); + if (!TryColumn(semantic, index, "float3", 12, out var offset)) return false; + value = new Vector3(_lease.ReadSingle(offset), _lease.ReadSingle(offset + 4), _lease.ReadSingle(offset + 8)); return true; } public bool TryReadFloat(string semantic, int index, out float value) { value = default; - if (!TryColumn(semantic, index, "float32", 4, out var column, out var accessor)) return false; - value = accessor.ReadSingle(column.ByteOffset + (long)index * column.ElementStride); + if (!TryColumn(semantic, index, "float32", 4, out var offset)) return false; + value = _lease.ReadSingle(offset); return true; } public bool TryReadInt32(string semantic, int index, out int value) { value = default; - if (!TryColumn(semantic, index, "int32", 4, out var column, out var accessor)) return false; - value = accessor.ReadInt32(column.ByteOffset + (long)index * column.ElementStride); + if (!TryColumn(semantic, index, "int32", 4, out var offset)) return false; + value = _lease.ReadInt32(offset); return true; } public bool TryReadUInt32(string semantic, int index, out uint value) { value = default; - if (!TryColumn(semantic, index, "uint32", 4, out var column, out var accessor)) return false; - value = accessor.ReadUInt32(column.ByteOffset + (long)index * column.ElementStride); + if (!TryColumn(semantic, index, "uint32", 4, out var offset)) return false; + value = unchecked((uint)_lease.ReadInt32(offset)); return true; } - public int EntityCount => Document.Columns.Length == 0 - ? 0 - : Document.Columns.Min(column => column.ElementCount); - public bool TryReadByte(string semantic, int index, out byte value) { value = default; - if (!TryColumn(semantic, index, "uint8", 1, out var column, out var accessor)) return false; - value = accessor.ReadByte(column.ByteOffset + (long)index * column.ElementStride); + if (!TryColumn(semantic, index, "uint8", 1, out var offset)) return false; + value = _lease.ReadByte(offset); return true; } - public void Dispose() + public void Dispose() => _lease.Dispose(); + + private static void Validate(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease) { - foreach (var accessor in _buffers.Values) accessor.Dispose(); - _buffers.Clear(); + if (!string.Equals(document.Schema, EveEntitySoaViewDocument.SchemaId, StringComparison.Ordinal)) + throw new ArgumentException($"Expected {EveEntitySoaViewDocument.SchemaId}, got '{document.Schema}'.", nameof(document)); + if (document.Buffers == null || document.Buffers.Length == 0) + throw new ArgumentException("Entity SoA view requires a primary logical buffer.", nameof(document)); + var descriptor = lease.Descriptor ?? throw new ArgumentException("Body lease has no descriptor.", nameof(lease)); + var primary = document.Buffers[0]; + if (!string.Equals(descriptor.BodyId, primary.BufferId, StringComparison.Ordinal) || + !string.Equals(descriptor.SchemaId, document.BodySchemaId, StringComparison.Ordinal) || + descriptor.LayoutVersion != document.LayoutVersion || + descriptor.ProducerEpoch != document.ProducerEpoch || + descriptor.Sequence != document.Sequence || + descriptor.Capacity != document.Capacity) + throw new InvalidOperationException("CultMesh body lease generation does not match the Eve entity layout."); + if (document.Capacity < 0 || document.Buffers.Any(buffer => + string.IsNullOrWhiteSpace(buffer.BufferId) || buffer.ByteOffset < 0 || buffer.ByteLength < 0 || + buffer.ByteOffset > descriptor.ByteSize - buffer.ByteLength)) + throw new InvalidOperationException("Eve entity layout contains an invalid logical buffer range."); + var buffers = document.Buffers.ToDictionary(buffer => buffer.BufferId, StringComparer.Ordinal); + foreach (var column in document.Columns ?? Array.Empty()) + { + if (!buffers.TryGetValue(column.BufferId, out var buffer) || column.ByteOffset < 0 || + column.ElementStride <= 0 || column.ElementCount < 0 || column.ElementCount > document.Capacity) + throw new InvalidOperationException($"Eve entity column '{column.ColumnId}' has invalid layout metadata."); + var width = ScalarWidth(column.ScalarType); + var occupied = column.ElementCount == 0 ? 0 : (long)(column.ElementCount - 1) * column.ElementStride + width; + if (width == 0 || column.ByteOffset > buffer.ByteLength - occupied) + throw new InvalidOperationException($"Eve entity column '{column.ColumnId}' exceeds its logical buffer."); + } } - private bool TryColumn( - string semantic, - int index, - string scalarType, - int minimumStride, - out EveEntitySoaColumn column, - out MemoryMappedViewAccessor accessor) + private bool TryColumn(string semantic, int index, string scalarType, int minimumStride, out long offset) { - column = null!; - accessor = null!; - if (index < 0 || !_columns.TryGetValue(semantic ?? "", out column)) return false; + offset = 0; + if (index < 0 || !_columns.TryGetValue(semantic ?? "", out var column)) return false; if (index >= column.ElementCount || column.ElementStride < minimumStride) return false; if (!string.Equals(column.ScalarType, scalarType, StringComparison.Ordinal)) return false; - return _buffers.TryGetValue(column.BufferId, out accessor); + if (!_buffers.TryGetValue(column.BufferId, out var buffer)) return false; + offset = buffer.ByteOffset + column.ByteOffset + (long)index * column.ElementStride; + return true; } + + private static int ScalarWidth(string scalarType) => scalarType switch + { + "float3" => 12, + "float32" => 4, + "int32" => 4, + "uint32" => 4, + "uint8" => 1, + _ => 0 + }; } } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs index 0fc0ad1..5871920 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs @@ -202,11 +202,10 @@ namespace GameCult.Eve.UnityScene if (_entityViewsConnected || _entityViews == null) return; _entityViews.EntityViewAvailable += OnEntityViewAvailable; _entityViewsConnected = true; - if (_entityViews.CurrentEntityView != null) - _entityPresenter.Apply(_entityViews.CurrentEntityView); } - private void OnEntityViewAvailable(EveEntitySoaViewDocument document) => _entityPresenter.Apply(document); + private void OnEntityViewAvailable(EveEntitySoaViewDocument document, GameCult.Mesh.ICultMeshBodyReadLease lease) => + _entityPresenter.Apply(document, lease); } public sealed class EveUnityLivePlayableWorldAssetProvider : IEveUnityNativeAssetProvider diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 10db12d..a153dee 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO.MemoryMappedFiles; using GameCult.Eve.Surface; using GameCult.Mesh; using NUnit.Framework; @@ -13,42 +12,58 @@ namespace GameCult.Eve.UnityScene.Tests public sealed class EveUnitySceneSurfaceLowererTests { [Test] - public void EntitySoaViewReadsGenericSemanticColumnsFromAdvertisedBackend() + public void EntitySoaViewReadsGenericSemanticColumnsFromInjectedLease() { - var location = $"eve-unity-soa-{Guid.NewGuid():N}"; - using var mapped = MemoryMappedFile.CreateNew(location, 32); - using (var writer = mapped.CreateViewAccessor()) - { - writer.Write(0, 4f); - writer.Write(4, 5f); - writer.Write(8, 6f); - writer.Write(12, 7f); - writer.Write(16, 8f); - writer.Write(20, 9f); - writer.Write(24, 41); - writer.Write(28, 42); - } + var bytes = new byte[32]; + Buffer.BlockCopy(new[] { 4f, 5f, 6f, 7f, 8f, 9f }, 0, bytes, 0, 24); + Buffer.BlockCopy(new[] { 41, 42 }, 0, bytes, 24, 8); var document = new EveEntitySoaViewDocument { ProviderId = "provider", ViewId = "pilot", - Generation = 7, - Backend = "memory_mapped_file", - Buffers = new[] { new EveEntitySoaBuffer { BufferId = "hot", Backend = "memory_mapped_file", Location = location, ByteLength = 32, Generation = 7 } }, + BodySchemaId = "test.entity.slab.v1", + LayoutVersion = 3, + ProducerEpoch = 5, + Sequence = 7, + Capacity = 2, + Buffers = new[] { new EveEntitySoaBuffer { BufferId = "hot", ByteLength = 32 } }, Columns = new[] { new EveEntitySoaColumn { ColumnId = "position", Semantic = "transform.position", BufferId = "hot", ScalarType = "float3", ElementStride = 12, ElementCount = 2 }, new EveEntitySoaColumn { ColumnId = "entity-index", Semantic = "entity.index", BufferId = "hot", ScalarType = "int32", ByteOffset = 24, ElementStride = 4, ElementCount = 2 } } }; + var lease = new FakeBodyReadLease(document, bytes); - using var view = EveUnityEntitySoaView.Open(document); + using var view = EveUnityEntitySoaView.Open(document, lease); Assert.That(view.TryReadVector3("transform.position", 1, out var position), Is.True); Assert.That(position, Is.EqualTo(new Vector3(7f, 8f, 9f))); Assert.That(view.TryReadInt32("entity.index", 0, out var entityIndex), Is.True); Assert.That(entityIndex, Is.EqualTo(41)); Assert.That(view.Generation, Is.EqualTo(7)); + Assert.That(lease.Disposed, Is.False); + } + + [TestCase("body")] + [TestCase("schema")] + [TestCase("layout")] + [TestCase("epoch")] + [TestCase("sequence")] + [TestCase("capacity")] + public void EntitySoaViewRejectsLeaseThatDisagreesWithLogicalLayout(string mismatch) + { + var document = EntityLeaseDocument(); + var descriptor = FakeBodyReadLease.DescriptorFor(document, 4); + if (mismatch == "body") descriptor.BodyId = "wrong"; + if (mismatch == "schema") descriptor.SchemaId = "wrong"; + if (mismatch == "layout") descriptor.LayoutVersion++; + if (mismatch == "epoch") descriptor.ProducerEpoch++; + if (mismatch == "sequence") descriptor.Sequence++; + if (mismatch == "capacity") descriptor.Capacity++; + + Assert.Throws(() => EveUnityEntitySoaView.Open( + document, new FakeBodyReadLease(descriptor, new byte[4]))); } [Test] @@ -1746,6 +1761,71 @@ namespace GameCult.Eve.UnityScene.Tests "", "", "", ""); } + private static EveEntitySoaViewDocument EntityLeaseDocument() => new EveEntitySoaViewDocument + { + ProviderId = "provider", + ViewId = "entities", + BodySchemaId = "test.entity.slab.v1", + LayoutVersion = 3, + ProducerEpoch = 5, + Sequence = 7, + Capacity = 1, + Buffers = new[] { new EveEntitySoaBuffer { BufferId = "hot", ByteLength = 4 } }, + Columns = new[] + { + new EveEntitySoaColumn + { + ColumnId = "entity-index", + Semantic = "entity.index", + BufferId = "hot", + ScalarType = "int32", + ElementStride = 4, + ElementCount = 1 + } + } + }; + + private sealed class FakeBodyReadLease : ICultMeshBodyReadLease + { + private readonly byte[] _bytes; + + public FakeBodyReadLease(EveEntitySoaViewDocument document, byte[] bytes) + : this(DescriptorFor(document, bytes.Length), bytes) { } + + public FakeBodyReadLease(CultMeshBodyDescriptor descriptor, byte[] bytes) + { + Descriptor = descriptor; + _bytes = bytes; + } + + public CultMeshBodyDescriptor Descriptor { get; } + public CultMeshBodyTransportKind TransportKind => CultMeshBodyTransportKind.Network; + public bool Disposed { get; private set; } + public byte ReadByte(long offset) => _bytes[checked((int)offset)]; + public int ReadInt32(long offset) => BitConverter.ToInt32(_bytes, checked((int)offset)); + public long ReadInt64(long offset) => BitConverter.ToInt64(_bytes, checked((int)offset)); + public float ReadSingle(long offset) => BitConverter.ToSingle(_bytes, checked((int)offset)); + public double ReadDouble(long offset) => BitConverter.ToDouble(_bytes, checked((int)offset)); + public int CopyTo(long offset, byte[] destination, int destinationOffset, int count) + { + Buffer.BlockCopy(_bytes, checked((int)offset), destination, destinationOffset, count); + return count; + } + public void Dispose() => Disposed = true; + + public static CultMeshBodyDescriptor DescriptorFor(EveEntitySoaViewDocument document, long byteSize) => + new CultMeshBodyDescriptor + { + BodyId = document.Buffers[0].BufferId, + SchemaId = document.BodySchemaId, + LayoutVersion = document.LayoutVersion, + ByteSize = byteSize, + Capacity = document.Capacity, + ProducerEpoch = document.ProducerEpoch, + Sequence = document.Sequence + }; + } + private sealed class FixedGameObjectAssetProvider : IEveUnityGameObjectAssetProvider { private readonly GameObject? _prefab; diff --git a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs index da482df..6b348ab 100644 --- a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs +++ b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs @@ -9,6 +9,41 @@ namespace GameCult.Eve.Surface.Tests; [TestFixture] public sealed class EveSurfaceSerializationCompatibilityTests { + [Test] + public void EntitySoaV2SerializesLogicalLayoutWithoutTransportFields() + { + var document = new EveEntitySoaViewDocument + { + ProviderId = "provider", + ViewId = "entities", + BodySchemaId = "entity.slab.v1", + LayoutVersion = 2, + ProducerEpoch = 3, + Sequence = 4, + Capacity = 8, + Buffers = new[] { new EveEntitySoaBuffer { BufferId = "entity-hot", ByteLength = 64 } } + }; + + var bytes = MessagePackSerializer.Serialize(document, Options); + var reader = new MessagePackReader(bytes); + var restored = MessagePackSerializer.Deserialize(bytes, Options); + + Assert.That(reader.ReadArrayHeader(), Is.EqualTo(14)); + Assert.That(restored.Schema, Is.EqualTo(EveEntitySoaViewDocument.SchemaId)); + Assert.That(restored.Buffers[0].BufferId, Is.EqualTo("entity-hot")); + } + + [Test] + public void EntitySoaContractsExposeNoTransportAuthorityFields() + { + var forbidden = new[] { "Backend", "Location", "CapabilityToken", "Synchronization", "SynchronizationMode", "Descriptor" }; + var exposed = new[] { typeof(EveEntitySoaViewDocument), typeof(EveEntitySoaBuffer), typeof(EveEntitySoaColumn) } + .SelectMany(type => type.GetProperties()) + .Select(property => property.Name); + + Assert.That(exposed, Has.None.Matches(name => forbidden.Contains(name, StringComparer.Ordinal))); + } + private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard; [Test] From a6f00c6ac0b4d47316b17957918bc88fe0a3c8bb Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 18:06:15 +0100 Subject: [PATCH 005/292] Restore entity identity semantics in SoA v2 --- .../Runtime/EveEntitySoaViewDocument.cs | 6 +++ .../Runtime/EveUnityEntitySoaPresenter.cs | 12 +++--- .../EveUnitySceneSurfaceLowererTests.cs | 42 +++++++++++++++++++ ...eSurfaceSerializationCompatibilityTests.cs | 25 ++++++++++- 4 files changed, 78 insertions(+), 7 deletions(-) diff --git a/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs b/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs index 961ea2f..d5148f1 100644 --- a/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs +++ b/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs @@ -26,6 +26,7 @@ namespace GameCult.Eve.Surface [Key(11)] public EveEntitySoaDirtyRange[] DirtyRanges { get; set; } = Array.Empty(); [Key(12)] public EveEntityRenderGroup[] RenderGroups { get; set; } = Array.Empty(); [Key(13)] public EveEntityIdentity[] Identities { get; set; } = Array.Empty(); + [Key(14)] public long FrameId { get; set; } } [MessagePackObject] @@ -65,6 +66,11 @@ namespace GameCult.Eve.Surface [Key(0)] public int Index { get; set; } [Key(1)] public string EntityId { get; set; } = ""; [Key(2)] public string EntityKind { get; set; } = ""; + [Key(3)] public string Label { get; set; } = ""; + [Key(4)] public string Faction { get; set; } = ""; + [Key(5)] public bool Selectable { get; set; } + [Key(6)] public bool Controllable { get; set; } + [Key(7)] public string AssetRef { get; set; } = ""; } [MessagePackObject] diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs index 17a89a0..eea519b 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs @@ -40,22 +40,24 @@ namespace GameCult.Eve.UnityScene var identity = document.Identities.FirstOrDefault(item => item.Index == entityIndex); var entityId = identity?.EntityId ?? $"entity:{entityIndex}"; next.Add(entityId); - var assetRef = renderGroup?.MeshAssetRef ?? ""; + var assetRef = string.IsNullOrWhiteSpace(identity?.AssetRef) + ? renderGroup?.MeshAssetRef ?? "" + : identity!.AssetRef; _sink.UpsertEntity( new EveUnityPlayableWorldEntity( entityId, entityId, identity?.EntityKind ?? "entity", - entityId, - "", + string.IsNullOrWhiteSpace(identity?.Label) ? entityId : identity!.Label, + identity?.Faction ?? "", assetRef, position.x, position.y, position.z, rotationRadians * 57.2957795f, scale > 0f ? scale : renderGroup?.DefaultScale ?? 1f, - selectable: true, - controllable: false, + selectable: identity?.Selectable ?? true, + controllable: identity?.Controllable ?? false, focusCommand: "", moveCommand: "", targetCommand: "", diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index a153dee..37203c4 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using GameCult.Eve.Surface; using GameCult.Mesh; using NUnit.Framework; @@ -66,6 +67,33 @@ namespace GameCult.Eve.UnityScene.Tests document, new FakeBodyReadLease(descriptor, new byte[4]))); } + [Test] + public void EntitySoaPresenterPreservesProviderAuthoredIdentityFacts() + { + var bytes = new byte[16]; + Buffer.BlockCopy(new[] { 12 }, 0, bytes, 0, 4); + Buffer.BlockCopy(new[] { 1f, 2f, 3f }, 0, bytes, 4, 12); + var document = EntityLeaseDocument(); + document.Buffers[0].ByteLength = bytes.Length; + document.Columns = new[] + { + new EveEntitySoaColumn { ColumnId = "entity-index", Semantic = "entity.index", BufferId = "hot", ScalarType = "int32", ElementStride = 4, ElementCount = 1 }, + new EveEntitySoaColumn { ColumnId = "position", Semantic = "transform.position", BufferId = "hot", ScalarType = "float3", ByteOffset = 4, ElementStride = 12, ElementCount = 1 } + }; + var sink = new FakePlayableWorldSceneSink(); + + new EveUnityEntitySoaPresenter(sink).Apply(document, new FakeBodyReadLease(document, bytes)); + + var entity = sink.Upserts.Single().entity; + Assert.That(entity.EntityId, Is.EqualTo("entity:pilot")); + Assert.That(entity.Label, Is.EqualTo("Pilot")); + Assert.That(entity.EntityKind, Is.EqualTo("player")); + Assert.That(entity.Faction, Is.EqualTo("alliance")); + Assert.That(entity.Selectable, Is.True); + Assert.That(entity.Controllable, Is.True); + Assert.That(entity.AssetRef, Is.EqualTo("cultmesh://assets/pilot")); + } + [Test] public void LowerCarriesProviderAdvertisedWorldBoundary() { @@ -1771,6 +1799,20 @@ namespace GameCult.Eve.UnityScene.Tests Sequence = 7, Capacity = 1, Buffers = new[] { new EveEntitySoaBuffer { BufferId = "hot", ByteLength = 4 } }, + Identities = new[] + { + new EveEntityIdentity + { + Index = 12, + EntityId = "entity:pilot", + EntityKind = "player", + Label = "Pilot", + Faction = "alliance", + Selectable = true, + Controllable = true, + AssetRef = "cultmesh://assets/pilot" + } + }, Columns = new[] { new EveEntitySoaColumn diff --git a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs index 6b348ab..a756e74 100644 --- a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs +++ b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs @@ -21,16 +21,37 @@ public sealed class EveSurfaceSerializationCompatibilityTests ProducerEpoch = 3, Sequence = 4, Capacity = 8, - Buffers = new[] { new EveEntitySoaBuffer { BufferId = "entity-hot", ByteLength = 64 } } + FrameId = 91, + Buffers = new[] { new EveEntitySoaBuffer { BufferId = "entity-hot", ByteLength = 64 } }, + Identities = new[] + { + new EveEntityIdentity + { + Index = 0, + EntityId = "player", + EntityKind = "ship", + Label = "Vanguard", + Faction = "alliance", + Selectable = true, + Controllable = true, + AssetRef = "cultmesh://assets/player" + } + } }; var bytes = MessagePackSerializer.Serialize(document, Options); var reader = new MessagePackReader(bytes); var restored = MessagePackSerializer.Deserialize(bytes, Options); - Assert.That(reader.ReadArrayHeader(), Is.EqualTo(14)); + Assert.That(reader.ReadArrayHeader(), Is.EqualTo(15)); Assert.That(restored.Schema, Is.EqualTo(EveEntitySoaViewDocument.SchemaId)); Assert.That(restored.Buffers[0].BufferId, Is.EqualTo("entity-hot")); + Assert.That(restored.FrameId, Is.EqualTo(91)); + Assert.That(restored.Identities[0].Label, Is.EqualTo("Vanguard")); + Assert.That(restored.Identities[0].Faction, Is.EqualTo("alliance")); + Assert.That(restored.Identities[0].Selectable, Is.True); + Assert.That(restored.Identities[0].Controllable, Is.True); + Assert.That(restored.Identities[0].AssetRef, Is.EqualTo("cultmesh://assets/player")); } [Test] From 010ddeff90b70ce5278da373ce2dd0bb2c9eb9ac Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 18:52:53 +0100 Subject: [PATCH 006/292] Release entity SoA v2 Unity packages --- ReleaseConsumerProject/Packages/manifest.json | 6 +++--- packages/org.gamecult.eve.surface/package.json | 4 ++-- packages/org.gamecult.eve.unity-scene/package.json | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 246c0a8..8bea128 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -5,10 +5,10 @@ "com.unity.modules.imageconversion": "1.0.0", "com.unity.modules.physics": "1.0.0", "com.unity.modules.uielements": "1.0.0", - "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.7", + "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.8", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", - "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#b6f001b300a88aff0bc13372b5c1dbc2002954f8", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#b6f001b300a88aff0bc13372b5c1dbc2002954f8", + "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.0", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.2.0", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0" }, "testables": [ diff --git a/packages/org.gamecult.eve.surface/package.json b/packages/org.gamecult.eve.surface/package.json index 7c99f0e..a529d21 100644 --- a/packages/org.gamecult.eve.surface/package.json +++ b/packages/org.gamecult.eve.surface/package.json @@ -1,11 +1,11 @@ { "name": "org.gamecult.eve.surface", "displayName": "Eve Surface Contract", - "version": "0.1.0", + "version": "0.2.0", "unity": "2021.3", "description": "Unity-compatible DTOs for gamecult.eve.surface.v1 documents.", "dependencies": { - "org.gamecult.cultlib": "1.0.6" + "org.gamecult.cultlib": "1.0.8" }, "keywords": [ "Eve", diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index a241183..f4b02d9 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,13 +1,13 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.1.4", + "version": "0.2.0", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { - "org.gamecult.cultlib": "1.0.6", + "org.gamecult.cultlib": "1.0.8", "org.gamecult.eve.plugin-fields": "0.1.1", - "org.gamecult.eve.surface": "0.1.0", + "org.gamecult.eve.surface": "0.2.0", "com.unity.mathematics": "1.3.2" }, "keywords": [ From d9c1de9130419103d7421d95fb2c6789a0921f46 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 19:47:19 +0100 Subject: [PATCH 007/292] Resolve CultMesh bodies by view generation --- .../EveUnityCultMeshLiveProviderTransport.cs | 21 +++++-- .../EveUnitySceneSurfaceLowererTests.cs | 56 +++++++++++++++++++ .../GameCult.Eve.UnityScene.Tests.asmdef | 1 + .../org.gamecult.eve.unity-scene/package.json | 4 +- 4 files changed, 76 insertions(+), 6 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 7b0d8ed..171ea4f 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -100,6 +100,17 @@ namespace GameCult.Eve.UnityScene public event Action? EntityViewAvailable; + private static CultMeshBodyPublicationHandle BodyPublicationHandle(EveEntitySoaViewDocument document) + { + if (document == null) throw new ArgumentNullException(nameof(document)); + if (document.Buffers == null || document.Buffers.Length == 0) + throw new InvalidOperationException("Entity layout does not name a primary logical buffer."); + return new CultMeshBodyPublicationHandle( + document.Buffers[0].BufferId, + document.ProducerEpoch, + document.Sequence); + } + public void Connect() { EnsureOpen(); @@ -293,16 +304,18 @@ namespace GameCult.Eve.UnityScene throw new UnauthorizedAccessException("Entity layout provider does not match the advertised Eve provider."); if (document.Buffers == null || document.Buffers.Length == 0) throw new InvalidOperationException("Entity layout does not name a primary logical buffer."); - var bodyId = document.Buffers[0].BufferId; + var handle = BodyPublicationHandle(document); var publication = _snapshot! .FetchDocumentsAsync( - recordKeys: new[] { CultMeshBodyPublicationDocument.CreateRecordKey(bodyId).Value }, + recordKeys: new[] { handle.RecordKey.Value }, schemaIds: new[] { CultMeshBodyPublicationSchemaVersions.Publication }) .GetAwaiter().GetResult().FirstOrDefault() - ?? throw new InvalidOperationException($"Provider did not publish CultMesh body '{bodyId}'."); + ?? throw new InvalidOperationException( + $"Provider did not publish CultMesh body generation '{handle.RecordKey.Value}'."); + handle.Validate(publication); var lease = _bodyResolver.ResolveReadOnly(publication, new CultMeshBodyValidationRequest { - BodyId = bodyId, + BodyId = handle.BodyId, SchemaId = document.BodySchemaId, LayoutVersion = document.LayoutVersion, ProducerEpoch = document.ProducerEpoch, diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 37203c4..3df7dfd 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; +using GameCult.Caching; using GameCult.Eve.Surface; using GameCult.Mesh; using NUnit.Framework; @@ -12,6 +14,29 @@ namespace GameCult.Eve.UnityScene.Tests { public sealed class EveUnitySceneSurfaceLowererTests { + [Test] + public async System.Threading.Tasks.Task LiveTransportResolvesViewGenerationWhenNextPublicationAlreadyExists() + { + var view = EntityLeaseDocument(); + var generationN = BodyPublication(view, sequence: view.Sequence); + var generationNPlusOne = BodyPublication(view, sequence: view.Sequence + 1); + var cache = new CultCache(); + await cache.UpsertAsync(generationN, + new CultRecordHandle(generationN.RecordKey)); + await cache.UpsertAsync(generationNPlusOne, + new CultRecordHandle(generationNPlusOne.RecordKey)); + + var handle = (CultMeshBodyPublicationHandle)typeof(EveUnityCultMeshLiveProviderTransport) + .GetMethod("BodyPublicationHandle", BindingFlags.NonPublic | BindingFlags.Static)! + .Invoke(null, new object[] { view })!; + var resolved = cache.Get(handle.RecordKey); + + Assert.That(resolved, Is.Not.Null); + handle.Validate(resolved!); + Assert.That(resolved!.Sequence, Is.EqualTo(view.Sequence)); + Assert.That(handle.RecordKey, Is.Not.EqualTo(generationNPlusOne.RecordKey)); + } + [Test] public void EntitySoaViewReadsGenericSemanticColumnsFromInjectedLease() { @@ -1827,6 +1852,37 @@ namespace GameCult.Eve.UnityScene.Tests } }; + private static CultMeshBodyPublicationDocument BodyPublication( + EveEntitySoaViewDocument view, + long sequence) + { + var descriptor = FakeBodyReadLease.DescriptorFor(view, view.Buffers[0].ByteLength); + descriptor.Sequence = sequence; + descriptor.Synchronization = CultMeshBodySynchronization.ImmutableSequence; + descriptor.LeaseExpiresAtUnixMs = DateTimeOffset.UtcNow.AddMinutes(1).ToUnixTimeMilliseconds(); + descriptor.TransportKind = CultMeshBodyTransportKind.SharedFileMapping; + var network = FakeBodyReadLease.DescriptorFor(view, view.Buffers[0].ByteLength); + network.Sequence = sequence; + network.Synchronization = descriptor.Synchronization; + network.LeaseExpiresAtUnixMs = descriptor.LeaseExpiresAtUnixMs; + network.TransportKind = CultMeshBodyTransportKind.Network; + return new CultMeshBodyPublicationDocument + { + BodyId = descriptor.BodyId, + ProducerId = view.ProviderId, + SchemaId = descriptor.SchemaId, + LayoutVersion = descriptor.LayoutVersion, + ByteSize = descriptor.ByteSize, + Capacity = descriptor.Capacity, + ProducerEpoch = descriptor.ProducerEpoch, + Sequence = sequence, + Synchronization = descriptor.Synchronization, + LivenessExpiresAtUnixMs = descriptor.LeaseExpiresAtUnixMs, + PreferredLocal = descriptor, + NetworkFallback = network + }; + } + private sealed class FakeBodyReadLease : ICultMeshBodyReadLease { private readonly byte[] _bytes; diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef b/packages/org.gamecult.eve.unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef index 2f8a14e..4428a96 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef @@ -4,6 +4,7 @@ "references": [ "GameCult.Eve.Surface", "GameCult.Eve.UnityScene", + "GameCult.Eve.UnityScene.CultMesh", "GameCult.Eve.PluginFields" ], "includePlatforms": [ diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index f4b02d9..ee51a03 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,11 +1,11 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.2.0", + "version": "0.2.1", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { - "org.gamecult.cultlib": "1.0.8", + "org.gamecult.cultlib": "1.0.9", "org.gamecult.eve.plugin-fields": "0.1.1", "org.gamecult.eve.surface": "0.2.0", "com.unity.mathematics": "1.3.2" From eae2e896c7396798b29e2dd3f819d63f22137a91 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 20:14:53 +0100 Subject: [PATCH 008/292] Skip expired CultMesh entity generations --- .../EveUnityCultMeshLiveProviderTransport.cs | 10 +++++++++- .../EveUnitySceneSurfaceLowererTests.cs | 20 +++++++++++++++++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 171ea4f..45a14d7 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -313,6 +313,9 @@ namespace GameCult.Eve.UnityScene ?? throw new InvalidOperationException( $"Provider did not publish CultMesh body generation '{handle.RecordKey.Value}'."); handle.Validate(publication); + var now = DateTimeOffset.UtcNow; + if (!IsPublicationLive(publication, now)) + return; var lease = _bodyResolver.ResolveReadOnly(publication, new CultMeshBodyValidationRequest { BodyId = handle.BodyId, @@ -322,7 +325,7 @@ namespace GameCult.Eve.UnityScene Sequence = document.Sequence, Capacity = document.Capacity, AccessMode = CultMeshBodyAccessMode.ReadOnly, - NowUtc = DateTimeOffset.UtcNow + NowUtc = now }); var handler = EntityViewAvailable; if (handler == null) @@ -341,6 +344,11 @@ namespace GameCult.Eve.UnityScene } } + private static bool IsPublicationLive( + CultMeshBodyPublicationDocument publication, + DateTimeOffset now) => + publication.LivenessExpiresAtUnixMs > now.ToUnixTimeMilliseconds(); + private void RefreshSurface() { var surface = _snapshot! diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 3df7dfd..1ad7b11 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -37,6 +37,26 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(handle.RecordKey, Is.Not.EqualTo(generationNPlusOne.RecordKey)); } + [Test] + public void LiveTransportDropsExpiredHistoricalGenerationWithoutReinterpretingLatest() + { + var view = EntityLeaseDocument(); + var publication = BodyPublication(view, view.Sequence); + publication.LivenessExpiresAtUnixMs = 999; + var method = typeof(EveUnityCultMeshLiveProviderTransport) + .GetMethod("IsPublicationLive", BindingFlags.NonPublic | BindingFlags.Static)!; + + var live = (bool)method.Invoke(null, new object[] + { + publication, + DateTimeOffset.FromUnixTimeMilliseconds(1000) + })!; + + Assert.That(live, Is.False); + Assert.That(publication.RecordKey, Is.EqualTo( + new CultMeshBodyPublicationHandle(view.Buffers[0].BufferId, view.ProducerEpoch, view.Sequence).RecordKey)); + } + [Test] public void EntitySoaViewReadsGenericSemanticColumnsFromInjectedLease() { diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index ee51a03..f02b183 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.2.1", + "version": "0.2.2", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 61a255a4d5f965c39cce01554a4ec4bce627d24e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 20:27:00 +0100 Subject: [PATCH 009/292] Resolve CultMesh network body fallback --- .../EveUnityCultMeshLiveProviderTransport.cs | 49 ++++++++++++++++++- .../EveUnitySceneSurfaceLowererTests.cs | 32 ++++++++++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 45a14d7..de75b0c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -31,6 +31,7 @@ namespace GameCult.Eve.UnityScene typeof(EveAssetCatalogDocument), typeof(EveEntitySoaViewDocument), typeof(CultMeshBodyPublicationDocument), + typeof(CultMeshNetworkBodyDocument), typeof(CultMeshCdnArtifactManifest), typeof(CultMeshCdnArtifactChunk) }; @@ -291,12 +292,56 @@ namespace GameCult.Eve.UnityScene { new CultMeshSharedMemoryBodyAdapter(), new CultMeshMappedBodyAdapter(mappedRoot), - new CultMeshNetworkBodyAdapter(_ => throw new NotSupportedException( - "The connected Verse does not expose a CultMesh network body byte reader.")) + new CultMeshNetworkBodyAdapter(ReadNetworkBody) }, (producerId, _) => string.Equals(producerId, _providerId, StringComparison.Ordinal))); } + private byte[] ReadNetworkBody(CultMeshBodyDescriptor descriptor) + { + var binding = _snapshot! + .FetchDocumentsAsync( + recordKeys: new[] { CultMeshNetworkBodyDocument.CreateRecordKey(descriptor.CapabilityToken).Value }, + schemaIds: new[] { CultMeshBodyPublicationSchemaVersions.NetworkBody }) + .GetAwaiter().GetResult().SingleOrDefault() + ?? throw new FileNotFoundException("Provider network body capability is missing.", descriptor.CapabilityToken); + var manifest = _snapshot + .FetchDocumentsAsync( + recordKeys: new[] { binding.ManifestRecordKey }, + schemaIds: new[] { CultMeshCdnSchemaVersions.ArtifactManifest }) + .GetAwaiter().GetResult().SingleOrDefault() + ?? throw new FileNotFoundException("Provider network body manifest is missing.", binding.ManifestRecordKey); + var chunks = _snapshot + .FetchDocumentsAsync( + recordKeys: manifest.Chunks.Select(chunk => chunk.RecordKey).ToArray(), + schemaIds: new[] { CultMeshCdnSchemaVersions.ArtifactChunk }) + .GetAwaiter().GetResult(); + return ResolveNetworkBody(descriptor, binding, manifest, chunks); + } + + private static byte[] ResolveNetworkBody( + CultMeshBodyDescriptor descriptor, + CultMeshNetworkBodyDocument binding, + CultMeshCdnArtifactManifest manifest, + IReadOnlyList chunks) + { + var registry = CultMesh.CreateCultCacheDocumentRegistry( + typeof(CultMeshNetworkBodyDocument), + typeof(CultMeshCdnArtifactManifest), + typeof(CultMeshCdnArtifactChunk)); + var cache = new CultCache(registry); + cache.UpsertAsync(binding, new CultRecordHandle(binding.RecordKey)) + .GetAwaiter().GetResult(); + cache.UpsertAsync(manifest, new CultRecordHandle( + CultMeshCdnArtifactManifest.CreateRecordKey(manifest))) + .GetAwaiter().GetResult(); + foreach (var chunk in chunks) + cache.UpsertAsync(chunk, new CultRecordHandle( + CultMeshCdnArtifactChunk.CreateRecordKey(chunk))) + .GetAwaiter().GetResult(); + return new CultMeshNetworkBodyResolver(cache).Fetch(descriptor); + } + private void PublishEntityView(EveEntitySoaViewDocument document) { ResolveAdvertisement(); diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 1ad7b11..185bfa9 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -57,6 +57,38 @@ namespace GameCult.Eve.UnityScene.Tests new CultMeshBodyPublicationHandle(view.Buffers[0].BufferId, view.ProducerEpoch, view.Sequence).RecordKey)); } + [Test] + public async System.Threading.Tasks.Task LiveTransportResolvesExactNetworkBodyThroughVerseDocuments() + { + var view = EntityLeaseDocument(); + var bytes = Enumerable.Range(0, 32).Select(value => (byte)value).ToArray(); + var source = new CultCache(); + var generation = new CultMeshBodyGeneration + { + BodyId = view.Buffers[0].BufferId, + ProducerId = view.ProviderId, + SchemaId = view.BodySchemaId, + LayoutVersion = view.LayoutVersion, + Capacity = view.Capacity, + ProducerEpoch = view.ProducerEpoch, + Sequence = view.Sequence, + Synchronization = CultMeshBodySynchronization.TripleBuffer, + LeaseExpiresAtUnixMs = DateTimeOffset.UtcNow.AddMinutes(1).ToUnixTimeMilliseconds() + }; + var descriptor = await new CultMeshNetworkBodyPublisher(source, _ => true).PublishAsync(generation, bytes); + var binding = source.Get( + CultMeshNetworkBodyDocument.CreateRecordKey(descriptor.CapabilityToken))!; + var manifest = source.Get(new CultRecordKey(binding.ManifestRecordKey))!; + var chunks = manifest.Chunks.Select(reference => + source.Get(new CultRecordKey(reference.RecordKey))!).ToArray(); + var method = typeof(EveUnityCultMeshLiveProviderTransport) + .GetMethod("ResolveNetworkBody", BindingFlags.NonPublic | BindingFlags.Static)!; + + var resolved = (byte[])method.Invoke(null, new object[] { descriptor, binding, manifest, chunks })!; + + Assert.That(resolved, Is.EqualTo(bytes)); + } + [Test] public void EntitySoaViewReadsGenericSemanticColumnsFromInjectedLease() { diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index f02b183..47e736b 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.2.2", + "version": "0.2.3", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From ce6a95c4f60f83657f1a966199dc8993bfb3af53 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 20:33:17 +0100 Subject: [PATCH 010/292] Stage selected CultLib package for Unity tests --- TestProject/Packages/manifest.json | 2 +- TestProject/Packages/packages-lock.json | 16 ++++++++++++---- scripts/run-package-tests.ps1 | 13 +++++++++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/TestProject/Packages/manifest.json b/TestProject/Packages/manifest.json index b08495a..1f31a7a 100644 --- a/TestProject/Packages/manifest.json +++ b/TestProject/Packages/manifest.json @@ -5,7 +5,7 @@ "com.unity.modules.imageconversion": "1.0.0", "com.unity.modules.physics": "1.0.0", "com.unity.modules.uielements": "1.0.0", - "org.gamecult.cultlib": "file:../../../CultLib/artifacts/unity/org.gamecult.cultlib", + "org.gamecult.cultlib": "file:../../artifacts/test-dependencies/org.gamecult.cultlib", "org.gamecult.eve.plugin-fields": "file:../../../EvePlugins/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields", "org.gamecult.eve.surface": "file:../../packages/org.gamecult.eve.surface", "org.gamecult.eve.unity-scene": "file:../../packages/org.gamecult.eve.unity-scene", diff --git a/TestProject/Packages/packages-lock.json b/TestProject/Packages/packages-lock.json index 6b032e8..c270403 100644 --- a/TestProject/Packages/packages-lock.json +++ b/TestProject/Packages/packages-lock.json @@ -6,6 +6,13 @@ "source": "builtin", "dependencies": {} }, + "com.unity.mathematics": { + "version": "1.3.3", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, "com.unity.test-framework": { "version": "1.6.0", "depth": 0, @@ -17,7 +24,7 @@ } }, "org.gamecult.cultlib": { - "version": "file:../../../CultLib/artifacts/unity/org.gamecult.cultlib", + "version": "file:../../artifacts/test-dependencies/org.gamecult.cultlib", "depth": 0, "source": "local", "dependencies": {} @@ -33,7 +40,7 @@ "depth": 0, "source": "local", "dependencies": { - "org.gamecult.cultlib": "1.0.3" + "org.gamecult.cultlib": "1.0.8" } }, "org.gamecult.eve.unity-scene": { @@ -41,9 +48,10 @@ "depth": 0, "source": "local", "dependencies": { - "org.gamecult.cultlib": "1.0.3", + "org.gamecult.cultlib": "1.0.9", "org.gamecult.eve.plugin-fields": "0.1.1", - "org.gamecult.eve.surface": "0.1.0" + "org.gamecult.eve.surface": "0.2.0", + "com.unity.mathematics": "1.3.2" } }, "org.gamecult.eve.unity-uitoolkit": { diff --git a/scripts/run-package-tests.ps1 b/scripts/run-package-tests.ps1 index 1b2ff11..dbf3962 100644 --- a/scripts/run-package-tests.ps1 +++ b/scripts/run-package-tests.ps1 @@ -17,6 +17,18 @@ foreach ($required in @($UnityExe, $projectPath, $cultLibBuilder)) { powershell -ExecutionPolicy Bypass -File $cultLibBuilder if ($LASTEXITCODE -ne 0) { throw "CultLib Unity package build failed with exit code $LASTEXITCODE" } +$builtCultLibPackage = Join-Path $CultLibRoot "artifacts\unity\org.gamecult.cultlib" +$stagedDependenciesRoot = Join-Path $repoRoot "artifacts\test-dependencies" +$stagedCultLibPackage = Join-Path $stagedDependenciesRoot "org.gamecult.cultlib" +if (-not (Test-Path -LiteralPath $builtCultLibPackage)) { + throw "CultLib Unity package build did not produce: $builtCultLibPackage" +} +New-Item -ItemType Directory -Force -Path $stagedDependenciesRoot | Out-Null +if (Test-Path -LiteralPath $stagedCultLibPackage) { + [IO.Directory]::Delete($stagedCultLibPackage, $true) +} +Copy-Item -LiteralPath $builtCultLibPackage -Destination $stagedCultLibPackage -Recurse + $runRoot = Join-Path $output (Get-Date -Format "yyyyMMddTHHmmss") New-Item -ItemType Directory -Force -Path $runRoot | Out-Null $resultsPath = Join-Path $runRoot "unity-editmode-results.xml" @@ -40,3 +52,4 @@ if ($null -eq $run -or [int]$run.total -eq 0 -or [int]$run.failed -gt 0) { } Write-Host "EveUnity generic package tests: $($run.passed) passed, $($run.total) total" Write-Host "Consumer: $projectPath" +Write-Host "CultLib package: $stagedCultLibPackage" From 890692e30f8327806b0fc93d3b7d33be2e7a10ca Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 21:18:10 +0100 Subject: [PATCH 011/292] Expose immutable presented entity generations --- .../org.gamecult.eve.unity-scene/README.md | 6 + .../Runtime/EveUnityEntitySoaPresenter.cs | 55 +++++--- ...veUnityGameObjectPlayableWorldSceneSink.cs | 113 ++++++++++++++- .../Runtime/EveUnityPlayableWorldCameraRig.cs | 15 +- .../EveUnityPlayableWorldClientHost.cs | 1 + .../Runtime/EveUnityPlayableWorldRuntime.cs | 3 + .../Runtime/EveUnityPresentedEntities.cs | 122 ++++++++++++++++ .../Runtime/EveUnityPresentedEntities.cs.meta | 2 + .../EveUnitySceneSurfaceLowererTests.cs | 132 ++++++++++++++++++ .../org.gamecult.eve.unity-scene/package.json | 4 +- 10 files changed, 420 insertions(+), 33 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs.meta diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index d93183b..12ca9f7 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -15,6 +15,12 @@ selection filters; they are not product knowledge required by the client. The package never imports provider product assemblies. +`IEveUnityPresentedEntityRegistry` exposes each committed SoA generation as +immutable entity facts plus presentation transforms, addressable by Eve entity +id or source index. Camera, selection, and provider UI adapters consume that +registry instead of reconstructing provider gameplay objects. It owns +presentation lookup only; simulation state and commands remain provider-owned. + Retained provider feedback is lowered without becoming Unity authority. `EveUnityFeedbackPresenter` emits each new `feedback.event` identity once, and `EveUnityShotReceiptPresenter` does the same for `shot.receipt` trajectories. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs index eea519b..c6ad595 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs @@ -27,6 +27,7 @@ namespace GameCult.Eve.UnityScene public void Apply(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease) { using var view = EveUnityEntitySoaView.Open(document, lease); + var presented = new List(); var next = new HashSet(StringComparer.Ordinal); for (var row = 0; row < view.EntityCount; row++) { @@ -34,7 +35,10 @@ namespace GameCult.Eve.UnityScene if (view.TryReadByte("render.visibility", row, out var visible) && visible == 0) continue; if (!view.TryReadVector3("transform.position", row, out var position)) continue; view.TryReadFloat("transform.rotation.radians", row, out var rotationRadians); + view.TryReadVector3("transform.velocity", row, out var velocity); + view.TryReadFloat("physics.body.radius", row, out var radius); view.TryReadFloat("render.scale", row, out var scale); + view.TryReadInt32("render.lod", row, out var lod); view.TryReadUInt32("render.group.id", row, out var groupId); var renderGroup = view.RenderGroups.FirstOrDefault(group => group.GroupId == groupId); var identity = document.Identities.FirstOrDefault(item => item.Index == entityIndex); @@ -43,34 +47,43 @@ namespace GameCult.Eve.UnityScene var assetRef = string.IsNullOrWhiteSpace(identity?.AssetRef) ? renderGroup?.MeshAssetRef ?? "" : identity!.AssetRef; + var fact = new EveUnityPresentedEntity( + entityIndex, + entityId, + identity?.EntityKind ?? "entity", + string.IsNullOrWhiteSpace(identity?.Label) ? entityId : identity!.Label, + identity?.Faction ?? "", + position, + rotationRadians * 57.2957795f, + velocity, + radius, + scale > 0f ? scale : renderGroup?.DefaultScale ?? 1f, + groupId, + lod, + identity?.Selectable ?? true, + identity?.Controllable ?? false, + assetRef); + presented.Add(fact); + if (_sink is IEveUnityEntityGenerationSink) + continue; _sink.UpsertEntity( - new EveUnityPlayableWorldEntity( - entityId, - entityId, - identity?.EntityKind ?? "entity", - string.IsNullOrWhiteSpace(identity?.Label) ? entityId : identity!.Label, - identity?.Faction ?? "", - assetRef, - position.x, - position.y, - position.z, - rotationRadians * 57.2957795f, - scale > 0f ? scale : renderGroup?.DefaultScale ?? 1f, - selectable: identity?.Selectable ?? true, - controllable: identity?.Controllable ?? false, - focusCommand: "", - moveCommand: "", - targetCommand: "", - actionCommand: "", - props: new Dictionary()), + fact.ToPlayableWorldEntity(), new EveUnityPlayableWorldAssetBinding( assetRef, "entity", string.IsNullOrWhiteSpace(assetRef) ? "unity-generated-placeholder" : "provider-asset-ref")); } - foreach (var entityId in _known.Where(entityId => !next.Contains(entityId)).ToArray()) - _sink.RemoveEntity(entityId); + if (_sink is IEveUnityEntityGenerationSink generationSink) + { + generationSink.ApplyGeneration(new EveUnityPresentedEntityGeneration( + document.ViewId, document.ProducerEpoch, document.Sequence, presented)); + } + else + { + foreach (var entityId in _known.Where(entityId => !next.Contains(entityId)).ToArray()) + _sink.RemoveEntity(entityId); + } _known.Clear(); foreach (var entityId in next) _known.Add(entityId); } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs index 75a9696..0035a83 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using UnityEngine; #nullable enable @@ -16,11 +17,18 @@ namespace GameCult.Eve.UnityScene UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType); } - public sealed class EveUnityGameObjectPlayableWorldSceneSink : IEveUnityPlayableWorldSceneSink + public sealed class EveUnityGameObjectPlayableWorldSceneSink : + IEveUnityPlayableWorldSceneSink, + IEveUnityEntityGenerationSink, + IEveUnityPresentedEntityRegistry { private readonly Transform _root; private readonly IEveUnityGameObjectAssetProvider _assetProvider; private readonly Dictionary _instances = new Dictionary(StringComparer.Ordinal); + private IReadOnlyDictionary _presentedById = + new Dictionary(StringComparer.Ordinal); + private IReadOnlyDictionary _presentedByIndex = + new Dictionary(); public EveUnityGameObjectPlayableWorldSceneSink( Transform root, @@ -31,6 +39,13 @@ namespace GameCult.Eve.UnityScene } public int ActiveEntityCount => _instances.Count; + public EveUnityPresentedEntityGeneration? CurrentGeneration { get; private set; } + + public bool TryGetByEntityId(string entityId, out EveUnityPresentedEntityHandle handle) => + _presentedById.TryGetValue(entityId ?? "", out handle!); + + public bool TryGetBySourceIndex(int sourceIndex, out EveUnityPresentedEntityHandle handle) => + _presentedByIndex.TryGetValue(sourceIndex, out handle!); public void ConfigureWorld(EveUnityPlayableWorldProjection world) { @@ -66,6 +81,90 @@ namespace GameCult.Eve.UnityScene ApplyEntity(instance, entity, asset); } + public void ApplyGeneration(EveUnityPresentedEntityGeneration generation) + { + if (generation == null) throw new ArgumentNullException(nameof(generation)); + if (generation.Entities.Any(entity => string.IsNullOrWhiteSpace(entity.EntityId))) + throw new InvalidOperationException("Presented entity generations require stable entity ids."); + var factsById = generation.Entities.ToDictionary(entity => entity.EntityId, StringComparer.Ordinal); + var factsByIndex = generation.Entities.ToDictionary(entity => entity.SourceIndex); + GameObject? staging = null; + var created = new Dictionary(StringComparer.Ordinal); + var nextInstances = new Dictionary(StringComparer.Ordinal); + try + { + foreach (var fact in generation.Entities) + { + if (_instances.TryGetValue(fact.EntityId, out var existing) && existing != null) + { + nextInstances.Add(fact.EntityId, existing); + continue; + } + + staging ??= new GameObject($"{_root.name}.generation.{generation.ProducerEpoch}.{generation.Sequence}"); + if (staging.transform.parent == null) + { + staging.SetActive(false); + staging.transform.SetParent(_root, false); + } + var entity = fact.ToPlayableWorldEntity(); + var asset = new EveUnityPlayableWorldAssetBinding( + fact.AssetRef, + fact.EntityKind, + string.IsNullOrWhiteSpace(fact.AssetRef) ? "unity-generated-placeholder" : "provider-asset-ref"); + var instance = InstantiateEntity(entity, asset, staging.transform); + created.Add(fact.EntityId, instance); + nextInstances.Add(fact.EntityId, instance); + } + + foreach (var fact in generation.Entities) + { + var entity = fact.ToPlayableWorldEntity(); + var asset = new EveUnityPlayableWorldAssetBinding( + fact.AssetRef, + fact.EntityKind, + string.IsNullOrWhiteSpace(fact.AssetRef) ? "unity-generated-placeholder" : "provider-asset-ref"); + ApplyEntity(nextInstances[fact.EntityId], entity, asset); + } + + var nextById = factsById.ToDictionary( + pair => pair.Key, + pair => new EveUnityPresentedEntityHandle(pair.Value, nextInstances[pair.Key].transform), + StringComparer.Ordinal); + var nextByIndex = factsByIndex.ToDictionary( + pair => pair.Key, + pair => nextById[pair.Value.EntityId]); + + foreach (var pair in _instances) + if (!nextInstances.ContainsKey(pair.Key) && pair.Value != null) + DestroyInstance(pair.Value); + _instances.Clear(); + foreach (var pair in nextInstances) + { + if (created.ContainsKey(pair.Key)) + { + pair.Value.transform.SetParent(_root, true); + pair.Value.SetActive(true); + } + _instances.Add(pair.Key, pair.Value); + } + _presentedById = nextById; + _presentedByIndex = nextByIndex; + CurrentGeneration = generation; + } + catch + { + foreach (var instance in created.Values) + if (instance != null) DestroyInstance(instance); + throw; + } + finally + { + if (staging != null) + DestroyInstance(staging); + } + } + public void RemoveEntity(string entityId) { if (string.IsNullOrWhiteSpace(entityId)) @@ -80,14 +179,18 @@ namespace GameCult.Eve.UnityScene DestroyInstance(instance); } - private GameObject InstantiateEntity(EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset) + private GameObject InstantiateEntity( + EveUnityPlayableWorldEntity entity, + EveUnityPlayableWorldAssetBinding asset, + Transform? parent = null) { + parent ??= _root; var prefab = _assetProvider.ResolvePrefab(asset); GameObject instance; if (prefab != null) { instance = new GameObject(entity.EntityId); - instance.transform.SetParent(_root, false); + instance.transform.SetParent(parent, false); var visual = UnityEngine.Object.Instantiate(prefab, instance.transform); visual.transform.localPosition = Vector3.zero; visual.transform.localRotation = Quaternion.identity; @@ -97,8 +200,8 @@ namespace GameCult.Eve.UnityScene instance = GameObject.CreatePrimitive(PrimitiveType.Capsule); } - if (instance.transform.parent != _root) - instance.transform.SetParent(_root, false); + if (instance.transform.parent != parent) + instance.transform.SetParent(parent, false); return instance; } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index 996c5bb..ade131c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -53,7 +53,7 @@ namespace GameCult.Eve.UnityScene } var camera = cameraTransform != null ? cameraTransform : transform; - var player = FindPlayerMarker(resolvedHost, activeWorld.PlayerEntityId); + var player = FindPlayer(resolvedHost, activeWorld.PlayerEntityId); if (player == null) return false; @@ -65,7 +65,7 @@ namespace GameCult.Eve.UnityScene cameraComponent.cullingMask = cullingMask; } var bounds = CalculateVisualBounds(player); - var target = bounds?.center ?? player.transform.position; + var target = bounds?.center ?? player.position; var radius = bounds?.extents.magnitude ?? 0f; var verticalFov = cameraComponent != null ? cameraComponent.fieldOfView : 60f; var framingDistance = radius > 0f @@ -84,7 +84,7 @@ namespace GameCult.Eve.UnityScene return true; } - private static Bounds? CalculateVisualBounds(EveUnityPlayableWorldEntityMarker player) + private static Bounds? CalculateVisualBounds(Transform player) { var renderers = player.GetComponentsInChildren(includeInactive: false); Bounds? bounds = null; @@ -121,18 +121,23 @@ namespace GameCult.Eve.UnityScene return host; } - private static EveUnityPlayableWorldEntityMarker? FindPlayerMarker( + private static Transform? FindPlayer( EveUnityPlayableWorldClientHost host, string playerEntityId) { if (string.IsNullOrWhiteSpace(playerEntityId)) return null; + if (host.PresentedEntities?.CurrentGeneration != null) + return host.PresentedEntities.TryGetByEntityId(playerEntityId, out var presented) + ? presented.Transform + : null; + var markers = host.SceneRoot.GetComponentsInChildren(); foreach (var marker in markers) { if (marker != null && marker.EntityId == playerEntityId) - return marker; + return marker.transform; } return null; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs index 2dfe724..60fefac 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs @@ -31,6 +31,7 @@ namespace GameCult.Eve.UnityScene public EveUnityPlayableWorldPresentation? LastPresentation => Runtime?.LastPresentation; public EveUnitySceneCommandReceipt? LastReceipt => Runtime?.LastReceipt; + public IEveUnityPresentedEntityRegistry? PresentedEntities => Runtime?.PresentedEntities; public long ActiveVersion => Runtime?.ActiveVersion ?? 0; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs index 5871920..a5d7e5c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs @@ -14,6 +14,7 @@ namespace GameCult.Eve.UnityScene private readonly EveUnityPlayableWorldAssetManifestDocumentSource? _assetManifestSource; private readonly IEveUnityEntitySoaViewDocumentSource? _entityViews; private readonly EveUnityEntitySoaPresenter _entityPresenter; + private readonly IEveUnityPresentedEntityRegistry? _presentedEntities; private bool _entityViewsConnected; private bool _assetManifestConnected; @@ -33,6 +34,7 @@ namespace GameCult.Eve.UnityScene AssetManifests = assetManifests ?? new EveUnityPlayableWorldAssetManifestCache(); _surfaceSource = new EveUnitySceneProviderSurfaceDocumentSource(surfaceDocuments); _entityViews = surfaceDocuments as IEveUnityEntitySoaViewDocumentSource; + _presentedEntities = sceneSink as IEveUnityPresentedEntityRegistry; _entityPresenter = new EveUnityEntitySoaPresenter(sceneSink); _connection = new EveUnitySceneProviderConnection(_surfaceSource, commandSink); _client = new EveUnityPlayableWorldLiveClient( @@ -47,6 +49,7 @@ namespace GameCult.Eve.UnityScene public EveUnityPlayableWorldAssetManifestCache AssetManifests { get; } public IEveUnityGameObjectAssetProvider? GameObjectAssetProvider { get; private set; } + public IEveUnityPresentedEntityRegistry? PresentedEntities => _presentedEntities; public EveUnityPlayableWorldProjection? ActiveWorld => _client.ActiveWorld; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs new file mode 100644 index 0000000..e34fff4 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +#nullable enable + +namespace GameCult.Eve.UnityScene +{ + public sealed class EveUnityPresentedEntity + { + public EveUnityPresentedEntity( + int sourceIndex, + string entityId, + string entityKind, + string label, + string faction, + Vector3 position, + float rotationYDegrees, + Vector3 velocity, + float radius, + float scale, + uint renderGroupId, + int lod, + bool selectable, + bool controllable, + string assetRef) + { + SourceIndex = sourceIndex; + EntityId = entityId ?? ""; + EntityKind = entityKind ?? ""; + Label = label ?? ""; + Faction = faction ?? ""; + Position = position; + RotationYDegrees = rotationYDegrees; + Velocity = velocity; + Radius = radius; + Scale = scale; + RenderGroupId = renderGroupId; + Lod = lod; + Selectable = selectable; + Controllable = controllable; + AssetRef = assetRef ?? ""; + } + + public int SourceIndex { get; } + public string EntityId { get; } + public string EntityKind { get; } + public string Label { get; } + public string Faction { get; } + public Vector3 Position { get; } + public float RotationYDegrees { get; } + public Vector3 Velocity { get; } + public float Radius { get; } + public float Scale { get; } + public uint RenderGroupId { get; } + public int Lod { get; } + public bool Selectable { get; } + public bool Controllable { get; } + public string AssetRef { get; } + + public EveUnityPlayableWorldEntity ToPlayableWorldEntity() => new EveUnityPlayableWorldEntity( + EntityId, + EntityId, + EntityKind, + Label, + Faction, + AssetRef, + Position.x, + Position.y, + Position.z, + RotationYDegrees, + Scale > 0f ? Scale : Radius, + Selectable, + Controllable, + "", "", "", ""); + } + + public sealed class EveUnityPresentedEntityGeneration + { + public EveUnityPresentedEntityGeneration( + string viewId, + long producerEpoch, + long sequence, + IEnumerable entities) + { + ViewId = viewId ?? ""; + ProducerEpoch = producerEpoch; + Sequence = sequence; + Entities = (entities ?? throw new ArgumentNullException(nameof(entities))).ToArray(); + } + + public string ViewId { get; } + public long ProducerEpoch { get; } + public long Sequence { get; } + public IReadOnlyList Entities { get; } + } + + public sealed class EveUnityPresentedEntityHandle + { + public EveUnityPresentedEntityHandle(EveUnityPresentedEntity entity, Transform transform) + { + Entity = entity ?? throw new ArgumentNullException(nameof(entity)); + Transform = transform != null ? transform : throw new ArgumentNullException(nameof(transform)); + } + + public EveUnityPresentedEntity Entity { get; } + public Transform Transform { get; } + } + + public interface IEveUnityPresentedEntityRegistry + { + EveUnityPresentedEntityGeneration? CurrentGeneration { get; } + bool TryGetByEntityId(string entityId, out EveUnityPresentedEntityHandle handle); + bool TryGetBySourceIndex(int sourceIndex, out EveUnityPresentedEntityHandle handle); + } + + public interface IEveUnityEntityGenerationSink + { + void ApplyGeneration(EveUnityPresentedEntityGeneration generation); + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs.meta new file mode 100644 index 0000000..c16abad --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3f7e67e0c24449bab80f2dd15cf2e118 diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 185bfa9..443fa6d 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -171,6 +171,102 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(entity.AssetRef, Is.EqualTo("cultmesh://assets/pilot")); } + [Test] + public void EntitySoaPresenterPublishesImmutableGenerationFactsAndRegistryLookups() + { + var root = new GameObject("presented-entities"); + try + { + var bytes = new byte[48]; + Buffer.BlockCopy(new[] { 12 }, 0, bytes, 0, 4); + Buffer.BlockCopy(new[] { 1f, 2f, 3f }, 0, bytes, 4, 12); + Buffer.BlockCopy(new[] { 0.5f }, 0, bytes, 16, 4); + Buffer.BlockCopy(new[] { 4f, 5f, 6f }, 0, bytes, 20, 12); + Buffer.BlockCopy(new[] { 7f }, 0, bytes, 32, 4); + Buffer.BlockCopy(new[] { 1.5f }, 0, bytes, 36, 4); + Buffer.BlockCopy(new[] { 9 }, 0, bytes, 40, 4); + Buffer.BlockCopy(new[] { 2 }, 0, bytes, 44, 4); + var document = EntityLeaseDocument(); + document.Buffers[0].ByteLength = bytes.Length; + document.Columns = new[] + { + SoaColumn("entity.index", "int32", 0, 4), + SoaColumn("transform.position", "float3", 4, 12), + SoaColumn("transform.rotation.radians", "float32", 16, 4), + SoaColumn("transform.velocity", "float3", 20, 12), + SoaColumn("physics.body.radius", "float32", 32, 4), + SoaColumn("render.scale", "float32", 36, 4), + SoaColumn("render.group.id", "uint32", 40, 4), + SoaColumn("render.lod", "int32", 44, 4) + }; + var sink = new EveUnityGameObjectPlayableWorldSceneSink( + root.transform, new FixedGameObjectAssetProvider(null)); + + new EveUnityEntitySoaPresenter(sink).Apply(document, new FakeBodyReadLease(document, bytes)); + + Assert.That(sink.CurrentGeneration, Is.Not.Null); + Assert.That(sink.CurrentGeneration!.ProducerEpoch, Is.EqualTo(document.ProducerEpoch)); + Assert.That(sink.CurrentGeneration.Sequence, Is.EqualTo(document.Sequence)); + Assert.That(sink.TryGetByEntityId("entity:pilot", out var byId), Is.True); + Assert.That(sink.TryGetBySourceIndex(12, out var byIndex), Is.True); + Assert.That(byIndex, Is.SameAs(byId)); + Assert.That(byId.Entity.Position, Is.EqualTo(new Vector3(1f, 2f, 3f))); + Assert.That(byId.Entity.Velocity, Is.EqualTo(new Vector3(4f, 5f, 6f))); + Assert.That(byId.Entity.Radius, Is.EqualTo(7f)); + Assert.That(byId.Entity.Scale, Is.EqualTo(1.5f)); + Assert.That(byId.Entity.RenderGroupId, Is.EqualTo(9)); + Assert.That(byId.Entity.Lod, Is.EqualTo(2)); + Assert.That(byId.Entity.Selectable, Is.True); + Assert.That(byId.Entity.Controllable, Is.True); + Assert.That(byId.Entity.AssetRef, Is.EqualTo("cultmesh://assets/pilot")); + } + finally + { + UnityEngine.Object.DestroyImmediate(root); + } + } + + [Test] + public void GameObjectGenerationRegistrySwapsOnlyAfterCompleteApplication() + { + var root = new GameObject("atomic-generation"); + EveUnityGameObjectPlayableWorldSceneSink? sink = null; + long observedSequenceDuringApply = -1; + try + { + var provider = new ObservingGameObjectAssetProvider(() => + observedSequenceDuringApply = sink?.CurrentGeneration?.Sequence ?? -1); + sink = new EveUnityGameObjectPlayableWorldSceneSink(root.transform, provider); + sink.ApplyGeneration(new EveUnityPresentedEntityGeneration( + "entities", 3, 10, + new[] { PresentedEntity(1, "first", "asset:first") })); + Assert.That(sink.TryGetByEntityId("first", out var firstGeneration), Is.True); + var firstTransform = firstGeneration.Transform; + + observedSequenceDuringApply = -1; + sink.ApplyGeneration(new EveUnityPresentedEntityGeneration( + "entities", 3, 11, + new[] + { + PresentedEntity(1, "first", "asset:first"), + PresentedEntity(2, "second", "asset:second") + })); + + Assert.That(observedSequenceDuringApply, Is.EqualTo(10)); + Assert.That(sink.CurrentGeneration!.Sequence, Is.EqualTo(11)); + Assert.That(sink.TryGetByEntityId("first", out var retained), Is.True); + Assert.That(retained.Transform, Is.SameAs(firstTransform)); + Assert.That(sink.TryGetByEntityId("second", out var second), Is.True); + Assert.That(sink.TryGetBySourceIndex(2, out var byIndex), Is.True); + Assert.That(byIndex, Is.SameAs(second)); + Assert.That(sink.ActiveEntityCount, Is.EqualTo(2)); + } + finally + { + UnityEngine.Object.DestroyImmediate(root); + } + } + [Test] public void LowerCarriesProviderAdvertisedWorldBoundary() { @@ -1935,6 +2031,26 @@ namespace GameCult.Eve.UnityScene.Tests }; } + private static EveEntitySoaColumn SoaColumn( + string semantic, + string scalarType, + long byteOffset, + int stride) => new EveEntitySoaColumn + { + ColumnId = semantic, + Semantic = semantic, + BufferId = "hot", + ScalarType = scalarType, + ByteOffset = byteOffset, + ElementStride = stride, + ElementCount = 1 + }; + + private static EveUnityPresentedEntity PresentedEntity(int index, string entityId, string assetRef) => + new EveUnityPresentedEntity( + index, entityId, "entity", entityId, "", Vector3.zero, 0f, Vector3.zero, + 1f, 1f, 0, 0, true, false, assetRef); + private sealed class FakeBodyReadLease : ICultMeshBodyReadLease { private readonly byte[] _bytes; @@ -1988,6 +2104,22 @@ namespace GameCult.Eve.UnityScene.Tests public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) => _prefab; } + private sealed class ObservingGameObjectAssetProvider : IEveUnityGameObjectAssetProvider + { + private readonly Action _onResolve; + + public ObservingGameObjectAssetProvider(Action onResolve) + { + _onResolve = onResolve; + } + + public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) + { + _onResolve(); + return null; + } + } + private sealed class FakePlayableWorldProviderComponent : MonoBehaviour, IEveUnitySceneProviderSurfaceDocumentSource, diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 47e736b..334ca56 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,11 +1,11 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.2.3", + "version": "0.2.4", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { - "org.gamecult.cultlib": "1.0.9", + "org.gamecult.cultlib": "1.0.10", "org.gamecult.eve.plugin-fields": "0.1.1", "org.gamecult.eve.surface": "0.2.0", "com.unity.mathematics": "1.3.2" From e45171952030dbb7ea3997f7abdc0af96b483b26 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 21:35:11 +0100 Subject: [PATCH 012/292] Lower semantic render channels in Unity --- docs/authority-map.md | 10 ++++--- .../org.gamecult.eve.unity-scene/README.md | 8 ++++++ .../EveUnityCultMeshLiveProviderTransport.cs | 26 ++++++++----------- .../EveUnityCultMeshPlayableWorldProvider.cs | 4 +-- .../Runtime/EveUnityPlayableWorldCameraRig.cs | 13 +++++++--- .../Runtime/EveUnitySceneSurfaceLowerer.cs | 20 ++++++++++++-- .../EveUnitySceneSurfaceLowererTests.cs | 23 ++++++++++++++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 8 files changed, 78 insertions(+), 28 deletions(-) diff --git a/docs/authority-map.md b/docs/authority-map.md index 15c6148..635cd36 100644 --- a/docs/authority-map.md +++ b/docs/authority-map.md @@ -18,6 +18,7 @@ drivers, asset-provider hooks, package release, runtime tests, and captures. - `gamecult.eve.surface.v1` - provider asset-manifest documents - provider command receipts +- semantic render-channel inclusion and exclusion policy - Unity input and local presentation assets ## Outputs @@ -29,15 +30,16 @@ drivers, asset-provider hooks, package release, runtime tests, and captures. ## Derived State -`ActiveWorld`, scene objects, transforms, markers, camera pose, asset caches, -presentation counters, and receipt display state are projections. They are not -world truth. +`ActiveWorld`, scene objects, transforms, markers, camera pose, native +render-channel layer mappings, asset caches, presentation counters, and receipt +display state are projections. They are not world truth. ## Forbidden Writers Unity transforms, input drivers, camera rigs, scene sinks, asset caches, and pending receipts may not mutate or simulate provider world state. Plugin -projection shells may not implement Sai, Norn, or TeX semantics. +projection shells may not implement Sai, Norn, or TeX semantics. Portable +surfaces may not carry Unity layer numbers or camera culling masks. ## Shared Paths diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index 12ca9f7..e94e088 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -21,6 +21,14 @@ id or source index. Camera, selection, and provider UI adapters consume that registry instead of reconstructing provider gameplay objects. It owns presentation lookup only; simulation state and commands remain provider-owned. +World surfaces may exclude semantic render channels through the +`excludedRenderChannels` world-root property. Runtime asset variants map those +channel names to Unity layers with `renderChannel..unityLayer` +metadata. Surfaces never publish Unity layer numbers or camera masks: providers +own which semantic content belongs in a view, while EveUnity owns the native +layer mapping and subtracts only those layers from the camera's configured +culling mask. + Retained provider feedback is lowered without becoming Unity authority. `EveUnityFeedbackPresenter` emits each new `feedback.event` identity once, and `EveUnityShotReceiptPresenter` does the same for `shot.receipt` trajectories. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index de75b0c..8f51c57 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -46,7 +46,7 @@ namespace GameCult.Eve.UnityScene private readonly Dictionary _prefabs = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _nativeAssets = new Dictionary(StringComparer.Ordinal); private readonly List _assetBundles = new List(); - private readonly Dictionary _cameraCullingMasks = new Dictionary(StringComparer.Ordinal); + private readonly Dictionary _renderChannelLayers = new Dictionary(StringComparer.Ordinal); private readonly ConcurrentQueue _liveDocuments = new ConcurrentQueue(); private CultMeshNode? _node; private CultMeshSnapshotSession? _snapshot; @@ -210,9 +210,9 @@ namespace GameCult.Eve.UnityScene ? value : null; } - public bool TryGetCameraCullingMask(string viewId, out int cullingMask) + public bool TryGetRenderChannelLayer(string channel, out int layer) { - return _cameraCullingMasks.TryGetValue(viewId ?? "", out cullingMask); + return _renderChannelLayers.TryGetValue(channel ?? "", out layer); } private void EnsureOpen() @@ -497,7 +497,7 @@ namespace GameCult.Eve.UnityScene _assetBundles.Clear(); _prefabs.Clear(); _nativeAssets.Clear(); - _cameraCullingMasks.Clear(); + _renderChannelLayers.Clear(); var selected = catalog.Assets .Select(asset => new { @@ -547,24 +547,20 @@ namespace GameCult.Eve.UnityScene private void ReadCameraPolicies(IEnumerable variants) { - _cameraCullingMasks.Clear(); + _renderChannelLayers.Clear(); foreach (var variant in variants) { foreach (var pair in variant.Metadata) { - const string prefix = "view."; - const string suffix = ".excludeUnityLayers"; + const string prefix = "renderChannel."; + const string suffix = ".unityLayer"; if (!pair.Key.StartsWith(prefix, StringComparison.Ordinal) || !pair.Key.EndsWith(suffix, StringComparison.Ordinal)) continue; - var viewId = pair.Key.Substring(prefix.Length, pair.Key.Length - prefix.Length - suffix.Length); - var mask = -1; - foreach (var token in (pair.Value ?? "").Split(',')) - { - if (int.TryParse(token.Trim(), out var layer) && layer >= 0 && layer < 32) - mask &= ~(1 << layer); - } - _cameraCullingMasks[viewId] = mask; + var channel = pair.Key.Substring(prefix.Length, pair.Key.Length - prefix.Length - suffix.Length); + if (!string.IsNullOrWhiteSpace(channel) && + int.TryParse(pair.Value, out var layer) && layer >= 0 && layer < 32) + _renderChannelLayers[channel] = layer; } } } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs index ec1a239..a91c832 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs @@ -108,10 +108,10 @@ namespace GameCult.Eve.UnityScene return _transport!.ResolveAsset(asset, assetType); } - public bool TryGetCameraCullingMask(string viewId, out int cullingMask) + public bool TryGetRenderChannelLayer(string channel, out int layer) { EnsureTransport(); - return _transport!.TryGetCameraCullingMask(viewId, out cullingMask); + return _transport!.TryGetRenderChannelLayer(channel, out layer); } private void OnDestroy() diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index ade131c..b4de75c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -6,7 +6,7 @@ namespace GameCult.Eve.UnityScene { public interface IEveUnityCameraRenderPolicySource { - bool TryGetCameraCullingMask(string viewId, out int cullingMask); + bool TryGetRenderChannelLayer(string channel, out int layer); } public sealed class EveUnityPlayableWorldCameraRig : MonoBehaviour @@ -58,10 +58,15 @@ namespace GameCult.Eve.UnityScene return false; var cameraComponent = camera.GetComponent(); - if (cameraComponent != null && - _renderPolicySource != null && - _renderPolicySource.TryGetCameraCullingMask(activeWorld.ViewId, out var cullingMask)) + if (cameraComponent != null && _renderPolicySource != null) { + var cullingMask = cameraComponent.cullingMask; + foreach (var channel in activeWorld.ExcludedRenderChannels) + { + if (_renderPolicySource.TryGetRenderChannelLayer(channel, out var layer) && + layer >= 0 && layer < 32) + cullingMask &= ~(1 << layer); + } cameraComponent.cullingMask = cullingMask; } var bounds = CalculateVisualBounds(player); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index ae13226..c6d5c88 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using GameCult.Eve.Surface; using GameCult.Mesh; @@ -124,7 +125,8 @@ namespace GameCult.Eve.UnityScene worldRoot.GetProp("entityViewPointerId"), worldRoot.GetProp("entityViewSchema"), worldRoot.GetProp("zoneRenderPointerId"), - worldRoot.GetProp("zoneRenderSchema")); + worldRoot.GetProp("zoneRenderSchema"), + ParseStringList(worldRoot.GetProp("excludedRenderChannels"))); } private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component) @@ -198,6 +200,16 @@ namespace GameCult.Eve.UnityScene string.Equals(value, "1", StringComparison.Ordinal); } + private static IReadOnlyList ParseStringList(string value) + { + return (value ?? "") + .Split(',') + .Select(part => part.Trim()) + .Where(part => !string.IsNullOrWhiteSpace(part)) + .Distinct(StringComparer.Ordinal) + .ToArray(); + } + private static EveUnityScenePluginProjection? BuildPluginProjection(EveSurfaceComponent component) { if (SaiVisualNovelAdapter.CanProject(component)) @@ -324,7 +336,8 @@ namespace GameCult.Eve.UnityScene string entityViewPointerId = "", string entityViewSchema = "", string zoneRenderPointerId = "", - string zoneRenderSchema = "") + string zoneRenderSchema = "", + IReadOnlyList? excludedRenderChannels = null) { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -342,6 +355,7 @@ namespace GameCult.Eve.UnityScene EntityViewSchema = entityViewSchema ?? ""; ZoneRenderPointerId = zoneRenderPointerId ?? ""; ZoneRenderSchema = zoneRenderSchema ?? ""; + ExcludedRenderChannels = excludedRenderChannels ?? Array.Empty(); } public string WorldRootId { get; } @@ -374,6 +388,8 @@ namespace GameCult.Eve.UnityScene public string ActionCommand { get; } + public IReadOnlyList ExcludedRenderChannels { get; } + public int EntityCount => Entities.Count; public IReadOnlyList Entities { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 443fa6d..9d47936 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -332,6 +332,7 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(projection.PlayableWorld.AssetManifest, Is.EqualTo("cultmesh://aetheria/assets/manifest")); Assert.That(projection.PlayableWorld.InputProfile, Is.EqualTo("arpg-third-person")); Assert.That(projection.PlayableWorld.CameraRig, Is.EqualTo("third-person-orbit")); + Assert.That(projection.PlayableWorld.ExcludedRenderChannels, Is.EqualTo(new[] { "map" })); Assert.That(projection.PlayableWorld.PlayerEntityId, Is.EqualTo("player-vanguard")); Assert.That(projection.PlayableWorld.MovementCommand, Is.EqualTo("aetheria.daemon.move_intent")); Assert.That(projection.PlayableWorld.FocusCommand, Is.EqualTo("aetheria.daemon.focus")); @@ -939,6 +940,8 @@ namespace GameCult.Eve.UnityScene.Tests var rig = hostObject.AddComponent(); rig.Host = host; rig.CameraTransform = cameraObject.transform; + rig.RenderPolicySource = new FixedRenderChannelPolicy("map", 14); + var camera = cameraObject.AddComponent(); var player = rootObject.GetComponentInChildren(); Assert.That(player, Is.Not.Null); @@ -947,6 +950,7 @@ namespace GameCult.Eve.UnityScene.Tests largeVisual.transform.localScale = Vector3.one * 100f; Assert.That(rig.ApplyRig(0f), Is.True); + Assert.That(camera.cullingMask & (1 << 14), Is.Zero); Assert.That(cameraObject.transform.position.y, Is.GreaterThan(0f)); Assert.That(Vector3.Distance(cameraObject.transform.position, largeVisual.transform.position), Is.GreaterThan(100f)); } @@ -1562,6 +1566,7 @@ namespace GameCult.Eve.UnityScene.Tests ["assetManifest"] = "cultmesh://aetheria/assets/manifest", ["inputProfile"] = "arpg-third-person", ["cameraRig"] = "third-person-orbit", + ["excludedRenderChannels"] = "map", ["playerEntityId"] = "player-vanguard", ["movementCommand"] = "aetheria.daemon.move_intent", ["focusCommand"] = "aetheria.daemon.focus", @@ -2120,6 +2125,24 @@ namespace GameCult.Eve.UnityScene.Tests } } + private sealed class FixedRenderChannelPolicy : IEveUnityCameraRenderPolicySource + { + private readonly string _channel; + private readonly int _layer; + + public FixedRenderChannelPolicy(string channel, int layer) + { + _channel = channel; + _layer = layer; + } + + public bool TryGetRenderChannelLayer(string channel, out int layer) + { + layer = _layer; + return string.Equals(channel, _channel, StringComparison.Ordinal); + } + } + private sealed class FakePlayableWorldProviderComponent : MonoBehaviour, IEveUnitySceneProviderSurfaceDocumentSource, diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 334ca56..02b50f3 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.2.4", + "version": "0.3.0", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 912e5aac7936a425b0c56790eb1c6a2f0abb1eae Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 21:49:25 +0100 Subject: [PATCH 013/292] Ignore hidden channels when framing cameras --- .../Runtime/EveUnityPlayableWorldCameraRig.cs | 9 ++++++--- .../Tests/Editor/EveUnitySceneSurfaceLowererTests.cs | 6 +++++- packages/org.gamecult.eve.unity-scene/package.json | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index b4de75c..fafe3c9 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -69,7 +69,9 @@ namespace GameCult.Eve.UnityScene } cameraComponent.cullingMask = cullingMask; } - var bounds = CalculateVisualBounds(player); + var bounds = CalculateVisualBounds( + player, + cameraComponent != null ? cameraComponent.cullingMask : -1); var target = bounds?.center ?? player.position; var radius = bounds?.extents.magnitude ?? 0f; var verticalFov = cameraComponent != null ? cameraComponent.fieldOfView : 60f; @@ -89,13 +91,14 @@ namespace GameCult.Eve.UnityScene return true; } - private static Bounds? CalculateVisualBounds(Transform player) + private static Bounds? CalculateVisualBounds(Transform player, int cullingMask) { var renderers = player.GetComponentsInChildren(includeInactive: false); Bounds? bounds = null; foreach (var visual in renderers) { - if (visual == null || !visual.enabled) + if (visual == null || !visual.enabled || + (cullingMask & (1 << visual.gameObject.layer)) == 0) continue; if (bounds.HasValue) { diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 9d47936..025582d 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -948,11 +948,15 @@ namespace GameCult.Eve.UnityScene.Tests var largeVisual = GameObject.CreatePrimitive(PrimitiveType.Cube); largeVisual.transform.SetParent(player.transform, false); largeVisual.transform.localScale = Vector3.one * 100f; + largeVisual.layer = 14; Assert.That(rig.ApplyRig(0f), Is.True); Assert.That(camera.cullingMask & (1 << 14), Is.Zero); Assert.That(cameraObject.transform.position.y, Is.GreaterThan(0f)); - Assert.That(Vector3.Distance(cameraObject.transform.position, largeVisual.transform.position), Is.GreaterThan(100f)); + Assert.That( + Vector3.Distance(cameraObject.transform.position, player.transform.position), + Is.LessThan(50f), + "An excluded map glyph changed pilot-camera framing."); } finally { diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 02b50f3..f460a19 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.0", + "version": "0.3.1", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 41008f1e467e314b2bfe6093b8aa8040eb1b84d5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 22:06:27 +0100 Subject: [PATCH 014/292] Report CultMesh provider identity mismatches --- .../Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs | 3 ++- packages/org.gamecult.eve.unity-scene/package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 8f51c57..084accb 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -346,7 +346,8 @@ namespace GameCult.Eve.UnityScene { ResolveAdvertisement(); if (!string.Equals(document.ProviderId, _advertisement!.ProviderId, StringComparison.Ordinal)) - throw new UnauthorizedAccessException("Entity layout provider does not match the advertised Eve provider."); + throw new UnauthorizedAccessException( + $"Entity layout provider '{document.ProviderId}' does not match advertised Eve provider '{_advertisement.ProviderId}'."); if (document.Buffers == null || document.Buffers.Length == 0) throw new InvalidOperationException("Entity layout does not name a primary logical buffer."); var handle = BodyPublicationHandle(document); diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index f460a19..19375e6 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.1", + "version": "0.3.2", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 9f4f88e43c06caea675904e61c1eb76734a13e26 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 22:21:47 +0100 Subject: [PATCH 015/292] Authorize body producers by advertised service --- .../EveUnityCultMeshLiveProviderTransport.cs | 19 +++++++- .../EveUnitySceneSurfaceLowererTests.cs | 48 +++++++++++++++++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 084accb..1c80bd6 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -55,7 +55,7 @@ namespace GameCult.Eve.UnityScene private CultNetDatabaseSubscriptionClient? _subscriptions; private EveProviderAdvertisementDocument? _advertisement; private EveAdvertisedSurface? _advertisedSurface; - private readonly CultMeshBodyPublicationResolver _bodyResolver; + private CultMeshBodyPublicationResolver? _bodyResolver; public EveUnityCultMeshLiveProviderTransport( string replicaPath, @@ -286,6 +286,7 @@ namespace GameCult.Eve.UnityScene private CultMeshBodyPublicationResolver CreateBodyResolver() { + var producerId = RequireAdvertisedBodyProducerId(_advertisement); var mappedRoot = Path.GetDirectoryName(_replicaPath) ?? "."; return new CultMeshBodyPublicationResolver(new CultMeshBodyTransportService( new ICultMeshBodyTransportAdapter[] @@ -294,7 +295,20 @@ namespace GameCult.Eve.UnityScene new CultMeshMappedBodyAdapter(mappedRoot), new CultMeshNetworkBodyAdapter(ReadNetworkBody) }, - (producerId, _) => string.Equals(producerId, _providerId, StringComparison.Ordinal))); + (candidateProducerId, _) => IsAdvertisedBodyProducer(producerId, candidateProducerId))); + } + + private static bool IsAdvertisedBodyProducer(string advertisedProducerId, string candidateProducerId) => + string.Equals(candidateProducerId, advertisedProducerId, StringComparison.Ordinal); + + private static string RequireAdvertisedBodyProducerId(EveProviderAdvertisementDocument? advertisement) + { + if (advertisement == null) + throw new InvalidOperationException("The Eve provider advertisement must be resolved before body transport authorization."); + if (string.IsNullOrWhiteSpace(advertisement.ServiceId)) + throw new InvalidOperationException( + $"Eve provider '{advertisement.ProviderId}' does not advertise a service identity for body production."); + return advertisement.ServiceId; } private byte[] ReadNetworkBody(CultMeshBodyDescriptor descriptor) @@ -362,6 +376,7 @@ namespace GameCult.Eve.UnityScene var now = DateTimeOffset.UtcNow; if (!IsPublicationLive(publication, now)) return; + _bodyResolver ??= CreateBodyResolver(); var lease = _bodyResolver.ResolveReadOnly(publication, new CultMeshBodyValidationRequest { BodyId = handle.BodyId, diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 025582d..4498bdf 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -89,6 +89,39 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(resolved, Is.EqualTo(bytes)); } + [Test] + public void LiveTransportAuthorizesBodiesWithAdvertisedServiceIdentity() + { + var advertisement = ProviderAdvertisement("aetheria", "aetheria.daemon"); + var method = typeof(EveUnityCultMeshLiveProviderTransport) + .GetMethod("RequireAdvertisedBodyProducerId", BindingFlags.NonPublic | BindingFlags.Static)!; + + var producerId = (string)method.Invoke(null, new object?[] { advertisement })!; + + Assert.That(producerId, Is.EqualTo("aetheria.daemon")); + Assert.That(producerId, Is.Not.EqualTo(advertisement.ProviderId)); + + var authorizes = typeof(EveUnityCultMeshLiveProviderTransport) + .GetMethod("IsAdvertisedBodyProducer", BindingFlags.NonPublic | BindingFlags.Static)!; + Assert.That(authorizes.Invoke(null, new object[] { producerId, "aetheria.daemon" }), Is.True); + Assert.That(authorizes.Invoke(null, new object[] { producerId, "aetheria" }), Is.False); + Assert.That(authorizes.Invoke(null, new object[] { producerId, "impostor" }), Is.False); + } + + [Test] + public void LiveTransportRejectsAdvertisementWithoutBodyProducerIdentity() + { + var advertisement = ProviderAdvertisement("aetheria", ""); + var method = typeof(EveUnityCultMeshLiveProviderTransport) + .GetMethod("RequireAdvertisedBodyProducerId", BindingFlags.NonPublic | BindingFlags.Static)!; + + var error = Assert.Throws(() => + method.Invoke(null, new object?[] { advertisement })); + + Assert.That(error!.InnerException, Is.TypeOf()); + StringAssert.Contains("does not advertise a service identity", error.InnerException!.Message); + } + [Test] public void EntitySoaViewReadsGenericSemanticColumnsFromInjectedLease() { @@ -2009,6 +2042,21 @@ namespace GameCult.Eve.UnityScene.Tests } }; + private static EveProviderAdvertisementDocument ProviderAdvertisement(string providerId, string serviceId) => + new EveProviderAdvertisementDocument( + providerId, + serviceId, + "test-verse", + "Test Provider", + "test", + "cultmesh://test/provider", + DateTimeOffset.UtcNow.ToString("O"), + new EveProviderFreshness("fresh", DateTimeOffset.UtcNow.ToString("O"), 5000), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty()); + private static CultMeshBodyPublicationDocument BodyPublication( EveEntitySoaViewDocument view, long sequence) diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 19375e6..064d093 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.2", + "version": "0.3.3", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 5d5b36cb980a322393a900a6a93ed716a8369b40 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 22:26:04 +0100 Subject: [PATCH 016/292] Defer body authorization until provider discovery --- .../EveUnityCultMeshLiveProviderTransport.cs | 2 +- .../Editor/EveUnitySceneSurfaceLowererTests.cs | 13 +++++++++++++ packages/org.gamecult.eve.unity-scene/package.json | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 1c80bd6..762ff17 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -78,7 +78,7 @@ namespace GameCult.Eve.UnityScene ? throw new ArgumentException("Surface id must be non-empty.", nameof(surfaceId)) : surfaceId.Trim(); _runtimeId = string.IsNullOrWhiteSpace(runtimeId) ? "eve-unity" : runtimeId.Trim(); - _bodyResolver = bodyResolver ?? CreateBodyResolver(); + _bodyResolver = bodyResolver; CurrentSurfaceDocument = EmptySurfaceDocument(); CurrentAssetManifestDocument = EmptyAssetManifest(); } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 4498bdf..14cd65e 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -14,6 +14,19 @@ namespace GameCult.Eve.UnityScene.Tests { public sealed class EveUnitySceneSurfaceLowererTests { + [Test] + public void LiveTransportDefersDefaultBodyAuthorizationUntilAdvertisementResolution() + { + Assert.DoesNotThrow(() => + { + using var transport = new EveUnityCultMeshLiveProviderTransport( + "test-replica.cc", + "cultnet://127.0.0.1:3075", + "aetheria", + "aetheria.pilot"); + }); + } + [Test] public async System.Threading.Tasks.Task LiveTransportResolvesViewGenerationWhenNextPublicationAlreadyExists() { diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 064d093..eb3c847 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.3", + "version": "0.3.4", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From d0aa50d31f3b6da96bffc194fdc3aa358f37e332 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 22:31:36 +0100 Subject: [PATCH 017/292] Advertise authorized CultMesh body producers --- .../Runtime/EveProviderDocuments.cs | 17 ++++++++++++----- packages/org.gamecult.eve.surface/package.json | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs b/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs index 874ace8..a52d436 100644 --- a/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs +++ b/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs @@ -27,8 +27,9 @@ namespace GameCult.Eve.Surface IReadOnlyList schemas, IReadOnlyList witnesses, IReadOnlyList surfaces, - IReadOnlyList commands) - : this(SchemaId, providerId, serviceId, verseId, title, kind, cultMeshAddress, updatedAtUtc, freshness, schemas, witnesses, surfaces, commands) + IReadOnlyList commands, + IReadOnlyList? authorizedBodyProducerIds = null) + : this(SchemaId, providerId, serviceId, verseId, title, kind, cultMeshAddress, updatedAtUtc, freshness, schemas, witnesses, surfaces, commands, authorizedBodyProducerIds) { } @@ -46,7 +47,8 @@ namespace GameCult.Eve.Surface IReadOnlyList schemas, IReadOnlyList witnesses, IReadOnlyList surfaces, - IReadOnlyList commands) + IReadOnlyList commands, + IReadOnlyList? authorizedBodyProducerIds = null) { Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema; ProviderId = providerId ?? ""; @@ -61,6 +63,7 @@ namespace GameCult.Eve.Surface Witnesses = witnesses ?? Array.Empty(); Surfaces = surfaces ?? Array.Empty(); Commands = commands ?? Array.Empty(); + AuthorizedBodyProducerIds = authorizedBodyProducerIds ?? Array.Empty(); } [Key(0)] public string Schema { get; } @@ -76,6 +79,7 @@ namespace GameCult.Eve.Surface [Key(10)] public IReadOnlyList Witnesses { get; } [Key(11)] public IReadOnlyList Surfaces { get; } [Key(12)] public IReadOnlyList Commands { get; } + [Key(13)] public IReadOnlyList AuthorizedBodyProducerIds { get; } } /// @@ -84,7 +88,7 @@ namespace GameCult.Eve.Surface public sealed class EveProviderAdvertisementCompatibilityFormatter : IMessagePackFormatter { - private const int CurrentFieldCount = 13; + private const int CurrentFieldCount = 14; private const int LegacyFieldCount = 16; public void Serialize( @@ -107,6 +111,7 @@ namespace GameCult.Eve.Surface Formatter>(options).Serialize(ref writer, value.Witnesses, options); Formatter>(options).Serialize(ref writer, value.Surfaces, options); Formatter>(options).Serialize(ref writer, value.Commands, options); + Formatter>(options).Serialize(ref writer, value.AuthorizedBodyProducerIds, options); } public EveProviderAdvertisementDocument? Deserialize( @@ -137,6 +142,7 @@ namespace GameCult.Eve.Surface var witnesses = fields > 10 ? Formatter>(options).Deserialize(ref reader, options) : null; var surfaces = fields > 11 ? Formatter>(options).Deserialize(ref reader, options) : null; var commands = fields > 12 ? Formatter>(options).Deserialize(ref reader, options) : null; + var authorizedBodyProducerIds = fields > 13 ? Formatter>(options).Deserialize(ref reader, options) : null; for (var index = CurrentFieldCount; index < fields; index++) reader.Skip(); return new EveProviderAdvertisementDocument( schema, providerId, serviceId, verseId, title, kind, address, updatedAtUtc, @@ -144,7 +150,8 @@ namespace GameCult.Eve.Surface schemas ?? Array.Empty(), witnesses ?? Array.Empty(), surfaces ?? Array.Empty(), - commands ?? Array.Empty()); + commands ?? Array.Empty(), + authorizedBodyProducerIds ?? Array.Empty()); } finally { reader.Depth--; } } diff --git a/packages/org.gamecult.eve.surface/package.json b/packages/org.gamecult.eve.surface/package.json index a529d21..0da4785 100644 --- a/packages/org.gamecult.eve.surface/package.json +++ b/packages/org.gamecult.eve.surface/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.surface", "displayName": "Eve Surface Contract", - "version": "0.2.0", + "version": "0.2.1", "unity": "2021.3", "description": "Unity-compatible DTOs for gamecult.eve.surface.v1 documents.", "dependencies": { From 2306b470f1753e3645de3be6766c9efe5c9f01a0 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 22:31:56 +0100 Subject: [PATCH 018/292] Enforce advertised body producer authority --- .../EveUnityCultMeshLiveProviderTransport.cs | 23 +++++--- .../EveUnitySceneSurfaceLowererTests.cs | 52 ++++++++++++++----- .../org.gamecult.eve.unity-scene/package.json | 4 +- 3 files changed, 55 insertions(+), 24 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 762ff17..0d57da3 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -286,7 +286,7 @@ namespace GameCult.Eve.UnityScene private CultMeshBodyPublicationResolver CreateBodyResolver() { - var producerId = RequireAdvertisedBodyProducerId(_advertisement); + var producerIds = RequireAdvertisedBodyProducerIds(_advertisement); var mappedRoot = Path.GetDirectoryName(_replicaPath) ?? "."; return new CultMeshBodyPublicationResolver(new CultMeshBodyTransportService( new ICultMeshBodyTransportAdapter[] @@ -295,20 +295,27 @@ namespace GameCult.Eve.UnityScene new CultMeshMappedBodyAdapter(mappedRoot), new CultMeshNetworkBodyAdapter(ReadNetworkBody) }, - (candidateProducerId, _) => IsAdvertisedBodyProducer(producerId, candidateProducerId))); + (candidateProducerId, _) => IsAdvertisedBodyProducer(producerIds, candidateProducerId))); } - private static bool IsAdvertisedBodyProducer(string advertisedProducerId, string candidateProducerId) => - string.Equals(candidateProducerId, advertisedProducerId, StringComparison.Ordinal); + private static bool IsAdvertisedBodyProducer( + IReadOnlyList advertisedProducerIds, + string candidateProducerId) => + advertisedProducerIds.Any(producerId => + string.Equals(candidateProducerId, producerId, StringComparison.Ordinal)); - private static string RequireAdvertisedBodyProducerId(EveProviderAdvertisementDocument? advertisement) + private static IReadOnlyList RequireAdvertisedBodyProducerIds(EveProviderAdvertisementDocument? advertisement) { if (advertisement == null) throw new InvalidOperationException("The Eve provider advertisement must be resolved before body transport authorization."); - if (string.IsNullOrWhiteSpace(advertisement.ServiceId)) + var producerIds = advertisement.AuthorizedBodyProducerIds + .Where(producerId => !string.IsNullOrWhiteSpace(producerId)) + .Distinct(StringComparer.Ordinal) + .ToArray(); + if (producerIds.Length == 0) throw new InvalidOperationException( - $"Eve provider '{advertisement.ProviderId}' does not advertise a service identity for body production."); - return advertisement.ServiceId; + $"Eve provider '{advertisement.ProviderId}' does not advertise an authorized body producer."); + return producerIds; } private byte[] ReadNetworkBody(CultMeshBodyDescriptor descriptor) diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 14cd65e..b202b93 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -103,36 +103,56 @@ namespace GameCult.Eve.UnityScene.Tests } [Test] - public void LiveTransportAuthorizesBodiesWithAdvertisedServiceIdentity() + public void LiveTransportAuthorizesOnlyExplicitlyAdvertisedBodyProducers() { - var advertisement = ProviderAdvertisement("aetheria", "aetheria.daemon"); + var advertisement = ProviderAdvertisement("aetheria", "aetheria-daemon-17", "aetheria.daemon"); var method = typeof(EveUnityCultMeshLiveProviderTransport) - .GetMethod("RequireAdvertisedBodyProducerId", BindingFlags.NonPublic | BindingFlags.Static)!; + .GetMethod("RequireAdvertisedBodyProducerIds", BindingFlags.NonPublic | BindingFlags.Static)!; - var producerId = (string)method.Invoke(null, new object?[] { advertisement })!; + var producerIds = (IReadOnlyList)method.Invoke(null, new object?[] { advertisement })!; - Assert.That(producerId, Is.EqualTo("aetheria.daemon")); - Assert.That(producerId, Is.Not.EqualTo(advertisement.ProviderId)); + Assert.That(producerIds, Is.EqualTo(new[] { "aetheria.daemon" })); + Assert.That(producerIds, Does.Not.Contain(advertisement.ProviderId)); + Assert.That(producerIds, Does.Not.Contain(advertisement.ServiceId)); var authorizes = typeof(EveUnityCultMeshLiveProviderTransport) .GetMethod("IsAdvertisedBodyProducer", BindingFlags.NonPublic | BindingFlags.Static)!; - Assert.That(authorizes.Invoke(null, new object[] { producerId, "aetheria.daemon" }), Is.True); - Assert.That(authorizes.Invoke(null, new object[] { producerId, "aetheria" }), Is.False); - Assert.That(authorizes.Invoke(null, new object[] { producerId, "impostor" }), Is.False); + Assert.That(authorizes.Invoke(null, new object[] { producerIds, "aetheria.daemon" }), Is.True); + Assert.That(authorizes.Invoke(null, new object[] { producerIds, "aetheria" }), Is.False); + Assert.That(authorizes.Invoke(null, new object[] { producerIds, "aetheria-daemon-17" }), Is.False); + Assert.That(authorizes.Invoke(null, new object[] { producerIds, "impostor" }), Is.False); } [Test] public void LiveTransportRejectsAdvertisementWithoutBodyProducerIdentity() { - var advertisement = ProviderAdvertisement("aetheria", ""); + var advertisement = ProviderAdvertisement("aetheria", "aetheria-daemon-17"); var method = typeof(EveUnityCultMeshLiveProviderTransport) - .GetMethod("RequireAdvertisedBodyProducerId", BindingFlags.NonPublic | BindingFlags.Static)!; + .GetMethod("RequireAdvertisedBodyProducerIds", BindingFlags.NonPublic | BindingFlags.Static)!; var error = Assert.Throws(() => method.Invoke(null, new object?[] { advertisement })); Assert.That(error!.InnerException, Is.TypeOf()); - StringAssert.Contains("does not advertise a service identity", error.InnerException!.Message); + StringAssert.Contains("does not advertise an authorized body producer", error.InnerException!.Message); + } + + [Test] + public void ProviderAdvertisementRoundTripPreservesBodyProducerAuthority() + { + var advertisement = ProviderAdvertisement( + "aetheria", + "aetheria-daemon-17", + "aetheria.daemon", + "aetheria.asset-publisher"); + + var bytes = MessagePack.MessagePackSerializer.Serialize(advertisement); + var decoded = MessagePack.MessagePackSerializer.Deserialize(bytes); + + Assert.That(decoded.ProviderId, Is.EqualTo("aetheria")); + Assert.That(decoded.ServiceId, Is.EqualTo("aetheria-daemon-17")); + Assert.That(decoded.AuthorizedBodyProducerIds, + Is.EqualTo(new[] { "aetheria.daemon", "aetheria.asset-publisher" })); } [Test] @@ -2055,7 +2075,10 @@ namespace GameCult.Eve.UnityScene.Tests } }; - private static EveProviderAdvertisementDocument ProviderAdvertisement(string providerId, string serviceId) => + private static EveProviderAdvertisementDocument ProviderAdvertisement( + string providerId, + string serviceId, + params string[] authorizedBodyProducerIds) => new EveProviderAdvertisementDocument( providerId, serviceId, @@ -2068,7 +2091,8 @@ namespace GameCult.Eve.UnityScene.Tests Array.Empty(), Array.Empty(), Array.Empty(), - Array.Empty()); + Array.Empty(), + authorizedBodyProducerIds); private static CultMeshBodyPublicationDocument BodyPublication( EveEntitySoaViewDocument view, diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index eb3c847..a5e4d6c 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,13 +1,13 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.4", + "version": "0.3.5", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { "org.gamecult.cultlib": "1.0.10", "org.gamecult.eve.plugin-fields": "0.1.1", - "org.gamecult.eve.surface": "0.2.0", + "org.gamecult.eve.surface": "0.2.1", "com.unity.mathematics": "1.3.2" }, "keywords": [ From c47f9b0b070b042f54641057bf1f1111d207c45c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 22:44:15 +0100 Subject: [PATCH 019/292] Fail closed on missing advertised bundle assets --- .../CultMesh/EveUnityCultMeshLiveProviderTransport.cs | 6 +++++- packages/org.gamecult.eve.unity-scene/package.json | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 0d57da3..10942bf 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -552,7 +552,11 @@ namespace GameCult.Eve.UnityScene foreach (var selection in group) { var value = bundle.LoadAsset(selection.Variant!.AssetKey); - if (value == null) continue; + if (value == null) + throw new InvalidOperationException( + $"Provider asset '{selection.Asset.AssetRef}' advertises missing Unity bundle asset " + + $"'{selection.Variant.AssetKey}'. Available assets: " + + string.Join(", ", bundle.GetAllAssetNames())); _nativeAssets[selection.Asset.AssetRef] = value; if (selection.Asset.Metadata.TryGetValue("presentationRole", out var role) && !string.IsNullOrWhiteSpace(role)) diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index a5e4d6c..58e78aa 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.5", + "version": "0.3.6", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From fd9e939b7b6dcc37fb4dc65746d652fc1ec147f4 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 22:58:01 +0100 Subject: [PATCH 020/292] Reject incompatible provider asset catalogs --- .../CultMesh/EveUnityCultMeshLiveProviderTransport.cs | 10 ++++++++++ packages/org.gamecult.eve.unity-scene/package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 10942bf..af544aa 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -531,6 +531,16 @@ namespace GameCult.Eve.UnityScene }) .Where(selection => selection.Variant != null) .ToArray(); + if (selected.Length == 0) + { + var advertisedVariants = catalog.Assets + .SelectMany(asset => asset.Variants.Select(variant => + $"{asset.AssetRef}:{variant.RuntimeId}/{variant.Platform}")) + .ToArray(); + throw new InvalidOperationException( + $"Provider asset catalog '{catalog.CatalogId}' has no unity-scene/{CurrentBundlePlatform()} variant. " + + $"Advertised variants: {string.Join(", ", advertisedVariants)}"); + } ReadCameraPolicies(selected.Select(selection => selection.Variant!)); foreach (var group in selected.GroupBy(selection => selection.Variant!.Uri, StringComparer.Ordinal)) { diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 58e78aa..0076b2d 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.6", + "version": "0.3.7", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 4733ac27199d7615fc97f47e779e7c2a79c35ca2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 23:07:55 +0100 Subject: [PATCH 021/292] Batch provider CDN chunk retrieval --- .../CultMesh/EveUnityCultMeshLiveProviderTransport.cs | 5 +++-- packages/org.gamecult.eve.unity-scene/package.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index af544aa..70e22b5 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -616,9 +616,10 @@ namespace GameCult.Eve.UnityScene { var bytes = new byte[checked((int)manifest.SizeBytes)]; var references = manifest.Chunks.OrderBy(chunk => chunk.Offset).ToArray(); - for (var index = 0; index < references.Length; index += 2) + const int chunkBatchSize = 32; + for (var index = 0; index < references.Length; index += chunkBatchSize) { - var batch = references.Skip(index).Take(2).ToArray(); + var batch = references.Skip(index).Take(chunkBatchSize).ToArray(); var chunks = _snapshot! .FetchDocumentsAsync( recordKeys: batch.Select(reference => reference.RecordKey).ToArray(), diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 0076b2d..2808a1e 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.7", + "version": "0.3.8", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 8881a3bbfea4f0e3df22e89c9a4a97b65613ff04 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 23:12:03 +0100 Subject: [PATCH 022/292] Keep provider discovery off the SoA hot path --- .../CultMesh/EveUnityCultMeshLiveProviderTransport.cs | 6 ++++-- packages/org.gamecult.eve.unity-scene/package.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 70e22b5..c1d220f 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -125,7 +125,7 @@ namespace GameCult.Eve.UnityScene public void Refresh() { EnsureOpen(); - ResolveAdvertisement(); + ResolveAdvertisement(forceRefresh: true); RefreshSurface(); RefreshAssetCatalog(); } @@ -266,8 +266,10 @@ namespace GameCult.Eve.UnityScene _networkRegistry); } - private void ResolveAdvertisement() + private void ResolveAdvertisement(bool forceRefresh = false) { + if (!forceRefresh && _advertisement != null && _advertisedSurface != null) + return; var documents = _snapshot! .FetchDocumentsAsync() .GetAwaiter() diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 2808a1e..149290e 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.8", + "version": "0.3.9", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 79acb1043403f207e1bdbe51a3e3caaf265d3575 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 23:16:07 +0100 Subject: [PATCH 023/292] Resolve SoA publications from the live replica --- .../EveUnityCultMeshLiveProviderTransport.cs | 15 ++++++++++----- .../org.gamecult.eve.unity-scene/package.json | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index c1d220f..125a956 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -374,11 +374,12 @@ namespace GameCult.Eve.UnityScene if (document.Buffers == null || document.Buffers.Length == 0) throw new InvalidOperationException("Entity layout does not name a primary logical buffer."); var handle = BodyPublicationHandle(document); - var publication = _snapshot! - .FetchDocumentsAsync( - recordKeys: new[] { handle.RecordKey.Value }, - schemaIds: new[] { CultMeshBodyPublicationSchemaVersions.Publication }) - .GetAwaiter().GetResult().FirstOrDefault() + var publication = _node!.Database.Cache.Get(handle.RecordKey) + ?? _snapshot! + .FetchDocumentsAsync( + recordKeys: new[] { handle.RecordKey.Value }, + schemaIds: new[] { CultMeshBodyPublicationSchemaVersions.Publication }) + .GetAwaiter().GetResult().FirstOrDefault() ?? throw new InvalidOperationException( $"Provider did not publish CultMesh body generation '{handle.RecordKey.Value}'."); handle.Validate(publication); @@ -489,6 +490,10 @@ namespace GameCult.Eve.UnityScene schemaIds: new[] { EveSurfaceDocument.SchemaId }, includeSnapshot: false) .GetAwaiter().GetResult(); + _subscriptions.SubscribeAsync( + "eve-unity-body-publications", + schemaIds: new[] { CultMeshBodyPublicationSchemaVersions.Publication }) + .GetAwaiter().GetResult(); _subscriptions.SubscribeAsync( "eve-unity-receipts", schemaIds: new[] { EveCommandReceiptDocument.SchemaId }, diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 149290e..35e0a9a 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.9", + "version": "0.3.10", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From f864bdea412eeec8b28a569285577b04e090def3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 23:21:28 +0100 Subject: [PATCH 024/292] Allow bounded CDN snapshot delivery --- .../Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs | 2 +- packages/org.gamecult.eve.unity-scene/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 125a956..ab3ed76 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -258,7 +258,7 @@ namespace GameCult.Eve.UnityScene ShardId = RemoteShardId, ShardEpoch = 1, ConnectTimeout = TimeSpan.FromSeconds(5), - ResponseTimeout = TimeSpan.FromSeconds(10), + ResponseTimeout = TimeSpan.FromSeconds(30), MessageIdPrefix = "eve-unity", RudpRuntimeId = _runtimeId, RudpMaxFragmentBytes = 1024 diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 35e0a9a..77c4245 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.10", + "version": "0.3.11", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From e3ccc75d5ee2c9dfa4b49f29bcbc901f8b1cbe5f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 23:44:55 +0100 Subject: [PATCH 025/292] Resolve normalized Unity bundle asset names --- .../EveUnityCultMeshLiveProviderTransport.cs | 12 +++++- .../EveUnityCultMeshAssetLookupTests.cs | 42 +++++++++++++++++++ .../EveUnityCultMeshAssetLookupTests.cs.meta | 2 + .../org.gamecult.eve.unity-scene/package.json | 2 +- 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs.meta diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index ab3ed76..385f8ab 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -566,14 +566,16 @@ namespace GameCult.Eve.UnityScene if (bundle == null) throw new InvalidOperationException($"Unity could not load provider asset bundle '{variant.Uri}'."); _assetBundles.Add(bundle); + var bundleAssetNames = bundle.GetAllAssetNames(); foreach (var selection in group) { - var value = bundle.LoadAsset(selection.Variant!.AssetKey); + var bundleAssetName = ResolveBundleAssetName(bundleAssetNames, selection.Variant!.AssetKey); + var value = bundleAssetName == null ? null : bundle.LoadAsset(bundleAssetName); if (value == null) throw new InvalidOperationException( $"Provider asset '{selection.Asset.AssetRef}' advertises missing Unity bundle asset " + $"'{selection.Variant.AssetKey}'. Available assets: " + - string.Join(", ", bundle.GetAllAssetNames())); + string.Join(", ", bundleAssetNames)); _nativeAssets[selection.Asset.AssetRef] = value; if (selection.Asset.Metadata.TryGetValue("presentationRole", out var role) && !string.IsNullOrWhiteSpace(role)) @@ -589,6 +591,12 @@ namespace GameCult.Eve.UnityScene CurrentAssetCatalogVersion = catalog.Version; } + private static string? ResolveBundleAssetName(IEnumerable bundleAssetNames, string advertisedAssetKey) + { + return bundleAssetNames.FirstOrDefault(assetName => + string.Equals(assetName, advertisedAssetKey, StringComparison.OrdinalIgnoreCase)); + } + private void ReadCameraPolicies(IEnumerable variants) { _renderChannelLayers.Clear(); diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs new file mode 100644 index 0000000..17603f9 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs @@ -0,0 +1,42 @@ +using System.Reflection; +using GameCult.Eve.UnityScene; +using NUnit.Framework; + +namespace GameCult.Eve.UnityScene.Tests +{ + public sealed class EveUnityCultMeshAssetLookupTests + { + [Test] + public void ResolvesUnityNormalizedBundleNamesWithoutChangingLogicalAssetIdentity() + { + var method = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod( + "ResolveBundleAssetName", + BindingFlags.Static | BindingFlags.NonPublic); + + Assert.That(method, Is.Not.Null); + var result = method!.Invoke(null, new object[] + { + new[] { "assets/generated/eve/thermal/death.asset" }, + "Assets/Generated/Eve/Thermal/Death.asset" + }); + + Assert.That(result, Is.EqualTo("assets/generated/eve/thermal/death.asset")); + } + + [Test] + public void RejectsBundleNamesThatDoNotMatchTheAdvertisedAsset() + { + var method = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod( + "ResolveBundleAssetName", + BindingFlags.Static | BindingFlags.NonPublic); + + var result = method!.Invoke(null, new object[] + { + new[] { "assets/generated/eve/thermal/heatstroke.asset" }, + "Assets/Generated/Eve/Thermal/Death.asset" + }); + + Assert.That(result, Is.Null); + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs.meta b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs.meta new file mode 100644 index 0000000..790a2ed --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d746a249576c8e74696ea8add7a25731 \ No newline at end of file diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 77c4245..bf99858 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.11", + "version": "0.3.12", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From acb341021f3bf06c8b866c3d1758b47e97c7b482 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 14 Jul 2026 00:01:48 +0100 Subject: [PATCH 026/292] Fetch initial live entity view explicitly --- .../CultMesh/EveUnityCultMeshLiveProviderTransport.cs | 8 +++++++- packages/org.gamecult.eve.unity-scene/package.json | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 385f8ab..422c52d 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -481,7 +481,13 @@ namespace GameCult.Eve.UnityScene recordKeys: new[] { entityViewPointer }, schemaIds: new[] { EveEntitySoaViewDocument.SchemaId }) .GetAwaiter().GetResult(); - if (_node.Database.Cache.Get(new CultRecordKey(entityViewPointer)) is EveEntitySoaViewDocument current) + var current = _node.Database.Cache.Get(new CultRecordKey(entityViewPointer)) as EveEntitySoaViewDocument + ?? _snapshot! + .FetchDocumentsAsync( + recordKeys: new[] { entityViewPointer }, + schemaIds: new[] { EveEntitySoaViewDocument.SchemaId }) + .GetAwaiter().GetResult().FirstOrDefault(); + if (current != null) _liveDocuments.Enqueue(current); } _subscriptions.SubscribeAsync( diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index bf99858..89cdd9a 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.12", + "version": "0.3.13", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 7bde1011a197a7c31df8d769daf8524ad35af7b1 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 14 Jul 2026 00:26:33 +0100 Subject: [PATCH 027/292] Poll pending CultMesh receipts by advertised record --- .../EveUnityCultMeshLiveProviderTransport.cs | 21 +++++++++++++++++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 422c52d..d5e2dac 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -56,6 +56,7 @@ namespace GameCult.Eve.UnityScene private EveProviderAdvertisementDocument? _advertisement; private EveAdvertisedSurface? _advertisedSurface; private CultMeshBodyPublicationResolver? _bodyResolver; + private DateTime _nextReceiptPollUtc; public EveUnityCultMeshLiveProviderTransport( string replicaPath, @@ -141,6 +142,26 @@ namespace GameCult.Eve.UnityScene else if (document is EveCommandReceiptDocument receipt) PublishReceipt(receipt); } + PollPendingReceipts(); + } + + private void PollPendingReceipts() + { + if (_pendingCommandIds.Count == 0 || DateTime.UtcNow < _nextReceiptPollUtc) + return; + _nextReceiptPollUtc = DateTime.UtcNow.AddMilliseconds(250); + var interaction = RequireWorldInteraction(); + if (string.IsNullOrWhiteSpace(interaction.ReceiptRecordRef)) + return; + var receipts = _snapshot! + .FetchDocumentsAsync( + recordKeys: _pendingCommandIds + .Select(commandId => ChildRecordKey(interaction.ReceiptRecordRef, commandId)) + .ToArray(), + schemaIds: new[] { EveCommandReceiptDocument.SchemaId }) + .GetAwaiter().GetResult(); + foreach (var receipt in receipts) + PublishReceipt(receipt); } public void SubmitCommand(EveSurfaceCommandRequest request) diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 89cdd9a..ebd9a68 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.13", + "version": "0.3.14", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 474cadac1c173e24b2c9b9ddde7b781059dbae4c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 14 Jul 2026 00:45:39 +0100 Subject: [PATCH 028/292] Poll advertised entity views for live updates --- .../EveUnityCultMeshLiveProviderTransport.cs | 40 ++++++++++++++++++- .../org.gamecult.eve.unity-scene/package.json | 2 +- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index d5e2dac..9129d74 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -57,6 +57,9 @@ namespace GameCult.Eve.UnityScene private EveAdvertisedSurface? _advertisedSurface; private CultMeshBodyPublicationResolver? _bodyResolver; private DateTime _nextReceiptPollUtc; + private DateTime _nextEntityViewPollUtc; + private long _lastQueuedEntityViewEpoch = -1; + private long _lastQueuedEntityViewSequence = -1; public EveUnityCultMeshLiveProviderTransport( string replicaPath, @@ -133,6 +136,7 @@ namespace GameCult.Eve.UnityScene public void PumpLiveEvents() { + PollCurrentEntityView(); while (_liveDocuments.TryDequeue(out var document)) { if (document is EveEntitySoaViewDocument entityView) @@ -145,6 +149,36 @@ namespace GameCult.Eve.UnityScene PollPendingReceipts(); } + private void PollCurrentEntityView() + { + if (DateTime.UtcNow < _nextEntityViewPollUtc) + return; + _nextEntityViewPollUtc = DateTime.UtcNow.AddMilliseconds(250); + var lowered = new EveUnitySceneSurfaceLowerer() + .Lower(CurrentSurfaceDocument.SurfaceDocument, CurrentSurfaceDocument.AdvertisedSurface); + var pointer = lowered.PlayableWorld?.EntityViewPointerId ?? ""; + if (string.IsNullOrWhiteSpace(pointer)) + return; + var current = _snapshot! + .FetchDocumentsAsync( + recordKeys: new[] { pointer }, + schemaIds: new[] { EveEntitySoaViewDocument.SchemaId }) + .GetAwaiter().GetResult().FirstOrDefault(); + if (current != null) + QueueEntityView(current); + } + + private void QueueEntityView(EveEntitySoaViewDocument document) + { + if (document.ProducerEpoch < _lastQueuedEntityViewEpoch || + (document.ProducerEpoch == _lastQueuedEntityViewEpoch && + document.Sequence <= _lastQueuedEntityViewSequence)) + return; + _lastQueuedEntityViewEpoch = document.ProducerEpoch; + _lastQueuedEntityViewSequence = document.Sequence; + _liveDocuments.Enqueue(document); + } + private void PollPendingReceipts() { if (_pendingCommandIds.Count == 0 || DateTime.UtcNow < _nextReceiptPollUtc) @@ -509,7 +543,7 @@ namespace GameCult.Eve.UnityScene schemaIds: new[] { EveEntitySoaViewDocument.SchemaId }) .GetAwaiter().GetResult().FirstOrDefault(); if (current != null) - _liveDocuments.Enqueue(current); + QueueEntityView(current); } _subscriptions.SubscribeAsync( "eve-unity-surface", @@ -530,7 +564,9 @@ namespace GameCult.Eve.UnityScene private void OnReplicatedDocumentChanged(CultNetReplicatedDocumentChange change) { - if (change.Document is EveEntitySoaViewDocument or EveSurfaceDocument or EveCommandReceiptDocument) + if (change.Document is EveEntitySoaViewDocument entityView) + QueueEntityView(entityView); + else if (change.Document is EveSurfaceDocument or EveCommandReceiptDocument) _liveDocuments.Enqueue(change.Document); } diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index ebd9a68..04df3ce 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.14", + "version": "0.3.15", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From b05e7daf05ddcf9aaa7f15fc09c9c10a593042f9 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 14 Jul 2026 00:49:36 +0100 Subject: [PATCH 029/292] Prove released Aetheria world lowering --- .../PlayMode/GenericWorldCaptureTests.cs | 118 ++++++++++++++--- ReleaseConsumerProject/Packages/manifest.json | 7 +- .../Packages/packages-lock.json | 119 ++++++++++++++++-- docs/aetheria-released-package-witness.md | 18 +++ scripts/run-aetheria-daemon-world-witness.ps1 | 54 ++++++-- 5 files changed, 279 insertions(+), 37 deletions(-) create mode 100644 docs/aetheria-released-package-witness.md diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index d5dc99b..9269482 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using GameCult.Caching; using GameCult.Caching.MessagePack; using GameCult.Eve.Surface; @@ -175,12 +176,16 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Path.Combine(Application.temporaryCachePath, $"eve-unity-{Guid.NewGuid():N}.cc"); var capturePath = Environment.GetEnvironmentVariable("EVEUNITY_AETHERIA_CAPTURE_PATH") ?? Path.Combine(Application.temporaryCachePath, "aetheria-daemon-world.png"); + var mapCapturePath = Environment.GetEnvironmentVariable("EVEUNITY_AETHERIA_MAP_CAPTURE_PATH") ?? + Path.Combine(Application.temporaryCachePath, "aetheria-daemon-map.png"); var root = new GameObject("Generic Eve World Projection"); var cameraObject = new GameObject("Generic Eve Capture Camera"); + var mapCameraObject = new GameObject("Generic Eve Map Camera"); var lightObject = new GameObject("Generic Eve World Light"); RenderTexture target = null; Texture2D pixels = null; EveUnityCultMeshPlayableWorldProvider provider = null; + EveUnityPlayableWorldClientHost host = null; EveUnityPlayableWorldRuntime runtime = null; WitnessReceipt movementReceipt = null; WitnessReceipt focusReceipt = null; @@ -217,18 +222,38 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(provider.Selection.VerseId, Is.Not.Empty); Assert.That(provider.Selection.ProviderId, Is.Not.Empty); Assert.That(provider.Selection.SurfaceId, Is.Not.Empty); - runtime = EveUnityPlayableWorldRuntime.CreateForGameObjectScene( + host = root.AddComponent(); + host.Configure( root.transform, provider, provider, - null, + provider, provider, provider); - var initial = runtime.Connect(); - Assert.That(initial.ActiveEntities, Is.GreaterThan(0)); + host.Connect(); + runtime = host.Runtime; + Assert.That(runtime, Is.Not.Null); Assert.That(runtime.ActiveWorld.PlayerEntityId, Is.Not.Empty); + var entityDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < entityDeadline && + (host.PresentedEntities?.CurrentGeneration == null || + host.PresentedEntities.CurrentGeneration.Entities.Count == 0)) + { + yield return new WaitForSecondsRealtime(0.1f); + host.Refresh(); + } + Assert.That(host.PresentedEntities?.CurrentGeneration, Is.Not.Null); + Assert.That(host.PresentedEntities.CurrentGeneration.Entities.Count, Is.GreaterThan(0)); var playerId = runtime.ActiveWorld.PlayerEntityId; + var playerFact = host.PresentedEntities.CurrentGeneration.Entities + .Single(entity => entity.EntityId == playerId); + var playerPrefab = provider.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( + playerFact.AssetRef, + playerFact.EntityKind, + "provider-asset-ref")); + Assert.That(playerPrefab, Is.Not.Null, + $"Provider asset catalog did not resolve player AssetRef '{playerFact.AssetRef}'."); var marker = FindMarker(root, playerId); Assert.That( marker.GetComponentsInChildren(includeInactive: true).Length, @@ -302,6 +327,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var camera = cameraObject.AddComponent(); camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(0.035f, 0.055f, 0.08f, 1f); + camera.cullingMask = -1; foreach (var entityMarker in root.GetComponentsInChildren()) { foreach (var renderer in entityMarker.GetComponentsInChildren()) @@ -316,15 +342,25 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var playerMarker = FindMarker(root, playerId); var playerRenderers = playerMarker.GetComponentsInChildren(); Assert.That(playerRenderers, Is.Not.Empty, "The provider-authored player prefab has no renderable visual."); - var playerBounds = playerRenderers[0].bounds; - foreach (var renderer in playerRenderers) - playerBounds.Encapsulate(renderer.bounds); - var halfFovRadians = camera.fieldOfView * 0.5f * Mathf.Deg2Rad; - var cameraDistance = Mathf.Max(8f, playerBounds.extents.magnitude / Mathf.Tan(halfFovRadians) * 1.35f); - camera.nearClipPlane = Mathf.Max(0.01f, cameraDistance * 0.001f); - camera.farClipPlane = Mathf.Max(1000f, cameraDistance + playerBounds.size.magnitude * 4f); - camera.transform.position = playerBounds.center + new Vector3(1f, 0.65f, -1.2f).normalized * cameraDistance; - camera.transform.LookAt(playerBounds.center); + Assert.That(provider.TryGetRenderChannelLayer("map", out var mapLayer), Is.True); + var mapRenderers = new List(); + foreach (var renderer in root.GetComponentsInChildren(includeInactive: true)) + { + if (renderer.gameObject.layer == mapLayer) + mapRenderers.Add(renderer); + } + Assert.That(mapRenderers, Is.Not.Empty, "The live provider prefab has no renderer assigned to its advertised map channel."); + + var cameraRig = cameraObject.AddComponent(); + cameraRig.Host = host; + cameraRig.CameraTransform = camera.transform; + cameraRig.RenderPolicySource = provider; + Assert.That(cameraRig.ApplyRig(0f), Is.True); + Assert.That(camera.cullingMask & (1 << mapLayer), Is.Zero); + + var mapCamera = mapCameraObject.AddComponent(); + mapCamera.cullingMask = 1 << mapLayer; + Assert.That(mapCamera.cullingMask & (1 << mapLayer), Is.Not.Zero); target = new RenderTexture(640, 360, 24, RenderTextureFormat.ARGB32); camera.targetTexture = target; yield return null; @@ -336,10 +372,36 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(capturePath))); File.WriteAllBytes(capturePath, pixels.EncodeToPNG()); Assert.That(new FileInfo(capturePath).Length, Is.GreaterThan(1024)); + + var mapBounds = mapRenderers[0].bounds; + foreach (var renderer in mapRenderers) + mapBounds.Encapsulate(renderer.bounds); + mapCamera.clearFlags = CameraClearFlags.SolidColor; + mapCamera.backgroundColor = camera.backgroundColor; + mapCamera.orthographic = true; + mapCamera.orthographicSize = Mathf.Max(0.1f, mapBounds.extents.x, mapBounds.extents.z) * 1.25f; + mapCamera.nearClipPlane = 0.01f; + mapCamera.farClipPlane = Mathf.Max(10f, mapBounds.size.magnitude * 4f); + mapCamera.transform.position = mapBounds.center + Vector3.up * (mapCamera.farClipPlane * 0.25f); + mapCamera.transform.rotation = Quaternion.Euler(90f, 0f, 0f); + mapCamera.targetTexture = target; + mapCamera.Render(); + RenderTexture.active = target; + pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); + pixels.Apply(); + var mapChangedPixels = CountChangedPixels(pixels, mapCamera.backgroundColor); + Assert.That(mapChangedPixels, Is.GreaterThan(25), "The map-only camera rendered no provider-authored map glyph pixels."); + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(mapCapturePath))); + File.WriteAllBytes(mapCapturePath, pixels.EncodeToPNG()); + Assert.That(new FileInfo(mapCapturePath).Length, Is.GreaterThan(1024)); WriteWitnessFacts( initialVersion, runtime.ActiveVersion, movementDistance, + mapRenderers.Count, + mapChangedPixels, + (camera.cullingMask & (1 << mapLayer)) == 0, + (mapCamera.cullingMask & (1 << mapLayer)) != 0, movementReceipt, focusReceipt, actionReceipt); @@ -349,10 +411,13 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests RenderTexture.active = null; var camera = cameraObject.GetComponent(); if (camera != null) camera.targetTexture = null; + var mapCamera = mapCameraObject.GetComponent(); + if (mapCamera != null) mapCamera.targetTexture = null; if (target != null) UnityEngine.Object.DestroyImmediate(target); if (pixels != null) UnityEngine.Object.DestroyImmediate(pixels); - runtime?.Dispose(); + host?.Disconnect(); UnityEngine.Object.DestroyImmediate(lightObject); + UnityEngine.Object.DestroyImmediate(mapCameraObject); UnityEngine.Object.DestroyImmediate(cameraObject); UnityEngine.Object.DestroyImmediate(root); } @@ -362,6 +427,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests long initialVersion, long finalVersion, float movementDistance, + int mapChannelRendererCount, + int mapChangedPixels, + bool pilotCameraExcludesMapChannel, + bool mapCameraIncludesMapChannel, WitnessReceipt movement, WitnessReceipt targeting, WitnessReceipt action) @@ -378,6 +447,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests initialVersion = initialVersion, finalVersion = finalVersion, movementDistance = movementDistance, + mapChannelRendererCount = mapChannelRendererCount, + mapChangedPixels = mapChangedPixels, + pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, + mapCameraIncludesMapChannel = mapCameraIncludesMapChannel, receipts = new[] { movement, targeting, action } }; Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); @@ -395,6 +468,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public long initialVersion; public long finalVersion; public float movementDistance; + public int mapChannelRendererCount; + public int mapChangedPixels; + public bool pilotCameraExcludesMapChannel; + public bool mapCameraIncludesMapChannel; public WitnessReceipt[] receipts; } @@ -428,6 +505,19 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests throw new InvalidOperationException($"Projected world has no entity marker '{entityId}'."); } + private static int CountChangedPixels(Texture2D texture, Color background) + { + var changed = 0; + foreach (var pixel in texture.GetPixels()) + { + if (Mathf.Abs(pixel.r - background.r) + + Mathf.Abs(pixel.g - background.g) + + Mathf.Abs(pixel.b - background.b) > 0.08f) + changed++; + } + return changed; + } + private static EveUnitySceneProviderSurfaceDocument WorldDocument() { var entities = new[] diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 8bea128..d4aefd5 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -5,10 +5,11 @@ "com.unity.modules.imageconversion": "1.0.0", "com.unity.modules.physics": "1.0.0", "com.unity.modules.uielements": "1.0.0", - "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.8", + "com.unity.render-pipelines.universal": "17.4.0", + "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.10", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", - "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.0", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.2.0", + "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.15", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 8d56d72..66c5fbc 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -1,5 +1,27 @@ { "dependencies": { + "com.unity.burst": { + "version": "1.8.29", + "depth": 2, + "source": "registry", + "dependencies": { + "com.unity.mathematics": "1.2.1", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.collections": { + "version": "6.4.0", + "depth": 2, + "source": "builtin", + "dependencies": { + "com.unity.burst": "1.8.23", + "com.unity.mathematics": "1.3.2", + "com.unity.nuget.mono-cecil": "1.11.5", + "com.unity.test-framework": "1.4.6", + "com.unity.test-framework.performance": "3.0.3" + } + }, "com.unity.ext.nunit": { "version": "2.0.5", "depth": 1, @@ -13,6 +35,60 @@ "dependencies": {}, "url": "https://packages.unity.com" }, + "com.unity.nuget.mono-cecil": { + "version": "1.11.6", + "depth": 3, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.render-pipelines.core": { + "version": "17.4.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.burst": "1.8.14", + "com.unity.mathematics": "1.3.2", + "com.unity.ugui": "2.0.0", + "com.unity.collections": "2.4.3", + "com.unity.modules.terrain": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.render-pipelines.universal": { + "version": "17.4.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.render-pipelines.core": "17.4.0", + "com.unity.shadergraph": "17.4.0", + "com.unity.render-pipelines.universal-config": "17.4.0" + } + }, + "com.unity.render-pipelines.universal-config": { + "version": "17.4.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.render-pipelines.core": "17.4.0" + } + }, + "com.unity.searcher": { + "version": "4.9.4", + "depth": 2, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.shadergraph": { + "version": "17.4.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.render-pipelines.core": "17.4.0", + "com.unity.searcher": "4.9.3" + } + }, "com.unity.test-framework": { "version": "1.6.0", "depth": 0, @@ -23,12 +99,31 @@ "com.unity.modules.jsonserialize": "1.0.0" } }, + "com.unity.test-framework.performance": { + "version": "3.4.0", + "depth": 3, + "source": "registry", + "dependencies": { + "com.unity.test-framework": "1.1.33", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ugui": { + "version": "2.0.0", + "depth": 2, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0" + } + }, "org.gamecult.cultlib": { - "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.7", + "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.10", "depth": 0, "source": "git", "dependencies": {}, - "hash": "1f8f832a788e2bdf59df90fb91da583574d3a42d" + "hash": "085aa2c0756328770623474c4e9fbacd7218b390" }, "org.gamecult.eve.plugin-fields": { "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", @@ -38,25 +133,25 @@ "hash": "0b099274a4e2b4c301eced4322166837792f585a" }, "org.gamecult.eve.surface": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#b6f001b300a88aff0bc13372b5c1dbc2002954f8", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", "depth": 0, "source": "git", "dependencies": { - "org.gamecult.cultlib": "1.0.6" + "org.gamecult.cultlib": "1.0.8" }, - "hash": "b6f001b300a88aff0bc13372b5c1dbc2002954f8" + "hash": "d0aa50d31f3b6da96bffc194fdc3aa358f37e332" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#b6f001b300a88aff0bc13372b5c1dbc2002954f8", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.15", "depth": 0, "source": "git", "dependencies": { - "org.gamecult.cultlib": "1.0.6", + "org.gamecult.cultlib": "1.0.10", "org.gamecult.eve.plugin-fields": "0.1.1", - "org.gamecult.eve.surface": "0.1.0", + "org.gamecult.eve.surface": "0.2.1", "com.unity.mathematics": "1.3.2" }, - "hash": "b6f001b300a88aff0bc13372b5c1dbc2002954f8" + "hash": "474cadac1c173e24b2c9b9ddde7b781059dbae4c" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0", @@ -103,6 +198,12 @@ "source": "builtin", "dependencies": {} }, + "com.unity.modules.terrain": { + "version": "1.0.0", + "depth": 2, + "source": "builtin", + "dependencies": {} + }, "com.unity.modules.ui": { "version": "1.0.0", "depth": 1, diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md new file mode 100644 index 0000000..cb64561 --- /dev/null +++ b/docs/aetheria-released-package-witness.md @@ -0,0 +1,18 @@ +# Aetheria released-package witness + +The warm-cache witness passes against `org.gamecult.eve.unity-scene` `0.3.15` +at commit `474cadac1c173e24b2c9b9ddde7b781059dbae4c`. + +Evidence is in `artifacts/render-channel-witness-41`: + +- `results.xml`: one passing PlayMode witness. +- `aetheria-daemon-world.png`: provider-authored 3D pilot view. +- `aetheria-daemon-map.png`: map-channel-only view. +- `witness-facts.json`: movement, targeting, action, receipts, and camera-mask facts. +- `runtime-witness.warm.json`: released package and screenshot hashes. + +The cold CDN path is not proven. `artifacts/render-channel-witness-21/results.xml` +records a timeout while fetching the 13,006,384-byte bundle through batches of +32 `gamecult.mesh.cdn_artifact_chunk.v1` snapshot records. The bundle must move +through the intended mapped/network body transport; increasing the snapshot +timeout is not an accepted fix. diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 137d7c4..e706ee5 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -1,6 +1,8 @@ param( [string] $UnityExe = "C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Unity.exe", [string] $AetheriaRoot = "E:\Projects\Aetheria", + [string] $CultLibRoot = "E:\Projects\CultLib", + [string] $EveUnityRoot = "E:\Projects\EveUnity", [string] $ClientProject = "ReleaseConsumerProject", [int] $Port = 3076, [string] $OutputDirectory = "artifacts\aetheria-daemon", @@ -18,6 +20,7 @@ $unityLogPath = Join-Path $outputRoot "unity.log" $bundleBuildLogPath = Join-Path $outputRoot "asset-bundle-build.log" $daemonLogPath = Join-Path $outputRoot "aetheria-daemon.log" $capturePath = Join-Path $outputRoot "aetheria-daemon-world.png" +$mapCapturePath = Join-Path $outputRoot "aetheria-daemon-map.png" $factsPath = Join-Path $outputRoot "witness-facts.json" $witnessPath = Join-Path $outputRoot "runtime-witness.json" $replicaPath = Join-Path $outputRoot "eve-unity-replica.cc" @@ -55,13 +58,24 @@ foreach ($ephemeralPath in @( if (Test-Path -LiteralPath $resolved) { Remove-Item -LiteralPath $resolved -Recurse -Force } } -$import = Start-Process -FilePath "dotnet" -ArgumentList @( - "run", "--project", $importProject, "--", $AetheriaRoot, $statePath -) -PassThru -WindowStyle Hidden -Wait ` - -RedirectStandardOutput (Join-Path $outputRoot "aetheria-import.log") ` - -RedirectStandardError (Join-Path $outputRoot "aetheria-import.error.log") -if ($import.ExitCode -ne 0) { - throw "Aetheria state import failed with exit code $($import.ExitCode). See $outputRoot\aetheria-import.error.log" +$importArguments = @( + "run", "--project", $importProject, + "-p:CultLibRoot=$CultLibRoot", "-p:EveUnityRoot=$EveUnityRoot", + "--", $AetheriaRoot, $statePath +) +$importLogPath = Join-Path $outputRoot "aetheria-import.log" +$importErrorLogPath = Join-Path $outputRoot "aetheria-import.error.log" +$previousErrorActionPreference = $ErrorActionPreference +try { + $ErrorActionPreference = "Continue" + & dotnet @importArguments 1> $importLogPath 2> $importErrorLogPath + $importExitCode = $LASTEXITCODE +} +finally { + $ErrorActionPreference = $previousErrorActionPreference +} +if ($importExitCode -ne 0) { + throw "Aetheria state import failed with exit code $importExitCode. See $importErrorLogPath" } if (-not $SkipAssetBundleBuild) { $bundleBuilder = Start-Process -FilePath $UnityExe -ArgumentList @( @@ -83,7 +97,9 @@ if (-not $SkipAssetBundleBuild) { } $daemonArguments = @( - "run", "--project", $daemonProject, "--", + "run", "--project", $daemonProject, + "-p:CultLibRoot=$CultLibRoot", "-p:EveUnityRoot=$EveUnityRoot", + "--", "--root", $AetheriaRoot, "--state", $statePath, "--client-cultmesh-host", "127.0.0.1", @@ -112,10 +128,12 @@ try { if (-not $ready) { throw "Aetheria daemon did not open CultMesh port $Port. See $daemonLogPath" } $env:EVEUNITY_RENDEZVOUS_ENDPOINT = "rudp://127.0.0.1:$Port" - Remove-Item Env:EVEUNITY_PROVIDER_ID -ErrorAction SilentlyContinue + $env:EVEUNITY_PROVIDER_ID = "aetheria" Remove-Item Env:EVEUNITY_SURFACE_ID -ErrorAction SilentlyContinue $env:EVEUNITY_REPLICA_PATH = $replicaPath $env:EVEUNITY_AETHERIA_CAPTURE_PATH = $capturePath + $env:EVEUNITY_AETHERIA_MAP_CAPTURE_PATH = $mapCapturePath + $env:EVEUNITY_DISABLE_AUTO_LAUNCHER = "1" $env:EVEUNITY_ASSET_CACHE_PATH = $assetCachePath $env:EVEUNITY_WITNESS_FACTS_PATH = $factsPath $arguments = @( @@ -135,6 +153,7 @@ try { $run = $results.SelectSingleNode("//test-run") if ($null -eq $run -or [int]$run.passed -ne 1 -or [int]$run.failed -ne 0) { throw "Live world witness did not pass: $resultsPath" } if (-not (Test-Path $capturePath) -or (Get-Item $capturePath).Length -lt 1024) { throw "Live world capture is missing: $capturePath" } + if (-not (Test-Path $mapCapturePath) -or (Get-Item $mapCapturePath).Length -lt 1024) { throw "Live map-channel capture is missing: $mapCapturePath" } if (-not (Test-Path $factsPath)) { throw "Live world witness facts are missing: $factsPath" } $facts = Get-Content -LiteralPath $factsPath -Raw | ConvertFrom-Json $releaseLock = Get-Content -LiteralPath (Join-Path $repoRoot "ReleaseConsumerProject\Packages\packages-lock.json") -Raw | ConvertFrom-Json @@ -144,6 +163,7 @@ try { $facts | Add-Member -NotePropertyName eveUnityPackageCommit -NotePropertyValue $releaseLock.dependencies.'org.gamecult.eve.unity-scene'.hash $facts | Add-Member -NotePropertyName cultLibPackageCommit -NotePropertyValue $releaseLock.dependencies.'org.gamecult.cultlib'.hash $capture = Get-Item -LiteralPath $capturePath + $mapCapture = Get-Item -LiteralPath $mapCapturePath $resultsArtifact = Get-Item -LiteralPath $resultsPath $durationMs = [Math]::Round(([DateTimeOffset]::UtcNow - $witnessStartedAt).TotalMilliseconds, 3) $testDurationMs = [Math]::Round(([double]$run.duration) * 1000, 3) @@ -152,7 +172,7 @@ try { witnessId = "eveunity.aetheria.game.$observedCacheState" runtimeId = "unity-scene" runtimeOwnerRepo = "EveUnity" - providerId = "aetheria.daemon" + providerId = "aetheria" surfaceId = "aetheria.game" projectionKind = "provider-authored-world-surface" status = "pass" @@ -169,6 +189,9 @@ try { height = 360 encodedSizeBytes = $capture.Length sha256 = (Get-FileHash -LiteralPath $capture.FullName -Algorithm SHA256).Hash.ToLowerInvariant() + mapEncodedSizeBytes = $mapCapture.Length + mapSha256 = (Get-FileHash -LiteralPath $mapCapture.FullName -Algorithm SHA256).Hash.ToLowerInvariant() + mapChangedPixels = $facts.mapChangedPixels } artifacts = @( [ordered]@{ @@ -179,6 +202,14 @@ try { width = 640 height = 360 }, + [ordered]@{ + kind = "unity-map-channel-png" + path = $mapCapture.Name + sha256 = (Get-FileHash -LiteralPath $mapCapture.FullName -Algorithm SHA256).Hash.ToLowerInvariant() + sizeBytes = $mapCapture.Length + width = 640 + height = 360 + }, [ordered]@{ kind = "unity-test-results" path = $resultsArtifact.Name @@ -193,11 +224,12 @@ try { $witness | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $stateWitnessPath -Encoding UTF8 Write-Host "Aetheria daemon world witness: passed" Write-Host "Capture: $capturePath" + Write-Host "Map capture: $mapCapturePath" Write-Host "Runtime witness: $witnessPath" Write-Host "Cache-state witness: $stateWitnessPath" } finally { if ($null -ne $daemon -and -not $daemon.HasExited) { Stop-Process -Id $daemon.Id -Force } - Remove-Item Env:EVEUNITY_RENDEZVOUS_ENDPOINT, Env:EVEUNITY_PROVIDER_ENDPOINT, Env:EVEUNITY_PROVIDER_ID, Env:EVEUNITY_SURFACE_ID, Env:EVEUNITY_REPLICA_PATH, Env:EVEUNITY_AETHERIA_CAPTURE_PATH, Env:EVEUNITY_ASSET_CACHE_PATH, Env:EVEUNITY_WITNESS_FACTS_PATH -ErrorAction SilentlyContinue + Remove-Item Env:EVEUNITY_RENDEZVOUS_ENDPOINT, Env:EVEUNITY_PROVIDER_ENDPOINT, Env:EVEUNITY_PROVIDER_ID, Env:EVEUNITY_SURFACE_ID, Env:EVEUNITY_REPLICA_PATH, Env:EVEUNITY_AETHERIA_CAPTURE_PATH, Env:EVEUNITY_AETHERIA_MAP_CAPTURE_PATH, Env:EVEUNITY_DISABLE_AUTO_LAUNCHER, Env:EVEUNITY_ASSET_CACHE_PATH, Env:EVEUNITY_WITNESS_FACTS_PATH -ErrorAction SilentlyContinue Remove-Item Env:AETHERIA_TRACE_EVE_SNAPSHOTS -ErrorAction SilentlyContinue } From cf7f5b4fd8b92036823c29f3df6c36d2f311e650 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 14 Jul 2026 01:26:47 +0100 Subject: [PATCH 030/292] Load provider assets through CultMesh content sessions --- .../EveUnityCultMeshLiveProviderTransport.cs | 149 ++++++++---------- .../EveUnitySceneSurfaceLowererTests.cs | 13 +- .../org.gamecult.eve.unity-scene/package.json | 4 +- 3 files changed, 74 insertions(+), 92 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 9129d74..7eb8f74 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Security.Cryptography; using System.Collections.Concurrent; using GameCult.Caching; using GameCult.Caching.MessagePack; @@ -33,7 +32,7 @@ namespace GameCult.Eve.UnityScene typeof(CultMeshBodyPublicationDocument), typeof(CultMeshNetworkBodyDocument), typeof(CultMeshCdnArtifactManifest), - typeof(CultMeshCdnArtifactChunk) + typeof(CultMeshContentTransferStateDocument) }; private readonly string _replicaPath; @@ -53,6 +52,8 @@ namespace GameCult.Eve.UnityScene private CultNetDocumentRegistry? _networkRegistry; private CultNetShardDescriptor? _replicaShard; private CultNetDatabaseSubscriptionClient? _subscriptions; + private CultMeshClient? _meshClient; + private CultMeshContentTransferService? _contentTransfer; private EveProviderAdvertisementDocument? _advertisement; private EveAdvertisedSurface? _advertisedSurface; private CultMeshBodyPublicationResolver? _bodyResolver; @@ -242,9 +243,12 @@ namespace GameCult.Eve.UnityScene _node?.Dispose(); _snapshot?.Dispose(); _subscriptions?.Dispose(); + _meshClient?.Dispose(); _node = null; _snapshot = null; _subscriptions = null; + _meshClient = null; + _contentTransfer = null; _networkRegistry = null; _replicaShard = null; _pendingCommandIds.Clear(); @@ -319,6 +323,10 @@ namespace GameCult.Eve.UnityScene RudpMaxFragmentBytes = 1024 }, _networkRegistry); + _meshClient = new CultMeshClient(new CultMeshClientOptions + { + RendezvousEndpoints = new[] { _endpoint } + }); } private void ResolveAdvertisement(bool forceRefresh = false) @@ -389,35 +397,41 @@ namespace GameCult.Eve.UnityScene schemaIds: new[] { CultMeshCdnSchemaVersions.ArtifactManifest }) .GetAwaiter().GetResult().SingleOrDefault() ?? throw new FileNotFoundException("Provider network body manifest is missing.", binding.ManifestRecordKey); - var chunks = _snapshot - .FetchDocumentsAsync( - recordKeys: manifest.Chunks.Select(chunk => chunk.RecordKey).ToArray(), - schemaIds: new[] { CultMeshCdnSchemaVersions.ArtifactChunk }) - .GetAwaiter().GetResult(); - return ResolveNetworkBody(descriptor, binding, manifest, chunks); + ValidateNetworkBody(descriptor, binding, manifest); + var path = ContentTransfer().FetchAsync(manifest).GetAwaiter().GetResult(); + return File.ReadAllBytes(path); } - private static byte[] ResolveNetworkBody( + private static void ValidateNetworkBody( CultMeshBodyDescriptor descriptor, CultMeshNetworkBodyDocument binding, - CultMeshCdnArtifactManifest manifest, - IReadOnlyList chunks) + CultMeshCdnArtifactManifest manifest) { - var registry = CultMesh.CreateCultCacheDocumentRegistry( - typeof(CultMeshNetworkBodyDocument), - typeof(CultMeshCdnArtifactManifest), - typeof(CultMeshCdnArtifactChunk)); - var cache = new CultCache(registry); - cache.UpsertAsync(binding, new CultRecordHandle(binding.RecordKey)) - .GetAwaiter().GetResult(); - cache.UpsertAsync(manifest, new CultRecordHandle( - CultMeshCdnArtifactManifest.CreateRecordKey(manifest))) - .GetAwaiter().GetResult(); - foreach (var chunk in chunks) - cache.UpsertAsync(chunk, new CultRecordHandle( - CultMeshCdnArtifactChunk.CreateRecordKey(chunk))) - .GetAwaiter().GetResult(); - return new CultMeshNetworkBodyResolver(cache).Fetch(descriptor); + if (!string.Equals(binding.CapabilityToken, descriptor.CapabilityToken, StringComparison.Ordinal) || + !string.Equals(binding.BodyId, descriptor.BodyId, StringComparison.Ordinal) || + !string.Equals(binding.SchemaId, descriptor.SchemaId, StringComparison.Ordinal) || + binding.LayoutVersion != descriptor.LayoutVersion || binding.ByteSize != descriptor.ByteSize || + binding.Capacity != descriptor.Capacity || binding.ProducerEpoch != descriptor.ProducerEpoch || + binding.Sequence != descriptor.Sequence || binding.Synchronization != descriptor.Synchronization || + binding.LeaseExpiresAtUnixMs != descriptor.LeaseExpiresAtUnixMs || + !string.Equals(binding.SemanticHash, descriptor.SemanticHash, StringComparison.Ordinal)) + throw new InvalidDataException("CultMesh network body descriptor disagrees with its capability binding."); + if (!string.Equals(CultMeshCdnArtifactManifest.CreateRecordKey(manifest).Value, + binding.ManifestRecordKey, StringComparison.Ordinal) || + manifest.SizeBytes != binding.ByteSize || + !string.Equals(NormalizeContentHash(manifest.ContentHash), + binding.SemanticHash, StringComparison.Ordinal)) + throw new InvalidDataException("CultMesh network body manifest disagrees with its capability binding."); + } + + private static string NormalizeContentHash(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new InvalidDataException("CultMesh content hash is missing."); + var normalized = value.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase) + ? value.Substring("sha256:".Length) + : value; + return normalized.ToLowerInvariant(); } private void PublishEntityView(EveEntitySoaViewDocument document) @@ -623,9 +637,8 @@ namespace GameCult.Eve.UnityScene var descriptor = manifest.FirstOrDefault(); if (descriptor == null) throw new InvalidOperationException($"Provider asset bundle manifest '{variant.Uri}' was not available."); - var bytes = ReadCachedBundle(descriptor, variant); - VerifyBundle(bytes, variant); - var bundle = AssetBundle.LoadFromMemory(bytes); + var path = ReadVerifiedBundlePath(descriptor, variant); + var bundle = AssetBundle.LoadFromFile(path); if (bundle == null) throw new InvalidOperationException($"Unity could not load provider asset bundle '{variant.Uri}'."); _assetBundles.Add(bundle); @@ -690,70 +703,38 @@ namespace GameCult.Eve.UnityScene : Application.platform.ToString(); } - private byte[] ReadBundle(CultMeshCdnArtifactManifest manifest) - { - var bytes = new byte[checked((int)manifest.SizeBytes)]; - var references = manifest.Chunks.OrderBy(chunk => chunk.Offset).ToArray(); - const int chunkBatchSize = 32; - for (var index = 0; index < references.Length; index += chunkBatchSize) - { - var batch = references.Skip(index).Take(chunkBatchSize).ToArray(); - var chunks = _snapshot! - .FetchDocumentsAsync( - recordKeys: batch.Select(reference => reference.RecordKey).ToArray(), - schemaIds: new[] { CultMeshCdnSchemaVersions.ArtifactChunk }) - .GetAwaiter() - .GetResult(); - foreach (var reference in batch) - { - var chunk = chunks.FirstOrDefault(candidate => - string.Equals(candidate.ChunkHash, reference.ChunkHash, StringComparison.Ordinal)); - if (chunk == null) - throw new InvalidOperationException($"Provider asset chunk '{reference.RecordKey}' was not available."); - if (chunk.SizeBytes != reference.SizeBytes || chunk.Payload.Length != reference.SizeBytes) - throw new InvalidDataException($"Provider asset chunk '{reference.RecordKey}' size did not match its manifest."); - Buffer.BlockCopy(chunk.Payload, 0, bytes, checked((int)reference.Offset), reference.SizeBytes); - } - } - return bytes; - } - - private byte[] ReadCachedBundle(CultMeshCdnArtifactManifest manifest, EveAssetVariant variant) + private CultMeshContentTransferService ContentTransfer() { + ResolveAdvertisement(); + if (_contentTransfer != null) + return _contentTransfer; + if (string.IsNullOrWhiteSpace(_advertisement!.ServiceId)) + throw new InvalidOperationException($"Eve provider '{_advertisement.ProviderId}' has no service-instance identity for content transport."); var cacheRoot = Environment.GetEnvironmentVariable("EVEUNITY_ASSET_CACHE_PATH"); if (string.IsNullOrWhiteSpace(cacheRoot)) cacheRoot = Path.Combine(Application.persistentDataPath, "EveUnity", "assets"); - Directory.CreateDirectory(cacheRoot); - var hash = variant.ContentHash.StartsWith("sha256:", StringComparison.Ordinal) - ? variant.ContentHash.Substring("sha256:".Length) - : variant.ContentHash; - var cachePath = Path.Combine(cacheRoot, hash + ".bundle"); - if (File.Exists(cachePath)) - { - var cached = File.ReadAllBytes(cachePath); - VerifyBundle(cached, variant); - return cached; - } - - var bytes = ReadBundle(manifest); - VerifyBundle(bytes, variant); - var temporaryPath = cachePath + ".tmp"; - File.WriteAllBytes(temporaryPath, bytes); - if (File.Exists(cachePath)) File.Delete(cachePath); - File.Move(temporaryPath, cachePath); - return bytes; + _contentTransfer = new CultMeshContentTransferService( + _node!.Cache, + new[] + { + _meshClient!.ContentProvider( + _advertisement.ProviderId, + _advertisement.ServiceId, + new CultMeshSessionContentProviderOptions { ResponseTimeout = TimeSpan.FromSeconds(30) }) + }, + new CultMeshContentTransferOptions(cacheRoot)); + return _contentTransfer; } - private static void VerifyBundle(byte[] bytes, EveAssetVariant variant) + private string ReadVerifiedBundlePath(CultMeshCdnArtifactManifest manifest, EveAssetVariant variant) { - if (bytes.LongLength != variant.SizeBytes) + var hash = NormalizeContentHash(variant.ContentHash); + if (manifest.SizeBytes != variant.SizeBytes) throw new InvalidOperationException($"Provider asset bundle '{variant.Uri}' size did not match its catalog."); - byte[] digest; - using (var sha256 = SHA256.Create()) - digest = sha256.ComputeHash(bytes); - var actual = "sha256:" + string.Concat(digest.Select(value => value.ToString("x2"))); - if (!string.Equals(actual, variant.ContentHash, StringComparison.Ordinal)) + if (!string.Equals(NormalizeContentHash(manifest.ContentHash), + hash, StringComparison.Ordinal)) throw new InvalidOperationException($"Provider asset bundle '{variant.Uri}' hash did not match its catalog."); + return ContentTransfer().FetchAsync(manifest).GetAwaiter().GetResult(); } private void PublishReceipt(EveCommandReceiptDocument receipt) diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index b202b93..6b3d0ff 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -71,7 +71,7 @@ namespace GameCult.Eve.UnityScene.Tests } [Test] - public async System.Threading.Tasks.Task LiveTransportResolvesExactNetworkBodyThroughVerseDocuments() + public async System.Threading.Tasks.Task LiveTransportValidatesNetworkBodyControlDocumentsWithoutSnapshotChunks() { var view = EntityLeaseDocument(); var bytes = Enumerable.Range(0, 32).Select(value => (byte)value).ToArray(); @@ -92,14 +92,15 @@ namespace GameCult.Eve.UnityScene.Tests var binding = source.Get( CultMeshNetworkBodyDocument.CreateRecordKey(descriptor.CapabilityToken))!; var manifest = source.Get(new CultRecordKey(binding.ManifestRecordKey))!; - var chunks = manifest.Chunks.Select(reference => - source.Get(new CultRecordKey(reference.RecordKey))!).ToArray(); var method = typeof(EveUnityCultMeshLiveProviderTransport) - .GetMethod("ResolveNetworkBody", BindingFlags.NonPublic | BindingFlags.Static)!; + .GetMethod("ValidateNetworkBody", BindingFlags.NonPublic | BindingFlags.Static)!; - var resolved = (byte[])method.Invoke(null, new object[] { descriptor, binding, manifest, chunks })!; + method.Invoke(null, new object[] { descriptor, binding, manifest }); + var wireTypes = (Type[])typeof(EveUnityCultMeshLiveProviderTransport) + .GetField("WireDocumentTypes", BindingFlags.NonPublic | BindingFlags.Static)! + .GetValue(null)!; - Assert.That(resolved, Is.EqualTo(bytes)); + Assert.That(wireTypes.Any(type => type == typeof(CultMeshCdnArtifactChunk)), Is.False); } [Test] diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 04df3ce..77b9c43 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,11 +1,11 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.15", + "version": "0.3.16", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { - "org.gamecult.cultlib": "1.0.10", + "org.gamecult.cultlib": "1.0.11", "org.gamecult.eve.plugin-fields": "0.1.1", "org.gamecult.eve.surface": "0.2.1", "com.unity.mathematics": "1.3.2" From fa5205a7530566ff37f03517c995ddbf2804a465 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 14 Jul 2026 01:28:09 +0100 Subject: [PATCH 031/292] Pin released content-session packages --- ReleaseConsumerProject/Packages/manifest.json | 4 ++-- ReleaseConsumerProject/Packages/packages-lock.json | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index d4aefd5..b4f30fb 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -6,10 +6,10 @@ "com.unity.modules.physics": "1.0.0", "com.unity.modules.uielements": "1.0.0", "com.unity.render-pipelines.universal": "17.4.0", - "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.10", + "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.11", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.15", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.16", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 66c5fbc..64689d8 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -119,11 +119,11 @@ } }, "org.gamecult.cultlib": { - "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.10", + "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.11", "depth": 0, "source": "git", "dependencies": {}, - "hash": "085aa2c0756328770623474c4e9fbacd7218b390" + "hash": "dc3ff2d9bbdeb4c60e55e01b83d1e443997ed4d3" }, "org.gamecult.eve.plugin-fields": { "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", @@ -142,16 +142,16 @@ "hash": "d0aa50d31f3b6da96bffc194fdc3aa358f37e332" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.15", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.16", "depth": 0, "source": "git", "dependencies": { - "org.gamecult.cultlib": "1.0.10", + "org.gamecult.cultlib": "1.0.11", "org.gamecult.eve.plugin-fields": "0.1.1", "org.gamecult.eve.surface": "0.2.1", "com.unity.mathematics": "1.3.2" }, - "hash": "474cadac1c173e24b2c9b9ddde7b781059dbae4c" + "hash": "cf7f5b4fd8b92036823c29f3df6c36d2f311e650" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0", From 979eee3f8e327c904d25d2d3091a9a6db977842d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 14 Jul 2026 01:35:40 +0100 Subject: [PATCH 032/292] Release EveUnity Scene 0.3.17 with CultLib 1.0.12 --- packages/org.gamecult.eve.unity-scene/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 77b9c43..30340a3 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,11 +1,11 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.16", + "version": "0.3.17", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { - "org.gamecult.cultlib": "1.0.11", + "org.gamecult.cultlib": "1.0.12", "org.gamecult.eve.plugin-fields": "0.1.1", "org.gamecult.eve.surface": "0.2.1", "com.unity.mathematics": "1.3.2" From 28137d90799ecaafe43ad648e48b62fa5f93f214 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 14 Jul 2026 01:36:35 +0100 Subject: [PATCH 033/292] Pin corrected content-session releases --- ReleaseConsumerProject/Packages/manifest.json | 4 ++-- ReleaseConsumerProject/Packages/packages-lock.json | 10 +++++----- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index b4f30fb..f2320b6 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -6,10 +6,10 @@ "com.unity.modules.physics": "1.0.0", "com.unity.modules.uielements": "1.0.0", "com.unity.render-pipelines.universal": "17.4.0", - "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.11", + "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.12", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.16", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.17", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 64689d8..05c3daa 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -119,11 +119,11 @@ } }, "org.gamecult.cultlib": { - "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.11", + "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.12", "depth": 0, "source": "git", "dependencies": {}, - "hash": "dc3ff2d9bbdeb4c60e55e01b83d1e443997ed4d3" + "hash": "26b4d1eadc4d0b1fe7a5ffb29a64373576544447" }, "org.gamecult.eve.plugin-fields": { "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", @@ -142,16 +142,16 @@ "hash": "d0aa50d31f3b6da96bffc194fdc3aa358f37e332" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.16", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.17", "depth": 0, "source": "git", "dependencies": { - "org.gamecult.cultlib": "1.0.11", + "org.gamecult.cultlib": "1.0.12", "org.gamecult.eve.plugin-fields": "0.1.1", "org.gamecult.eve.surface": "0.2.1", "com.unity.mathematics": "1.3.2" }, - "hash": "cf7f5b4fd8b92036823c29f3df6c36d2f311e650" + "hash": "979eee3f8e327c904d25d2d3091a9a6db977842d" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0", diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index e706ee5..16e2629 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -37,7 +37,7 @@ New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null if ($CacheState -eq "cold" -and (Test-Path -LiteralPath $assetCachePath)) { Remove-Item -LiteralPath $assetCachePath -Recurse -Force } -$cacheWasWarm = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.bundle -File -ErrorAction SilentlyContinue).Count -gt 0 +$cacheWasWarm = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.body -File -ErrorAction SilentlyContinue).Count -gt 0 if ($CacheState -eq "warm" -and -not $cacheWasWarm) { throw "Warm witness requested without an existing verified bundle cache at $assetCachePath" } From 4ae82bdf569e086c39d37bcdfbdc331321350a73 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 14 Jul 2026 01:45:35 +0100 Subject: [PATCH 034/292] Record released warm proof and cold transfer limit --- docs/aetheria-released-package-witness.md | 26 +++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index cb64561..83dadd9 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -1,18 +1,26 @@ # Aetheria released-package witness -The warm-cache witness passes against `org.gamecult.eve.unity-scene` `0.3.15` -at commit `474cadac1c173e24b2c9b9ddde7b781059dbae4c`. +The warm-cache witness passes against `org.gamecult.eve.unity-scene` `0.3.17` +at commit `979eee3f8e327c904d25d2d3091a9a6db977842d` and +`org.gamecult.cultlib` `1.0.12` at commit +`26b4d1eadc4d0b1fe7a5ffb29a64373576544447`. -Evidence is in `artifacts/render-channel-witness-41`: +Evidence is in `artifacts/render-channel-witness-content-warm`: - `results.xml`: one passing PlayMode witness. - `aetheria-daemon-world.png`: provider-authored 3D pilot view. - `aetheria-daemon-map.png`: map-channel-only view. - `witness-facts.json`: movement, targeting, action, receipts, and camera-mask facts. -- `runtime-witness.warm.json`: released package and screenshot hashes. +- `runtime-witness.warm.json`: released package commits and screenshot hashes. -The cold CDN path is not proven. `artifacts/render-channel-witness-21/results.xml` -records a timeout while fetching the 13,006,384-byte bundle through batches of -32 `gamecult.mesh.cdn_artifact_chunk.v1` snapshot records. The bundle must move -through the intended mapped/network body transport; increasing the snapshot -timeout is not an accepted fix. +The warm cache was seeded from the previously hash-verified bundle and renamed +to CultMesh's content-addressed `.body` cache shape. This proves released-client +loading and lowering, not cold delivery. + +The cold CDN path remains failed. The 2026-07-14 run in +`artifacts/render-channel-witness-content-cold` used the managed +`cultmesh.content.v1` session and created the resumable 13,006,384-byte partial +body, but transferred only about 5.3 MB before the fixed 300-second witness +deadline. Snapshot chunk batching is no longer involved. The remaining fault is +the sequential one-chunk request path over RUDP; increasing the witness timeout +is not an accepted fix. From ed21479e467faea8ff624f2d2f5a7d2fecf913f4 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 14 Jul 2026 03:27:59 +0100 Subject: [PATCH 035/292] Release EveUnity Scene 0.3.18 with CultLib 1.0.13 --- packages/org.gamecult.eve.unity-scene/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 30340a3..7d51868 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,11 +1,11 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.17", + "version": "0.3.18", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { - "org.gamecult.cultlib": "1.0.12", + "org.gamecult.cultlib": "1.0.13", "org.gamecult.eve.plugin-fields": "0.1.1", "org.gamecult.eve.surface": "0.2.1", "com.unity.mathematics": "1.3.2" From 3948379fec543ca673bce745014c124ee752e47f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 14 Jul 2026 03:39:21 +0100 Subject: [PATCH 036/292] Pin content throughput package releases --- ReleaseConsumerProject/Packages/manifest.json | 4 ++-- ReleaseConsumerProject/Packages/packages-lock.json | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index f2320b6..f929590 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -6,10 +6,10 @@ "com.unity.modules.physics": "1.0.0", "com.unity.modules.uielements": "1.0.0", "com.unity.render-pipelines.universal": "17.4.0", - "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.12", + "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.17", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.18", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 05c3daa..6023605 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -119,11 +119,11 @@ } }, "org.gamecult.cultlib": { - "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.12", + "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "depth": 0, "source": "git", "dependencies": {}, - "hash": "26b4d1eadc4d0b1fe7a5ffb29a64373576544447" + "hash": "feb5c71513e71d681699f462fe3682b3168c6f73" }, "org.gamecult.eve.plugin-fields": { "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", @@ -142,16 +142,16 @@ "hash": "d0aa50d31f3b6da96bffc194fdc3aa358f37e332" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.17", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.18", "depth": 0, "source": "git", "dependencies": { - "org.gamecult.cultlib": "1.0.12", + "org.gamecult.cultlib": "1.0.13", "org.gamecult.eve.plugin-fields": "0.1.1", "org.gamecult.eve.surface": "0.2.1", "com.unity.mathematics": "1.3.2" }, - "hash": "979eee3f8e327c904d25d2d3091a9a6db977842d" + "hash": "ed21479e467faea8ff624f2d2f5a7d2fecf913f4" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0", From b45b08e23277b1bd5b6189c75dabc0bc502ce047 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 14 Jul 2026 03:59:45 +0100 Subject: [PATCH 037/292] Record released cold content and warm camera proof --- .../PlayMode/GenericWorldCaptureTests.cs | 8 +- .../PlayMode/GenericWorldCaptureTests.cs | 126 +++++++++++++++--- docs/aetheria-released-package-witness.md | 45 ++++--- 3 files changed, 148 insertions(+), 31 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 9269482..31c58b6 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -248,6 +248,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var playerId = runtime.ActiveWorld.PlayerEntityId; var playerFact = host.PresentedEntities.CurrentGeneration.Entities .Single(entity => entity.EntityId == playerId); + var targetEntityId = host.PresentedEntities.CurrentGeneration.Entities + .Where(entity => entity.EntityId != playerId) + .Select(entity => entity.EntityId) + .FirstOrDefault(); + Assert.That(targetEntityId, Is.Not.Null.And.Not.Empty, + "The advertised live generation has no explicit targeting candidate."); var playerPrefab = provider.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( playerFact.AssetRef, playerFact.EntityKind, @@ -285,7 +291,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests movementReceipt = WitnessReceipt.From("movement", runtime.LastReceipt); var movementVersion = runtime.ActiveVersion; - var focus = runtime.SubmitFocusIntent(playerId); + var focus = runtime.SubmitTargetIntent(playerId, targetEntityId); var focusDeadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < focusDeadline && (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != focus.CommandId || diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index d5dc99b..31c58b6 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using GameCult.Caching; using GameCult.Caching.MessagePack; using GameCult.Eve.Surface; @@ -175,12 +176,16 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Path.Combine(Application.temporaryCachePath, $"eve-unity-{Guid.NewGuid():N}.cc"); var capturePath = Environment.GetEnvironmentVariable("EVEUNITY_AETHERIA_CAPTURE_PATH") ?? Path.Combine(Application.temporaryCachePath, "aetheria-daemon-world.png"); + var mapCapturePath = Environment.GetEnvironmentVariable("EVEUNITY_AETHERIA_MAP_CAPTURE_PATH") ?? + Path.Combine(Application.temporaryCachePath, "aetheria-daemon-map.png"); var root = new GameObject("Generic Eve World Projection"); var cameraObject = new GameObject("Generic Eve Capture Camera"); + var mapCameraObject = new GameObject("Generic Eve Map Camera"); var lightObject = new GameObject("Generic Eve World Light"); RenderTexture target = null; Texture2D pixels = null; EveUnityCultMeshPlayableWorldProvider provider = null; + EveUnityPlayableWorldClientHost host = null; EveUnityPlayableWorldRuntime runtime = null; WitnessReceipt movementReceipt = null; WitnessReceipt focusReceipt = null; @@ -217,18 +222,44 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(provider.Selection.VerseId, Is.Not.Empty); Assert.That(provider.Selection.ProviderId, Is.Not.Empty); Assert.That(provider.Selection.SurfaceId, Is.Not.Empty); - runtime = EveUnityPlayableWorldRuntime.CreateForGameObjectScene( + host = root.AddComponent(); + host.Configure( root.transform, provider, provider, - null, + provider, provider, provider); - var initial = runtime.Connect(); - Assert.That(initial.ActiveEntities, Is.GreaterThan(0)); + host.Connect(); + runtime = host.Runtime; + Assert.That(runtime, Is.Not.Null); Assert.That(runtime.ActiveWorld.PlayerEntityId, Is.Not.Empty); + var entityDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < entityDeadline && + (host.PresentedEntities?.CurrentGeneration == null || + host.PresentedEntities.CurrentGeneration.Entities.Count == 0)) + { + yield return new WaitForSecondsRealtime(0.1f); + host.Refresh(); + } + Assert.That(host.PresentedEntities?.CurrentGeneration, Is.Not.Null); + Assert.That(host.PresentedEntities.CurrentGeneration.Entities.Count, Is.GreaterThan(0)); var playerId = runtime.ActiveWorld.PlayerEntityId; + var playerFact = host.PresentedEntities.CurrentGeneration.Entities + .Single(entity => entity.EntityId == playerId); + var targetEntityId = host.PresentedEntities.CurrentGeneration.Entities + .Where(entity => entity.EntityId != playerId) + .Select(entity => entity.EntityId) + .FirstOrDefault(); + Assert.That(targetEntityId, Is.Not.Null.And.Not.Empty, + "The advertised live generation has no explicit targeting candidate."); + var playerPrefab = provider.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( + playerFact.AssetRef, + playerFact.EntityKind, + "provider-asset-ref")); + Assert.That(playerPrefab, Is.Not.Null, + $"Provider asset catalog did not resolve player AssetRef '{playerFact.AssetRef}'."); var marker = FindMarker(root, playerId); Assert.That( marker.GetComponentsInChildren(includeInactive: true).Length, @@ -260,7 +291,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests movementReceipt = WitnessReceipt.From("movement", runtime.LastReceipt); var movementVersion = runtime.ActiveVersion; - var focus = runtime.SubmitFocusIntent(playerId); + var focus = runtime.SubmitTargetIntent(playerId, targetEntityId); var focusDeadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < focusDeadline && (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != focus.CommandId || @@ -302,6 +333,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var camera = cameraObject.AddComponent(); camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(0.035f, 0.055f, 0.08f, 1f); + camera.cullingMask = -1; foreach (var entityMarker in root.GetComponentsInChildren()) { foreach (var renderer in entityMarker.GetComponentsInChildren()) @@ -316,15 +348,25 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var playerMarker = FindMarker(root, playerId); var playerRenderers = playerMarker.GetComponentsInChildren(); Assert.That(playerRenderers, Is.Not.Empty, "The provider-authored player prefab has no renderable visual."); - var playerBounds = playerRenderers[0].bounds; - foreach (var renderer in playerRenderers) - playerBounds.Encapsulate(renderer.bounds); - var halfFovRadians = camera.fieldOfView * 0.5f * Mathf.Deg2Rad; - var cameraDistance = Mathf.Max(8f, playerBounds.extents.magnitude / Mathf.Tan(halfFovRadians) * 1.35f); - camera.nearClipPlane = Mathf.Max(0.01f, cameraDistance * 0.001f); - camera.farClipPlane = Mathf.Max(1000f, cameraDistance + playerBounds.size.magnitude * 4f); - camera.transform.position = playerBounds.center + new Vector3(1f, 0.65f, -1.2f).normalized * cameraDistance; - camera.transform.LookAt(playerBounds.center); + Assert.That(provider.TryGetRenderChannelLayer("map", out var mapLayer), Is.True); + var mapRenderers = new List(); + foreach (var renderer in root.GetComponentsInChildren(includeInactive: true)) + { + if (renderer.gameObject.layer == mapLayer) + mapRenderers.Add(renderer); + } + Assert.That(mapRenderers, Is.Not.Empty, "The live provider prefab has no renderer assigned to its advertised map channel."); + + var cameraRig = cameraObject.AddComponent(); + cameraRig.Host = host; + cameraRig.CameraTransform = camera.transform; + cameraRig.RenderPolicySource = provider; + Assert.That(cameraRig.ApplyRig(0f), Is.True); + Assert.That(camera.cullingMask & (1 << mapLayer), Is.Zero); + + var mapCamera = mapCameraObject.AddComponent(); + mapCamera.cullingMask = 1 << mapLayer; + Assert.That(mapCamera.cullingMask & (1 << mapLayer), Is.Not.Zero); target = new RenderTexture(640, 360, 24, RenderTextureFormat.ARGB32); camera.targetTexture = target; yield return null; @@ -336,10 +378,36 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(capturePath))); File.WriteAllBytes(capturePath, pixels.EncodeToPNG()); Assert.That(new FileInfo(capturePath).Length, Is.GreaterThan(1024)); + + var mapBounds = mapRenderers[0].bounds; + foreach (var renderer in mapRenderers) + mapBounds.Encapsulate(renderer.bounds); + mapCamera.clearFlags = CameraClearFlags.SolidColor; + mapCamera.backgroundColor = camera.backgroundColor; + mapCamera.orthographic = true; + mapCamera.orthographicSize = Mathf.Max(0.1f, mapBounds.extents.x, mapBounds.extents.z) * 1.25f; + mapCamera.nearClipPlane = 0.01f; + mapCamera.farClipPlane = Mathf.Max(10f, mapBounds.size.magnitude * 4f); + mapCamera.transform.position = mapBounds.center + Vector3.up * (mapCamera.farClipPlane * 0.25f); + mapCamera.transform.rotation = Quaternion.Euler(90f, 0f, 0f); + mapCamera.targetTexture = target; + mapCamera.Render(); + RenderTexture.active = target; + pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); + pixels.Apply(); + var mapChangedPixels = CountChangedPixels(pixels, mapCamera.backgroundColor); + Assert.That(mapChangedPixels, Is.GreaterThan(25), "The map-only camera rendered no provider-authored map glyph pixels."); + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(mapCapturePath))); + File.WriteAllBytes(mapCapturePath, pixels.EncodeToPNG()); + Assert.That(new FileInfo(mapCapturePath).Length, Is.GreaterThan(1024)); WriteWitnessFacts( initialVersion, runtime.ActiveVersion, movementDistance, + mapRenderers.Count, + mapChangedPixels, + (camera.cullingMask & (1 << mapLayer)) == 0, + (mapCamera.cullingMask & (1 << mapLayer)) != 0, movementReceipt, focusReceipt, actionReceipt); @@ -349,10 +417,13 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests RenderTexture.active = null; var camera = cameraObject.GetComponent(); if (camera != null) camera.targetTexture = null; + var mapCamera = mapCameraObject.GetComponent(); + if (mapCamera != null) mapCamera.targetTexture = null; if (target != null) UnityEngine.Object.DestroyImmediate(target); if (pixels != null) UnityEngine.Object.DestroyImmediate(pixels); - runtime?.Dispose(); + host?.Disconnect(); UnityEngine.Object.DestroyImmediate(lightObject); + UnityEngine.Object.DestroyImmediate(mapCameraObject); UnityEngine.Object.DestroyImmediate(cameraObject); UnityEngine.Object.DestroyImmediate(root); } @@ -362,6 +433,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests long initialVersion, long finalVersion, float movementDistance, + int mapChannelRendererCount, + int mapChangedPixels, + bool pilotCameraExcludesMapChannel, + bool mapCameraIncludesMapChannel, WitnessReceipt movement, WitnessReceipt targeting, WitnessReceipt action) @@ -378,6 +453,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests initialVersion = initialVersion, finalVersion = finalVersion, movementDistance = movementDistance, + mapChannelRendererCount = mapChannelRendererCount, + mapChangedPixels = mapChangedPixels, + pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, + mapCameraIncludesMapChannel = mapCameraIncludesMapChannel, receipts = new[] { movement, targeting, action } }; Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); @@ -395,6 +474,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public long initialVersion; public long finalVersion; public float movementDistance; + public int mapChannelRendererCount; + public int mapChangedPixels; + public bool pilotCameraExcludesMapChannel; + public bool mapCameraIncludesMapChannel; public WitnessReceipt[] receipts; } @@ -428,6 +511,19 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests throw new InvalidOperationException($"Projected world has no entity marker '{entityId}'."); } + private static int CountChangedPixels(Texture2D texture, Color background) + { + var changed = 0; + foreach (var pixel in texture.GetPixels()) + { + if (Mathf.Abs(pixel.r - background.r) + + Mathf.Abs(pixel.g - background.g) + + Mathf.Abs(pixel.b - background.b) > 0.08f) + changed++; + } + return changed; + } + private static EveUnitySceneProviderSurfaceDocument WorldDocument() { var entities = new[] diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 83dadd9..f520e89 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -1,11 +1,11 @@ # Aetheria released-package witness -The warm-cache witness passes against `org.gamecult.eve.unity-scene` `0.3.17` -at commit `979eee3f8e327c904d25d2d3091a9a6db977842d` and -`org.gamecult.cultlib` `1.0.12` at commit -`26b4d1eadc4d0b1fe7a5ffb29a64373576544447`. +The warm-cache witness passes against `org.gamecult.eve.unity-scene` `0.3.18` +at commit `ed21479e467faea8ff624f2d2f5a7d2fecf913f4` and +`org.gamecult.cultlib` `1.0.13` at commit +`feb5c71513e71d681699f462fe3682b3168c6f73`. -Evidence is in `artifacts/render-channel-witness-content-warm`: +Evidence is in `artifacts/render-channel-witness-content-drain-cold-2`: - `results.xml`: one passing PlayMode witness. - `aetheria-daemon-world.png`: provider-authored 3D pilot view. @@ -13,14 +13,29 @@ Evidence is in `artifacts/render-channel-witness-content-warm`: - `witness-facts.json`: movement, targeting, action, receipts, and camera-mask facts. - `runtime-witness.warm.json`: released package commits and screenshot hashes. -The warm cache was seeded from the previously hash-verified bundle and renamed -to CultMesh's content-addressed `.body` cache shape. This proves released-client -loading and lowering, not cold delivery. +The warm run reused the body promoted by the preceding cold content transfer. +It reconciled movement, explicit targeting, and action receipts. The pilot +camera excluded all seven renderers assigned to the provider's `map` channel; +the map-only camera rendered 2,664 changed glyph pixels. Visual inspection +confirms provider-authored ships in the pilot view without map glyphs, and only +the colored provider-authored glyphs in the map view. -The cold CDN path remains failed. The 2026-07-14 run in -`artifacts/render-channel-witness-content-cold` used the managed -`cultmesh.content.v1` session and created the resumable 13,006,384-byte partial -body, but transferred only about 5.3 MB before the fixed 300-second witness -deadline. Snapshot chunk batching is no longer involved. The remaining fault is -the sequential one-chunk request path over RUDP; increasing the witness timeout -is not an accepted fix. +Cold content delivery now succeeds through the managed `cultmesh.content.v1` +session under the unchanged 300-second deadline. The run in +`artifacts/render-channel-witness-content-drain-cold-3` began with an empty +asset cache and atomically promoted: + +- path: `asset-cache/0d5b575a18ecb2e8f4f1c309a7fa009262bdbb2ff671c14a4bf610d5052685dd.body` +- bytes: `13,006,384` +- SHA-256: `0d5b575a18ecb2e8f4f1c309a7fa009262bdbb2ff671c14a4bf610d5052685dd` +- partial files after completion: zero + +The full cold gameplay witness is not yet a pass. During the roughly 19 seconds +of authoritative simulation needed to deliver and load the body, the witness +world can advance until its initial non-player targeting candidates are gone. +The released client then correctly reports a generation containing no explicit +target and the witness stops before screenshots. The warm run proves the +generic gameplay and camera contracts; the cold run proves content delivery. +Closing the combined cold proof requires an explicit readiness/lifecycle +contract or a stable witness-world scenario. A renderer fallback or a larger +timeout would conceal the ownership problem and is not accepted. From c73143029788abdf61139fc897bced7dfaf0a768 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 14 Jul 2026 16:26:38 +0100 Subject: [PATCH 038/292] Harden released world witness evidence --- .../PlayMode/GenericWorldCaptureTests.cs | 87 ++++++++++++++----- scripts/run-aetheria-daemon-world-witness.ps1 | 46 +++++++++- 2 files changed, 111 insertions(+), 22 deletions(-) diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 31c58b6..344a044 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -271,21 +271,23 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var deadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < deadline && - (runtime.LastReceipt == null || runtime.ActiveVersion <= initialVersion || + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != request.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || + runtime.ActiveVersion <= initialVersion || Vector3.Distance(FindMarker(root, playerId).transform.position, initialPosition) < 0.01f)) { yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); } - Assert.That(runtime.LastReceipt, Is.Not.Null); - Assert.That(runtime.LastReceipt.CommandId, Is.EqualTo(request.CommandId)); - Assert.That( - runtime.LastReceipt.State, - Is.EqualTo("accepted").Or.EqualTo("reconciled"), - runtime.LastReceipt.Message); - Assert.That(runtime.LastReceipt.SourceVersion, Is.GreaterThan(initialVersion)); + AssertReconciledProviderReceipt( + runtime.LastReceipt, + request.CommandId, + provider.Selection.ProviderId, + provider.Selection.SurfaceId, + initialVersion); Assert.That(runtime.ActiveVersion, Is.GreaterThan(initialVersion)); + Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); movementDistance = Vector3.Distance(FindMarker(root, playerId).transform.position, initialPosition); Assert.That(movementDistance, Is.GreaterThan(0.01f)); movementReceipt = WitnessReceipt.From("movement", runtime.LastReceipt); @@ -295,15 +297,19 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var focusDeadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < focusDeadline && (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != focus.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || runtime.ActiveVersion < runtime.LastReceipt.SourceVersion)) { yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); } - Assert.That(runtime.LastReceipt, Is.Not.Null); - Assert.That(runtime.LastReceipt.CommandId, Is.EqualTo(focus.CommandId)); - Assert.That(runtime.LastReceipt.State, Is.EqualTo("accepted").Or.EqualTo("reconciled")); - Assert.That(runtime.LastReceipt.SourceVersion, Is.GreaterThan(movementVersion)); + AssertReconciledProviderReceipt( + runtime.LastReceipt, + focus.CommandId, + provider.Selection.ProviderId, + provider.Selection.SurfaceId, + movementVersion); + Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); focusReceipt = WitnessReceipt.From("targeting", runtime.LastReceipt); var focusVersion = runtime.ActiveVersion; @@ -311,18 +317,18 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var actionDeadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < actionDeadline && (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != action.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || runtime.ActiveVersion < runtime.LastReceipt.SourceVersion)) { yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); } - Assert.That(runtime.LastReceipt, Is.Not.Null); - Assert.That(runtime.LastReceipt.CommandId, Is.EqualTo(action.CommandId)); - Assert.That( - runtime.LastReceipt.State, - Is.EqualTo("accepted").Or.EqualTo("reconciled"), - runtime.LastReceipt.Message); - Assert.That(runtime.LastReceipt.SourceVersion, Is.GreaterThan(focusVersion)); + AssertReconciledProviderReceipt( + runtime.LastReceipt, + action.CommandId, + provider.Selection.ProviderId, + provider.Selection.SurfaceId, + focusVersion); Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); actionReceipt = WitnessReceipt.From("action", runtime.LastReceipt); @@ -366,7 +372,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var mapCamera = mapCameraObject.AddComponent(); mapCamera.cullingMask = 1 << mapLayer; - Assert.That(mapCamera.cullingMask & (1 << mapLayer), Is.Not.Zero); + Assert.That(mapCamera.cullingMask, Is.EqualTo(1 << mapLayer)); target = new RenderTexture(640, 360, 24, RenderTextureFormat.ARGB32); camera.targetTexture = target; yield return null; @@ -375,6 +381,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pixels = new Texture2D(640, 360, TextureFormat.RGB24, false); pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); pixels.Apply(); + var pilotChangedPixels = CountChangedPixels(pixels, camera.backgroundColor); + Assert.That(pilotChangedPixels, Is.GreaterThan(25), "The pilot camera rendered no provider-authored world pixels."); Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(capturePath))); File.WriteAllBytes(capturePath, pixels.EncodeToPNG()); Assert.That(new FileInfo(capturePath).Length, Is.GreaterThan(1024)); @@ -404,6 +412,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests initialVersion, runtime.ActiveVersion, movementDistance, + playerRenderers.Length, + pilotChangedPixels, mapRenderers.Count, mapChangedPixels, (camera.cullingMask & (1 << mapLayer)) == 0, @@ -433,6 +443,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests long initialVersion, long finalVersion, float movementDistance, + int playerRendererCount, + int pilotChangedPixels, int mapChannelRendererCount, int mapChangedPixels, bool pilotCameraExcludesMapChannel, @@ -453,6 +465,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests initialVersion = initialVersion, finalVersion = finalVersion, movementDistance = movementDistance, + playerRendererCount = playerRendererCount, + pilotChangedPixels = pilotChangedPixels, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, @@ -474,6 +488,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public long initialVersion; public long finalVersion; public float movementDistance; + public int playerRendererCount; + public int pilotChangedPixels; public int mapChannelRendererCount; public int mapChangedPixels; public bool pilotCameraExcludesMapChannel; @@ -487,6 +503,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public string commandKind; public string commandId; public string state; + public string receiptId; + public string ownerRepo; + public string authority; + public string providerId; + public string surfaceId; + public bool isProviderOwned; public long sourceVersion; public static WitnessReceipt From(string commandKind, EveUnitySceneCommandReceipt receipt) @@ -496,11 +518,36 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests commandKind = commandKind, commandId = receipt.CommandId, state = receipt.State, + receiptId = receipt.ReceiptId, + ownerRepo = receipt.OwnerRepo, + authority = receipt.Authority, + providerId = receipt.ProviderId, + surfaceId = receipt.SurfaceId, + isProviderOwned = receipt.IsProviderOwned, sourceVersion = receipt.SourceVersion }; } } + private static void AssertReconciledProviderReceipt( + EveUnitySceneCommandReceipt receipt, + string commandId, + string providerId, + string surfaceId, + long minimumSourceVersion) + { + Assert.That(receipt, Is.Not.Null); + Assert.That(receipt.CommandId, Is.EqualTo(commandId)); + Assert.That(receipt.State, Is.EqualTo("reconciled"), receipt.Message); + Assert.That(receipt.IsProviderOwned, Is.True); + Assert.That(receipt.ProviderId, Is.EqualTo(providerId)); + Assert.That(receipt.SurfaceId, Is.EqualTo(surfaceId)); + Assert.That(receipt.ReceiptId, Is.Not.Empty); + Assert.That(receipt.OwnerRepo, Is.Not.Empty); + Assert.That(receipt.Authority, Is.Not.Empty); + Assert.That(receipt.SourceVersion, Is.GreaterThan(minimumSourceVersion)); + } + private static EveUnityPlayableWorldEntityMarker FindMarker(GameObject root, string entityId) { foreach (var marker in root.GetComponentsInChildren()) diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 16e2629..c549801 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -12,6 +12,8 @@ param( ) $ErrorActionPreference = "Stop" +$expectedEveUnityCommit = "ed21479e467faea8ff624f2d2f5a7d2fecf913f4" +$expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot $projectRoot = $ClientProject $outputRoot = if ([IO.Path]::IsPathRooted($OutputDirectory)) { $OutputDirectory } else { Join-Path $repoRoot $OutputDirectory } @@ -34,9 +36,17 @@ foreach ($required in @($UnityExe, (Join-Path $repoRoot $projectRoot), $daemonPr } New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null +foreach ($priorArtifact in @($resultsPath, $capturePath, $mapCapturePath, $factsPath, $witnessPath)) { + if (Test-Path -LiteralPath $priorArtifact) { Remove-Item -LiteralPath $priorArtifact -Force } +} if ($CacheState -eq "cold" -and (Test-Path -LiteralPath $assetCachePath)) { Remove-Item -LiteralPath $assetCachePath -Recurse -Force } +$initialBodyCount = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.body -File -Recurse -ErrorAction SilentlyContinue).Count +$initialPartialCount = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.partial -File -Recurse -ErrorAction SilentlyContinue).Count +if ($CacheState -eq "cold" -and ($initialBodyCount -ne 0 -or $initialPartialCount -ne 0)) { + throw "Cold witness cache was not empty before Unity launch. bodies=$initialBodyCount partials=$initialPartialCount" +} $cacheWasWarm = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.body -File -ErrorAction SilentlyContinue).Count -gt 0 if ($CacheState -eq "warm" -and -not $cacheWasWarm) { throw "Warm witness requested without an existing verified bundle cache at $assetCachePath" @@ -155,13 +165,45 @@ try { if (-not (Test-Path $capturePath) -or (Get-Item $capturePath).Length -lt 1024) { throw "Live world capture is missing: $capturePath" } if (-not (Test-Path $mapCapturePath) -or (Get-Item $mapCapturePath).Length -lt 1024) { throw "Live map-channel capture is missing: $mapCapturePath" } if (-not (Test-Path $factsPath)) { throw "Live world witness facts are missing: $factsPath" } + foreach ($freshArtifact in @($resultsPath, $capturePath, $mapCapturePath, $factsPath)) { + if ((Get-Item -LiteralPath $freshArtifact).LastWriteTimeUtc -lt $witnessStartedAt.UtcDateTime) { + throw "Live witness artifact predates this run: $freshArtifact" + } + } $facts = Get-Content -LiteralPath $factsPath -Raw | ConvertFrom-Json $releaseLock = Get-Content -LiteralPath (Join-Path $repoRoot "ReleaseConsumerProject\Packages\packages-lock.json") -Raw | ConvertFrom-Json $releasedPackageClient = $projectRoot -eq "ReleaseConsumerProject" + $sceneLock = $releaseLock.dependencies.'org.gamecult.eve.unity-scene' + $cultLibLock = $releaseLock.dependencies.'org.gamecult.cultlib' + if (-not $releasedPackageClient) { throw "Released witness must run ReleaseConsumerProject, got '$projectRoot'." } + if ($sceneLock.source -ne "git" -or $cultLibLock.source -ne "git" -or + $sceneLock.version -like "file:*" -or $cultLibLock.version -like "file:*") { + throw "Released witness resolved a non-git/local package dependency." + } + if ($sceneLock.hash -ne $expectedEveUnityCommit -or $cultLibLock.hash -ne $expectedCultLibCommit) { + throw "Released package commits do not match the witnessed releases. scene=$($sceneLock.hash) cultlib=$($cultLibLock.hash)" + } + $finalBodies = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.body -File -Recurse -ErrorAction SilentlyContinue) + $finalPartials = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.partial -File -Recurse -ErrorAction SilentlyContinue) + if ($finalBodies.Count -ne 1 -or $finalPartials.Count -ne 0) { + throw "Content promotion is incomplete. bodies=$($finalBodies.Count) partials=$($finalPartials.Count)" + } + $bodyHash = (Get-FileHash -LiteralPath $finalBodies[0].FullName -Algorithm SHA256).Hash.ToLowerInvariant() + if ($finalBodies[0].BaseName -ne $bodyHash) { + throw "Promoted content body name does not match its SHA-256. name=$($finalBodies[0].BaseName) hash=$bodyHash" + } $facts | Add-Member -NotePropertyName releasedPackageClient -NotePropertyValue $releasedPackageClient $facts | Add-Member -NotePropertyName clientProject -NotePropertyValue $projectRoot - $facts | Add-Member -NotePropertyName eveUnityPackageCommit -NotePropertyValue $releaseLock.dependencies.'org.gamecult.eve.unity-scene'.hash - $facts | Add-Member -NotePropertyName cultLibPackageCommit -NotePropertyValue $releaseLock.dependencies.'org.gamecult.cultlib'.hash + $facts | Add-Member -NotePropertyName eveUnityPackageCommit -NotePropertyValue $sceneLock.hash + $facts | Add-Member -NotePropertyName cultLibPackageCommit -NotePropertyValue $cultLibLock.hash + $facts | Add-Member -NotePropertyName contentDelivery -NotePropertyValue ([ordered]@{ + initialBodyCount = $initialBodyCount + initialPartialCount = $initialPartialCount + finalBodyCount = $finalBodies.Count + finalPartialCount = $finalPartials.Count + contentHash = $bodyHash + sizeBytes = $finalBodies[0].Length + }) $capture = Get-Item -LiteralPath $capturePath $mapCapture = Get-Item -LiteralPath $mapCapturePath $resultsArtifact = Get-Item -LiteralPath $resultsPath From c6476c76d465bdfd9d1642d6454c4b727c4b1e34 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 14 Jul 2026 16:39:56 +0100 Subject: [PATCH 039/292] Keep released witness assertions in sync --- .../PlayMode/GenericWorldCaptureTests.cs | 87 ++++++++++++++----- scripts/run-aetheria-daemon-world-witness.ps1 | 6 ++ 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 31c58b6..344a044 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -271,21 +271,23 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var deadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < deadline && - (runtime.LastReceipt == null || runtime.ActiveVersion <= initialVersion || + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != request.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || + runtime.ActiveVersion <= initialVersion || Vector3.Distance(FindMarker(root, playerId).transform.position, initialPosition) < 0.01f)) { yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); } - Assert.That(runtime.LastReceipt, Is.Not.Null); - Assert.That(runtime.LastReceipt.CommandId, Is.EqualTo(request.CommandId)); - Assert.That( - runtime.LastReceipt.State, - Is.EqualTo("accepted").Or.EqualTo("reconciled"), - runtime.LastReceipt.Message); - Assert.That(runtime.LastReceipt.SourceVersion, Is.GreaterThan(initialVersion)); + AssertReconciledProviderReceipt( + runtime.LastReceipt, + request.CommandId, + provider.Selection.ProviderId, + provider.Selection.SurfaceId, + initialVersion); Assert.That(runtime.ActiveVersion, Is.GreaterThan(initialVersion)); + Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); movementDistance = Vector3.Distance(FindMarker(root, playerId).transform.position, initialPosition); Assert.That(movementDistance, Is.GreaterThan(0.01f)); movementReceipt = WitnessReceipt.From("movement", runtime.LastReceipt); @@ -295,15 +297,19 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var focusDeadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < focusDeadline && (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != focus.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || runtime.ActiveVersion < runtime.LastReceipt.SourceVersion)) { yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); } - Assert.That(runtime.LastReceipt, Is.Not.Null); - Assert.That(runtime.LastReceipt.CommandId, Is.EqualTo(focus.CommandId)); - Assert.That(runtime.LastReceipt.State, Is.EqualTo("accepted").Or.EqualTo("reconciled")); - Assert.That(runtime.LastReceipt.SourceVersion, Is.GreaterThan(movementVersion)); + AssertReconciledProviderReceipt( + runtime.LastReceipt, + focus.CommandId, + provider.Selection.ProviderId, + provider.Selection.SurfaceId, + movementVersion); + Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); focusReceipt = WitnessReceipt.From("targeting", runtime.LastReceipt); var focusVersion = runtime.ActiveVersion; @@ -311,18 +317,18 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var actionDeadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < actionDeadline && (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != action.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || runtime.ActiveVersion < runtime.LastReceipt.SourceVersion)) { yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); } - Assert.That(runtime.LastReceipt, Is.Not.Null); - Assert.That(runtime.LastReceipt.CommandId, Is.EqualTo(action.CommandId)); - Assert.That( - runtime.LastReceipt.State, - Is.EqualTo("accepted").Or.EqualTo("reconciled"), - runtime.LastReceipt.Message); - Assert.That(runtime.LastReceipt.SourceVersion, Is.GreaterThan(focusVersion)); + AssertReconciledProviderReceipt( + runtime.LastReceipt, + action.CommandId, + provider.Selection.ProviderId, + provider.Selection.SurfaceId, + focusVersion); Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); actionReceipt = WitnessReceipt.From("action", runtime.LastReceipt); @@ -366,7 +372,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var mapCamera = mapCameraObject.AddComponent(); mapCamera.cullingMask = 1 << mapLayer; - Assert.That(mapCamera.cullingMask & (1 << mapLayer), Is.Not.Zero); + Assert.That(mapCamera.cullingMask, Is.EqualTo(1 << mapLayer)); target = new RenderTexture(640, 360, 24, RenderTextureFormat.ARGB32); camera.targetTexture = target; yield return null; @@ -375,6 +381,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pixels = new Texture2D(640, 360, TextureFormat.RGB24, false); pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); pixels.Apply(); + var pilotChangedPixels = CountChangedPixels(pixels, camera.backgroundColor); + Assert.That(pilotChangedPixels, Is.GreaterThan(25), "The pilot camera rendered no provider-authored world pixels."); Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(capturePath))); File.WriteAllBytes(capturePath, pixels.EncodeToPNG()); Assert.That(new FileInfo(capturePath).Length, Is.GreaterThan(1024)); @@ -404,6 +412,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests initialVersion, runtime.ActiveVersion, movementDistance, + playerRenderers.Length, + pilotChangedPixels, mapRenderers.Count, mapChangedPixels, (camera.cullingMask & (1 << mapLayer)) == 0, @@ -433,6 +443,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests long initialVersion, long finalVersion, float movementDistance, + int playerRendererCount, + int pilotChangedPixels, int mapChannelRendererCount, int mapChangedPixels, bool pilotCameraExcludesMapChannel, @@ -453,6 +465,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests initialVersion = initialVersion, finalVersion = finalVersion, movementDistance = movementDistance, + playerRendererCount = playerRendererCount, + pilotChangedPixels = pilotChangedPixels, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, @@ -474,6 +488,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public long initialVersion; public long finalVersion; public float movementDistance; + public int playerRendererCount; + public int pilotChangedPixels; public int mapChannelRendererCount; public int mapChangedPixels; public bool pilotCameraExcludesMapChannel; @@ -487,6 +503,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public string commandKind; public string commandId; public string state; + public string receiptId; + public string ownerRepo; + public string authority; + public string providerId; + public string surfaceId; + public bool isProviderOwned; public long sourceVersion; public static WitnessReceipt From(string commandKind, EveUnitySceneCommandReceipt receipt) @@ -496,11 +518,36 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests commandKind = commandKind, commandId = receipt.CommandId, state = receipt.State, + receiptId = receipt.ReceiptId, + ownerRepo = receipt.OwnerRepo, + authority = receipt.Authority, + providerId = receipt.ProviderId, + surfaceId = receipt.SurfaceId, + isProviderOwned = receipt.IsProviderOwned, sourceVersion = receipt.SourceVersion }; } } + private static void AssertReconciledProviderReceipt( + EveUnitySceneCommandReceipt receipt, + string commandId, + string providerId, + string surfaceId, + long minimumSourceVersion) + { + Assert.That(receipt, Is.Not.Null); + Assert.That(receipt.CommandId, Is.EqualTo(commandId)); + Assert.That(receipt.State, Is.EqualTo("reconciled"), receipt.Message); + Assert.That(receipt.IsProviderOwned, Is.True); + Assert.That(receipt.ProviderId, Is.EqualTo(providerId)); + Assert.That(receipt.SurfaceId, Is.EqualTo(surfaceId)); + Assert.That(receipt.ReceiptId, Is.Not.Empty); + Assert.That(receipt.OwnerRepo, Is.Not.Empty); + Assert.That(receipt.Authority, Is.Not.Empty); + Assert.That(receipt.SourceVersion, Is.GreaterThan(minimumSourceVersion)); + } + private static EveUnityPlayableWorldEntityMarker FindMarker(GameObject root, string entityId) { foreach (var marker in root.GetComponentsInChildren()) diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index c549801..2fe6b32 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -34,6 +34,12 @@ $importProject = Join-Path $AetheriaRoot "Aetheria.State.Import\Aetheria.State.I foreach ($required in @($UnityExe, (Join-Path $repoRoot $projectRoot), $daemonProject, $importProject)) { if (-not (Test-Path -LiteralPath $required)) { throw "Required world witness path not found: $required" } } +$canonicalWitnessTest = Join-Path $repoRoot "TestProject\Assets\Tests\PlayMode\GenericWorldCaptureTests.cs" +$releaseWitnessTest = Join-Path $repoRoot "ReleaseConsumerProject\Assets\Tests\PlayMode\GenericWorldCaptureTests.cs" +if ((Get-FileHash -LiteralPath $canonicalWitnessTest -Algorithm SHA256).Hash -ne + (Get-FileHash -LiteralPath $releaseWitnessTest -Algorithm SHA256).Hash) { + throw "Released consumer witness drifted from the canonical generic-client test." +} New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null foreach ($priorArtifact in @($resultsPath, $capturePath, $mapCapturePath, $factsPath, $witnessPath)) { From f0d25c57166004a6efc1f66c300bbd9271c2270e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 14 Jul 2026 16:46:42 +0100 Subject: [PATCH 040/292] Record combined cold released witness --- docs/aetheria-released-package-witness.md | 70 +++++++++++++---------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index f520e89..dbb9fd3 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -1,41 +1,49 @@ # Aetheria released-package witness -The warm-cache witness passes against `org.gamecult.eve.unity-scene` `0.3.18` -at commit `ed21479e467faea8ff624f2d2f5a7d2fecf913f4` and -`org.gamecult.cultlib` `1.0.13` at commit -`feb5c71513e71d681699f462fe3682b3168c6f73`. +The combined cold-cache witness passes against the released generic runtime: -Evidence is in `artifacts/render-channel-witness-content-drain-cold-2`: +- `org.gamecult.eve.unity-scene` `0.3.18`, commit + `ed21479e467faea8ff624f2d2f5a7d2fecf913f4` +- `org.gamecult.cultlib` `1.0.13`, commit + `feb5c71513e71d681699f462fe3682b3168c6f73` -- `results.xml`: one passing PlayMode witness. -- `aetheria-daemon-world.png`: provider-authored 3D pilot view. -- `aetheria-daemon-map.png`: map-channel-only view. -- `witness-facts.json`: movement, targeting, action, receipts, and camera-mask facts. -- `runtime-witness.warm.json`: released package commits and screenshot hashes. +Evidence is in `artifacts/render-channel-witness-gravity-cold-4`. The run used +`ReleaseConsumerProject`, resolved both dependencies from tagged Git URLs, and +finished under the unchanged 300-second Unity limit. -The warm run reused the body promoted by the preceding cold content transfer. -It reconciled movement, explicit targeting, and action receipts. The pilot -camera excluded all seven renderers assigned to the provider's `map` channel; -the map-only camera rendered 2,664 changed glyph pixels. Visual inspection -confirms provider-authored ships in the pilot view without map glyphs, and only -the colored provider-authored glyphs in the map view. - -Cold content delivery now succeeds through the managed `cultmesh.content.v1` -session under the unchanged 300-second deadline. The run in -`artifacts/render-channel-witness-content-drain-cold-3` began with an empty -asset cache and atomically promoted: +The cache began with zero `.body` and `.partial` files. Managed +`cultmesh.content.v1` delivery atomically promoted exactly one body: - path: `asset-cache/0d5b575a18ecb2e8f4f1c309a7fa009262bdbb2ff671c14a4bf610d5052685dd.body` - bytes: `13,006,384` - SHA-256: `0d5b575a18ecb2e8f4f1c309a7fa009262bdbb2ff671c14a4bf610d5052685dd` -- partial files after completion: zero +- final partial files: zero -The full cold gameplay witness is not yet a pass. During the roughly 19 seconds -of authoritative simulation needed to deliver and load the body, the witness -world can advance until its initial non-player targeting candidates are gone. -The released client then correctly reports a generation containing no explicit -target and the witness stops before screenshots. The warm run proves the -generic gameplay and camera contracts; the cold run proves content delivery. -Closing the combined cold proof requires an explicit readiness/lifecycle -contract or a stable witness-world scenario. A renderer fallback or a larger -timeout would conceal the ownership problem and is not accepted. +Snapshot delivery indexes the artifact manifest only. Aetheria's legacy +snapshot `asset_blob` schema, byte injection, path resolver, and Electron client +API are deleted; managed content sessions are the sole bundle-body transport. + +The released generic client instantiated the provider-authored prefab rather +than its primitive fallback. Movement, explicit targeting, and action each +reached a provider-owned `reconciled` receipt with matching command, +provider/surface identity, receipt identity, owner, authority, and observed +source version. + +Camera-channel facts: + +- provider-authored player renderers: `12` +- pilot changed pixels: `6,265` +- map-channel renderers: `11` +- map changed pixels: `3,413` +- pilot camera excludes the advertised map layer +- map camera culls exactly the advertised map layer + +Visual inspection confirms provider-authored ship geometry in the pilot view +without map glyphs, and only provider-authored glyphs in the map view. + +Primary artifacts: + +- `results.xml`: one passing PlayMode witness +- `runtime-witness.cold.json`: package, content, receipt, camera, and hash facts +- `aetheria-daemon-world.png`: fresh pilot-camera capture +- `aetheria-daemon-map.png`: fresh map-only capture From d9b762f429056676877d00955dc617e4d423b429 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 03:21:28 +0100 Subject: [PATCH 041/292] Lower provider-authored combat presentation --- .../PlayMode/GenericWorldCaptureTests.cs | 149 ++++++++-- .../PlayMode/GenericWorldCaptureTests.cs | 149 ++++++++-- .../org.gamecult.eve.unity-scene/README.md | 8 + .../EveUnityCultMeshLiveProviderTransport.cs | 36 +++ .../EveUnityCultMeshPlayableWorldProvider.cs | 12 +- .../Runtime/EveUnityCombatPresentation.cs | 98 +++++++ .../EveUnityCombatPresentation.cs.meta | 2 + .../EveUnityCombatPresentationRenderer.cs | 262 ++++++++++++++++++ ...EveUnityCombatPresentationRenderer.cs.meta | 2 + .../Runtime/EveUnityPlayableWorldCameraRig.cs | 49 +++- .../EveUnityPlayableWorldClientHost.cs | 6 + .../EveUnityPlayableWorldInputDriver.cs | 38 ++- .../Runtime/EveUnityShotReceiptPresenter.cs | 6 + .../Editor/EveUnityCombatPresentationTests.cs | 83 ++++++ .../EveUnityCombatPresentationTests.cs.meta | 2 + .../EveUnitySceneSurfaceLowererTests.cs | 42 +++ .../EveUnityShotReceiptPresenterTests.cs | 6 +- .../eve-runtime-capability.json | 5 +- .../org.gamecult.eve.unity-scene/package.json | 2 +- scripts/run-aetheria-daemon-world-witness.ps1 | 17 +- scripts/run-interactive-aetheria.ps1 | 121 ++++++++ 21 files changed, 1019 insertions(+), 76 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentation.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentation.cs.meta create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs.meta create mode 100644 packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs.meta create mode 100644 scripts/run-interactive-aetheria.ps1 diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 344a044..088d8a4 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -190,6 +190,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests WitnessReceipt movementReceipt = null; WitnessReceipt focusReceipt = null; WitnessReceipt actionReceipt = null; + EveUnityShotReceipt observedShot = null; + EveUnityCombatPresentation observedCombat = null; + float shieldRatioBefore = 0f; + float hullRatioBefore = 0f; long initialVersion = 0; float movementDistance = 0f; @@ -231,6 +235,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests provider, provider); host.Connect(); + host.ShotAvailable += shot => observedShot = shot; runtime = host.Runtime; Assert.That(runtime, Is.Not.Null); Assert.That(runtime.ActiveWorld.PlayerEntityId, Is.Not.Empty); @@ -248,12 +253,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var playerId = runtime.ActiveWorld.PlayerEntityId; var playerFact = host.PresentedEntities.CurrentGeneration.Entities .Single(entity => entity.EntityId == playerId); - var targetEntityId = host.PresentedEntities.CurrentGeneration.Entities - .Where(entity => entity.EntityId != playerId) - .Select(entity => entity.EntityId) - .FirstOrDefault(); - Assert.That(targetEntityId, Is.Not.Null.And.Not.Empty, - "The advertised live generation has no explicit targeting candidate."); var playerPrefab = provider.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( playerFact.AssetRef, playerFact.EntityKind, @@ -292,13 +291,27 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(movementDistance, Is.GreaterThan(0.01f)); movementReceipt = WitnessReceipt.From("movement", runtime.LastReceipt); + var releaseVersion = runtime.ActiveVersion; + var release = runtime.SubmitMoveVectorIntent(playerId, 0f, 0f, 0f); + var releaseDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < releaseDeadline && + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != release.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase))) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + AssertReconciledProviderReceipt(runtime.LastReceipt, release.CommandId, + provider.Selection.ProviderId, provider.Selection.SurfaceId, releaseVersion); + var movementVersion = runtime.ActiveVersion; - var focus = runtime.SubmitTargetIntent(playerId, targetEntityId); + var focus = runtime.SubmitFocusIntent(playerId); var focusDeadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < focusDeadline && (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != focus.CommandId || !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || - runtime.ActiveVersion < runtime.LastReceipt.SourceVersion)) + runtime.ActiveVersion < runtime.LastReceipt.SourceVersion || + string.IsNullOrWhiteSpace(EveUnityCombatPresentation.Find(runtime.ActiveProjection)?.SelectedTargetEntityId))) { yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); @@ -311,12 +324,49 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests movementVersion); Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); focusReceipt = WitnessReceipt.From("targeting", runtime.LastReceipt); + observedCombat = EveUnityCombatPresentation.Find(runtime.ActiveProjection); + Assert.That(observedCombat, Is.Not.Null); + Assert.That(observedCombat.TargetVisible, Is.True); + Assert.That(observedCombat.TargetHostile, Is.True); + var targetEntityId = observedCombat.SelectedTargetEntityId; + var targetEntityIndex = observedCombat.SelectedTargetEntityIndex; + Assert.That(targetEntityId, Is.Not.Empty); + Assert.That(host.PresentedEntities.CurrentGeneration.Entities.Any(entity => entity.EntityId == targetEntityId), Is.True, + "The provider-selected combat target is absent from the advertised SoA generation."); + shieldRatioBefore = observedCombat.ShieldRatio; + hullRatioBefore = observedCombat.HullRatio; + + var targetFact = host.PresentedEntities.CurrentGeneration.Entities + .Single(entity => entity.EntityId == targetEntityId); + playerFact = host.PresentedEntities.CurrentGeneration.Entities + .Single(entity => entity.EntityId == playerId); + var aim = targetFact.Position - playerFact.Position; + aim.y = 0; + Assert.That(aim.sqrMagnitude, Is.GreaterThan(0.0001f)); + aim.Normalize(); + var steerVersion = runtime.ActiveVersion; + var steer = runtime.SubmitMoveVectorIntent(playerId, aim.x, aim.z, 0.25f); + var steerDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < steerDeadline && + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != steer.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase))) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + AssertReconciledProviderReceipt(runtime.LastReceipt, steer.CommandId, + provider.Selection.ProviderId, provider.Selection.SurfaceId, steerVersion); var focusVersion = runtime.ActiveVersion; - var action = runtime.SubmitActionIntent(playerId, "0"); + var inputDriver = root.GetComponent() ?? + root.AddComponent(); + inputDriver.Host = host; + var action = inputDriver.SubmitPrimaryAction(); + Assert.That(action, Is.Not.Null, + "The advertised input capability did not bind a primary action for this generic client."); var actionDeadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < actionDeadline && - (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != action.CommandId || + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != action!.CommandId || !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || runtime.ActiveVersion < runtime.LastReceipt.SourceVersion)) { @@ -325,13 +375,38 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } AssertReconciledProviderReceipt( runtime.LastReceipt, - action.CommandId, + action!.CommandId, provider.Selection.ProviderId, provider.Selection.SurfaceId, focusVersion); Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); actionReceipt = WitnessReceipt.From("action", runtime.LastReceipt); + var shotDeadline = Time.realtimeSinceStartup + 15f; + while (Time.realtimeSinceStartup < shotDeadline && observedShot == null) + { + yield return new WaitForSecondsRealtime(0.05f); + runtime.Refresh(); + } + Assert.That(observedShot, Is.Not.Null, + "A reconciled fire request never reached daemon-owned lock acquisition and shot resolution."); + observedCombat = EveUnityCombatPresentation.Find(runtime.ActiveProjection); + Assert.That(observedShot.TargetEntityIndex, Is.EqualTo(targetEntityIndex)); + Assert.That(observedShot.LockQuality, Is.GreaterThan(0.99)); + var trajectories = root.GetComponent(); + var combatRenderer = root.GetComponent(); + Assert.That(trajectories, Is.Not.Null); + Assert.That(trajectories.ActiveTrajectoryCount, Is.GreaterThan(0)); + Assert.That(combatRenderer, Is.Not.Null); + combatRenderer.RefreshNow(); + if (observedCombat != null && !string.IsNullOrWhiteSpace(observedCombat.SelectedTargetEntityId)) + { + Assert.That(combatRenderer.ReticleVisible, Is.True); + Assert.That(combatRenderer.LockVisible, Is.True); + } + if (observedShot.Hit && (observedShot.AppliedDamage > 0 || observedShot.ShieldAbsorbedDamage > 0)) + Assert.That(combatRenderer.HitMarkerVisible, Is.True); + var light = lightObject.AddComponent(); light.type = LightType.Directional; light.intensity = 1.4f; @@ -340,17 +415,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(0.035f, 0.055f, 0.08f, 1f); camera.cullingMask = -1; - foreach (var entityMarker in root.GetComponentsInChildren()) - { - foreach (var renderer in entityMarker.GetComponentsInChildren()) - { - renderer.material.color = entityMarker.Controllable - ? new Color(0.18f, 0.75f, 1f) - : entityMarker.EntityKind == "enemy" - ? new Color(1f, 0.25f, 0.2f) - : new Color(1f, 0.78f, 0.15f); - } - } var playerMarker = FindMarker(root, playerId); var playerRenderers = playerMarker.GetComponentsInChildren(); Assert.That(playerRenderers, Is.Not.Empty, "The provider-authored player prefab has no renderable visual."); @@ -387,6 +451,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests File.WriteAllBytes(capturePath, pixels.EncodeToPNG()); Assert.That(new FileInfo(capturePath).Length, Is.GreaterThan(1024)); + mapRenderers = mapRenderers.Where(renderer => renderer != null).ToList(); + Assert.That(mapRenderers, Is.Not.Empty, + "All advertised map-channel renderers were destroyed before map capture."); var mapBounds = mapRenderers[0].bounds; foreach (var renderer in mapRenderers) mapBounds.Encapsulate(renderer.bounds); @@ -420,7 +487,13 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests (mapCamera.cullingMask & (1 << mapLayer)) != 0, movementReceipt, focusReceipt, - actionReceipt); + actionReceipt, + observedShot, + shieldRatioBefore, + observedCombat?.ShieldRatio ?? shieldRatioBefore, + hullRatioBefore, + observedCombat?.HullRatio ?? hullRatioBefore, + (float)observedShot.LockQuality); } finally { @@ -451,7 +524,13 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests bool mapCameraIncludesMapChannel, WitnessReceipt movement, WitnessReceipt targeting, - WitnessReceipt action) + WitnessReceipt action, + EveUnityShotReceipt shot, + float shieldRatioBefore, + float shieldRatioAfter, + float hullRatioBefore, + float hullRatioAfter, + float lockProgress) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); if (string.IsNullOrWhiteSpace(path)) return; @@ -462,6 +541,17 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests movement = movement != null, targeting = targeting != null, action = action != null, + combatPresentation = shot != null, + shotId = shot?.ShotId ?? "", + shotHit = shot?.Hit ?? false, + impactKind = shot?.ImpactKind ?? "", + appliedDamage = (float)(shot?.AppliedDamage ?? 0), + shieldAbsorbedDamage = (float)(shot?.ShieldAbsorbedDamage ?? 0), + shieldRatioBefore = shieldRatioBefore, + shieldRatioAfter = shieldRatioAfter, + hullRatioBefore = hullRatioBefore, + hullRatioAfter = hullRatioAfter, + lockProgress = lockProgress, initialVersion = initialVersion, finalVersion = finalVersion, movementDistance = movementDistance, @@ -485,6 +575,17 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public bool movement; public bool targeting; public bool action; + public bool combatPresentation; + public string shotId; + public bool shotHit; + public string impactKind; + public float appliedDamage; + public float shieldAbsorbedDamage; + public float shieldRatioBefore; + public float shieldRatioAfter; + public float hullRatioBefore; + public float hullRatioAfter; + public float lockProgress; public long initialVersion; public long finalVersion; public float movementDistance; diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 344a044..088d8a4 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -190,6 +190,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests WitnessReceipt movementReceipt = null; WitnessReceipt focusReceipt = null; WitnessReceipt actionReceipt = null; + EveUnityShotReceipt observedShot = null; + EveUnityCombatPresentation observedCombat = null; + float shieldRatioBefore = 0f; + float hullRatioBefore = 0f; long initialVersion = 0; float movementDistance = 0f; @@ -231,6 +235,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests provider, provider); host.Connect(); + host.ShotAvailable += shot => observedShot = shot; runtime = host.Runtime; Assert.That(runtime, Is.Not.Null); Assert.That(runtime.ActiveWorld.PlayerEntityId, Is.Not.Empty); @@ -248,12 +253,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var playerId = runtime.ActiveWorld.PlayerEntityId; var playerFact = host.PresentedEntities.CurrentGeneration.Entities .Single(entity => entity.EntityId == playerId); - var targetEntityId = host.PresentedEntities.CurrentGeneration.Entities - .Where(entity => entity.EntityId != playerId) - .Select(entity => entity.EntityId) - .FirstOrDefault(); - Assert.That(targetEntityId, Is.Not.Null.And.Not.Empty, - "The advertised live generation has no explicit targeting candidate."); var playerPrefab = provider.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( playerFact.AssetRef, playerFact.EntityKind, @@ -292,13 +291,27 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(movementDistance, Is.GreaterThan(0.01f)); movementReceipt = WitnessReceipt.From("movement", runtime.LastReceipt); + var releaseVersion = runtime.ActiveVersion; + var release = runtime.SubmitMoveVectorIntent(playerId, 0f, 0f, 0f); + var releaseDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < releaseDeadline && + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != release.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase))) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + AssertReconciledProviderReceipt(runtime.LastReceipt, release.CommandId, + provider.Selection.ProviderId, provider.Selection.SurfaceId, releaseVersion); + var movementVersion = runtime.ActiveVersion; - var focus = runtime.SubmitTargetIntent(playerId, targetEntityId); + var focus = runtime.SubmitFocusIntent(playerId); var focusDeadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < focusDeadline && (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != focus.CommandId || !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || - runtime.ActiveVersion < runtime.LastReceipt.SourceVersion)) + runtime.ActiveVersion < runtime.LastReceipt.SourceVersion || + string.IsNullOrWhiteSpace(EveUnityCombatPresentation.Find(runtime.ActiveProjection)?.SelectedTargetEntityId))) { yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); @@ -311,12 +324,49 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests movementVersion); Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); focusReceipt = WitnessReceipt.From("targeting", runtime.LastReceipt); + observedCombat = EveUnityCombatPresentation.Find(runtime.ActiveProjection); + Assert.That(observedCombat, Is.Not.Null); + Assert.That(observedCombat.TargetVisible, Is.True); + Assert.That(observedCombat.TargetHostile, Is.True); + var targetEntityId = observedCombat.SelectedTargetEntityId; + var targetEntityIndex = observedCombat.SelectedTargetEntityIndex; + Assert.That(targetEntityId, Is.Not.Empty); + Assert.That(host.PresentedEntities.CurrentGeneration.Entities.Any(entity => entity.EntityId == targetEntityId), Is.True, + "The provider-selected combat target is absent from the advertised SoA generation."); + shieldRatioBefore = observedCombat.ShieldRatio; + hullRatioBefore = observedCombat.HullRatio; + + var targetFact = host.PresentedEntities.CurrentGeneration.Entities + .Single(entity => entity.EntityId == targetEntityId); + playerFact = host.PresentedEntities.CurrentGeneration.Entities + .Single(entity => entity.EntityId == playerId); + var aim = targetFact.Position - playerFact.Position; + aim.y = 0; + Assert.That(aim.sqrMagnitude, Is.GreaterThan(0.0001f)); + aim.Normalize(); + var steerVersion = runtime.ActiveVersion; + var steer = runtime.SubmitMoveVectorIntent(playerId, aim.x, aim.z, 0.25f); + var steerDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < steerDeadline && + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != steer.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase))) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + AssertReconciledProviderReceipt(runtime.LastReceipt, steer.CommandId, + provider.Selection.ProviderId, provider.Selection.SurfaceId, steerVersion); var focusVersion = runtime.ActiveVersion; - var action = runtime.SubmitActionIntent(playerId, "0"); + var inputDriver = root.GetComponent() ?? + root.AddComponent(); + inputDriver.Host = host; + var action = inputDriver.SubmitPrimaryAction(); + Assert.That(action, Is.Not.Null, + "The advertised input capability did not bind a primary action for this generic client."); var actionDeadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < actionDeadline && - (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != action.CommandId || + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != action!.CommandId || !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || runtime.ActiveVersion < runtime.LastReceipt.SourceVersion)) { @@ -325,13 +375,38 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } AssertReconciledProviderReceipt( runtime.LastReceipt, - action.CommandId, + action!.CommandId, provider.Selection.ProviderId, provider.Selection.SurfaceId, focusVersion); Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); actionReceipt = WitnessReceipt.From("action", runtime.LastReceipt); + var shotDeadline = Time.realtimeSinceStartup + 15f; + while (Time.realtimeSinceStartup < shotDeadline && observedShot == null) + { + yield return new WaitForSecondsRealtime(0.05f); + runtime.Refresh(); + } + Assert.That(observedShot, Is.Not.Null, + "A reconciled fire request never reached daemon-owned lock acquisition and shot resolution."); + observedCombat = EveUnityCombatPresentation.Find(runtime.ActiveProjection); + Assert.That(observedShot.TargetEntityIndex, Is.EqualTo(targetEntityIndex)); + Assert.That(observedShot.LockQuality, Is.GreaterThan(0.99)); + var trajectories = root.GetComponent(); + var combatRenderer = root.GetComponent(); + Assert.That(trajectories, Is.Not.Null); + Assert.That(trajectories.ActiveTrajectoryCount, Is.GreaterThan(0)); + Assert.That(combatRenderer, Is.Not.Null); + combatRenderer.RefreshNow(); + if (observedCombat != null && !string.IsNullOrWhiteSpace(observedCombat.SelectedTargetEntityId)) + { + Assert.That(combatRenderer.ReticleVisible, Is.True); + Assert.That(combatRenderer.LockVisible, Is.True); + } + if (observedShot.Hit && (observedShot.AppliedDamage > 0 || observedShot.ShieldAbsorbedDamage > 0)) + Assert.That(combatRenderer.HitMarkerVisible, Is.True); + var light = lightObject.AddComponent(); light.type = LightType.Directional; light.intensity = 1.4f; @@ -340,17 +415,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(0.035f, 0.055f, 0.08f, 1f); camera.cullingMask = -1; - foreach (var entityMarker in root.GetComponentsInChildren()) - { - foreach (var renderer in entityMarker.GetComponentsInChildren()) - { - renderer.material.color = entityMarker.Controllable - ? new Color(0.18f, 0.75f, 1f) - : entityMarker.EntityKind == "enemy" - ? new Color(1f, 0.25f, 0.2f) - : new Color(1f, 0.78f, 0.15f); - } - } var playerMarker = FindMarker(root, playerId); var playerRenderers = playerMarker.GetComponentsInChildren(); Assert.That(playerRenderers, Is.Not.Empty, "The provider-authored player prefab has no renderable visual."); @@ -387,6 +451,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests File.WriteAllBytes(capturePath, pixels.EncodeToPNG()); Assert.That(new FileInfo(capturePath).Length, Is.GreaterThan(1024)); + mapRenderers = mapRenderers.Where(renderer => renderer != null).ToList(); + Assert.That(mapRenderers, Is.Not.Empty, + "All advertised map-channel renderers were destroyed before map capture."); var mapBounds = mapRenderers[0].bounds; foreach (var renderer in mapRenderers) mapBounds.Encapsulate(renderer.bounds); @@ -420,7 +487,13 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests (mapCamera.cullingMask & (1 << mapLayer)) != 0, movementReceipt, focusReceipt, - actionReceipt); + actionReceipt, + observedShot, + shieldRatioBefore, + observedCombat?.ShieldRatio ?? shieldRatioBefore, + hullRatioBefore, + observedCombat?.HullRatio ?? hullRatioBefore, + (float)observedShot.LockQuality); } finally { @@ -451,7 +524,13 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests bool mapCameraIncludesMapChannel, WitnessReceipt movement, WitnessReceipt targeting, - WitnessReceipt action) + WitnessReceipt action, + EveUnityShotReceipt shot, + float shieldRatioBefore, + float shieldRatioAfter, + float hullRatioBefore, + float hullRatioAfter, + float lockProgress) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); if (string.IsNullOrWhiteSpace(path)) return; @@ -462,6 +541,17 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests movement = movement != null, targeting = targeting != null, action = action != null, + combatPresentation = shot != null, + shotId = shot?.ShotId ?? "", + shotHit = shot?.Hit ?? false, + impactKind = shot?.ImpactKind ?? "", + appliedDamage = (float)(shot?.AppliedDamage ?? 0), + shieldAbsorbedDamage = (float)(shot?.ShieldAbsorbedDamage ?? 0), + shieldRatioBefore = shieldRatioBefore, + shieldRatioAfter = shieldRatioAfter, + hullRatioBefore = hullRatioBefore, + hullRatioAfter = hullRatioAfter, + lockProgress = lockProgress, initialVersion = initialVersion, finalVersion = finalVersion, movementDistance = movementDistance, @@ -485,6 +575,17 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public bool movement; public bool targeting; public bool action; + public bool combatPresentation; + public string shotId; + public bool shotHit; + public string impactKind; + public float appliedDamage; + public float shieldAbsorbedDamage; + public float shieldRatioBefore; + public float shieldRatioAfter; + public float hullRatioBefore; + public float hullRatioAfter; + public float lockProgress; public long initialVersion; public long finalVersion; public float movementDistance; diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index e94e088..27f94bd 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -35,6 +35,14 @@ Retained provider feedback is lowered without becoming Unity authority. The generic `EveUnityShotTrajectoryRenderer` renders provider-authored origin, endpoint, duration, and hit outcome, then expires the visual locally. It never uses Unity collision to infer damage or report a hit back to the provider. +Providers may publish a non-spatial `combat.presentation` component beside the +SoA body view. `EveUnityCombatPresentationRenderer` lowers its selected target, +contact visibility, lock progress, meter ratios, semantic roles, and timing +into reticle, lock, shield/hull, and hit-marker visuals. Hit markers require a +new deduplicated shot receipt from the advertised controlled source to the +advertised selection with authoritative applied or shield-absorbed damage. +SoA remains the only transform authority; the presentation component never +duplicates bodies or computes combat outcomes. The reusable `LightningCompute` renderer and its shader/material bundle live in this runtime package with their original Unity GUIDs. Provider asset bundles may therefore reference the effect without importing Aetheria gameplay code. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 7eb8f74..3c43f78 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -25,6 +25,7 @@ namespace GameCult.Eve.UnityScene { typeof(EveProviderAdvertisementDocument), typeof(EveSurfaceDocument), + typeof(EveInputCapabilityDocument), typeof(EveSurfaceCommandRequest), typeof(EveCommandReceiptDocument), typeof(EveAssetCatalogDocument), @@ -86,6 +87,7 @@ namespace GameCult.Eve.UnityScene _bodyResolver = bodyResolver; CurrentSurfaceDocument = EmptySurfaceDocument(); CurrentAssetManifestDocument = EmptyAssetManifest(); + CurrentInputCapability = new EveInputCapabilityDocument(); } public string TransportKind => "eve-cultmesh-remote-replica"; @@ -98,6 +100,8 @@ namespace GameCult.Eve.UnityScene public EveUnityPlayableWorldAssetManifestDocument CurrentAssetManifestDocument { get; private set; } + public EveInputCapabilityDocument CurrentInputCapability { get; private set; } + public event Action? SurfaceDocumentAvailable; public event Action? AssetManifestDocumentAvailable; @@ -132,9 +136,41 @@ namespace GameCult.Eve.UnityScene EnsureOpen(); ResolveAdvertisement(forceRefresh: true); RefreshSurface(); + RefreshInputCapability(); RefreshAssetCatalog(); } + private void RefreshInputCapability() + { + var recordRef = FindComponentProp( + CurrentSurfaceDocument.SurfaceDocument.Surface.Root, + "inputCapability"); + if (string.IsNullOrWhiteSpace(recordRef)) + throw new InvalidOperationException("The advertised playable world does not publish an input capability record."); + CurrentInputCapability = _snapshot! + .FetchDocumentsAsync( + recordKeys: new[] { recordRef }, + schemaIds: new[] { EveInputCapabilityDocument.SchemaId }) + .GetAwaiter() + .GetResult() + .FirstOrDefault() + ?? throw new InvalidOperationException($"The advertised input capability '{recordRef}' was not available."); + } + + private static string FindComponentProp(EveSurfaceComponent component, string key) + { + var value = component.GetProp(key); + if (!string.IsNullOrWhiteSpace(value)) + return value; + foreach (var child in component.Children) + { + value = FindComponentProp(child, key); + if (!string.IsNullOrWhiteSpace(value)) + return value; + } + return ""; + } + public void PumpLiveEvents() { PollCurrentEntityView(); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs index a91c832..a2f6ea3 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs @@ -18,7 +18,8 @@ namespace GameCult.Eve.UnityScene IEveUnityProviderRefreshSource, IEveUnityEntitySoaViewDocumentSource, IEveUnityCameraRenderPolicySource, - IEveUnityNativeAssetProvider + IEveUnityNativeAssetProvider, + IEveUnityInputCapabilitySource { [SerializeField] private string rendezvousEndpoint = ""; [SerializeField] private string providerFilter = ""; @@ -40,6 +41,15 @@ namespace GameCult.Eve.UnityScene public EveUnitySceneProviderSurfaceDocument CurrentDocument => Bridge.CurrentSurfaceDocument; + public EveInputCapabilityDocument CurrentInputCapability + { + get + { + EnsureTransport(); + return _transport!.CurrentInputCapability; + } + } + public event Action? DocumentAvailable { add { Bridge.DocumentAvailable += value; } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentation.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentation.cs new file mode 100644 index 0000000..d3879af --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentation.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +#nullable enable + +namespace GameCult.Eve.UnityScene +{ + public sealed class EveUnityCombatPresentation + { + public EveUnityCombatPresentation(EveUnitySceneNode node) + { + if (node == null) throw new ArgumentNullException(nameof(node)); + ControlledEntityId = Get(node, "controlledEntityId"); + ControlledEntityIndex = Int(node, "controlledEntityIndex", -1); + SelectedTargetEntityId = Get(node, "selectedTargetEntityId"); + SelectedTargetEntityIndex = Int(node, "selectedTargetEntityIndex", -1); + TargetVisible = Bool(node, "targetVisible"); + TargetHostile = Bool(node, "targetHostile"); + ContactInformation = Ratio(node, "contactInformation"); + ShieldRatio = Ratio(node, "shieldRatio"); + HullRatio = Ratio(node, "hullRatio"); + LockProgress = Ratio(node, "lockProgress"); + ReticleRole = Get(node, "reticleRole"); + LockRole = Get(node, "lockRole"); + ShieldMeterRole = Get(node, "shieldMeterRole"); + HullMeterRole = Get(node, "hullMeterRole"); + HitMarkerRole = Get(node, "hitMarkerRole"); + LockDisplayThreshold = Ratio(node, "lockDisplayThreshold", 0.01f); + HitMarkerDurationSeconds = Math.Max(0.01f, Number(node, "hitMarkerDurationSeconds", 0.25f)); + RadialFillMinimum = Ratio(node, "radialFillMinimum", 0.25f); + RadialFillMaximum = Ratio(node, "radialFillMaximum", 0.75f); + } + + public string ControlledEntityId { get; } + public int ControlledEntityIndex { get; } + public string SelectedTargetEntityId { get; } + public int SelectedTargetEntityIndex { get; } + public bool TargetVisible { get; } + public bool TargetHostile { get; } + public float ContactInformation { get; } + public float ShieldRatio { get; } + public float HullRatio { get; } + public float LockProgress { get; } + public string ReticleRole { get; } + public string LockRole { get; } + public string ShieldMeterRole { get; } + public string HullMeterRole { get; } + public string HitMarkerRole { get; } + public float LockDisplayThreshold { get; } + public float HitMarkerDurationSeconds { get; } + public float RadialFillMinimum { get; } + public float RadialFillMaximum { get; } + + public float RadialFill(float ratio) => + RadialFillMinimum + (RadialFillMaximum - RadialFillMinimum) * Math.Max(0, Math.Min(1, ratio)); + + public bool PresentsHit(EveUnityShotReceipt receipt) => + receipt != null && + receipt.SourceEntityIndex == ControlledEntityIndex && + receipt.TargetEntityIndex == SelectedTargetEntityIndex && + receipt.Hit && + (receipt.AppliedDamage > 0 || receipt.ShieldAbsorbedDamage > 0); + + public static EveUnityCombatPresentation? Find(EveUnitySceneProjection? projection) + { + if (projection == null) return null; + return Find(projection.Root); + } + + private static EveUnityCombatPresentation? Find(EveUnitySceneNode node) + { + if (string.Equals(node.ComponentKind, "combat.presentation", StringComparison.Ordinal)) + return new EveUnityCombatPresentation(node); + foreach (var child in node.Children) + { + var found = Find(child); + if (found != null) return found; + } + return null; + } + + private static string Get(EveUnitySceneNode node, string key) => + node.Props.TryGetValue(key, out var value) ? value ?? "" : ""; + + private static int Int(EveUnitySceneNode node, string key, int fallback) => + int.TryParse(Get(node, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : fallback; + + private static bool Bool(EveUnitySceneNode node, string key) => + bool.TryParse(Get(node, key), out var value) && value; + + private static float Number(EveUnitySceneNode node, string key, float fallback = 0) => + float.TryParse(Get(node, key), NumberStyles.Float, CultureInfo.InvariantCulture, out var value) ? value : fallback; + + private static float Ratio(EveUnitySceneNode node, string key, float fallback = 0) => + Math.Max(0, Math.Min(1, Number(node, key, fallback))); + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentation.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentation.cs.meta new file mode 100644 index 0000000..e0219d9 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentation.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e15acab8e9094b18b6dbd3abea0e055d diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs new file mode 100644 index 0000000..6b0cd5b --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs @@ -0,0 +1,262 @@ +using System; +using UnityEngine; + +#nullable enable + +namespace GameCult.Eve.UnityScene +{ + public sealed class EveUnityCombatPresentationRenderer : MonoBehaviour + { + [SerializeField] private Color reticleColor = new Color(0.2f, 0.9f, 1f, 0.9f); + [SerializeField] private Color lockColor = new Color(1f, 0.7f, 0.15f, 1f); + [SerializeField] private Color shieldColor = new Color(0.15f, 0.65f, 1f, 1f); + [SerializeField] private Color hullColor = new Color(1f, 0.35f, 0.2f, 1f); + [SerializeField] private Color hitColor = Color.white; + [SerializeField] private float width = 0.12f; + + private EveUnityPlayableWorldClientHost? _host; + private Transform? _visualRoot; + private LineRenderer? _reticle; + private LineRenderer? _lock; + private LineRenderer? _shield; + private LineRenderer? _hull; + private LineRenderer? _hitA; + private LineRenderer? _hitB; + private Material? _material; + private float _hitUntil; + private EveUnityCombatPresentation? _lastPresentation; + private Vector3 _lastCenter; + private Vector3 _lastRight; + private Vector3 _lastUp; + private float _lastRadius; + private bool _hasLastTarget; + + public EveUnityCombatPresentation? Current { get; private set; } + public bool ReticleVisible => _reticle != null && _reticle.enabled; + public bool LockVisible => _lock != null && _lock.enabled; + public bool HitMarkerVisible => _hitA != null && _hitA.enabled; + + public void Bind(EveUnityPlayableWorldClientHost host) + { + if (host == null) throw new ArgumentNullException(nameof(host)); + if (ReferenceEquals(_host, host)) return; + Unbind(); + _host = host; + _host.ShotAvailable += OnShotAvailable; + } + + public void Unbind() + { + if (_host != null) _host.ShotAvailable -= OnShotAvailable; + _host = null; + Current = null; + SetVisible(false); + } + + public void RefreshNow() + { + Current = EveUnityCombatPresentation.Find(_host?.ActiveProjection); + if (Current == null || string.IsNullOrWhiteSpace(Current.SelectedTargetEntityId)) + { + PresentRetainedHit(); + return; + } + + var marker = FindMarker(Current.SelectedTargetEntityId); + if (marker == null) + { + PresentRetainedHit(); + return; + } + + _lastPresentation = Current; + EnsureVisuals(); + PresentationAxes(out var right, out var up, out var towardCamera, out var cullingMask); + var visualBounds = VisualBounds(marker.transform, cullingMask); + var center = visualBounds?.center ?? (marker.transform.position + Vector3.up * 0.25f); + var radius = Math.Max(0.8f, visualBounds?.extents.magnitude ?? marker.transform.lossyScale.magnitude * 0.8f); + center += towardCamera * 0.1f; + _lastCenter = center; + _lastRight = right; + _lastUp = up; + _lastRadius = radius; + _hasLastTarget = true; + Arc(_reticle!, center, right, up, radius * 1.35f, 1f, 0f); + _reticle!.enabled = Current.TargetVisible; + + var lockVisible = Current.TargetVisible && Current.TargetHostile && + Current.LockProgress > Current.LockDisplayThreshold; + var noise = (1f - Current.LockProgress) * radius * 0.2f; + var frequency = Mathf.Lerp(0.01f, 10f, Mathf.Pow(Current.LockProgress, 4f)); + var noisyCenter = center + + right * ((Mathf.PerlinNoise(Time.unscaledTime * frequency, 0.37f) - 0.5f) * noise) + + up * ((Mathf.PerlinNoise(0.73f, Time.unscaledTime * frequency) - 0.5f) * noise); + var spin = Time.unscaledTime * Mathf.Lerp(5f, 720f, Current.LockProgress * Current.LockProgress); + Arc(_lock!, noisyCenter, right, up, radius * 1.55f, Current.LockProgress, spin); + _lock!.enabled = lockVisible; + + Arc(_shield!, center, right, up, radius * 1.78f, Current.RadialFill(Current.ShieldRatio), -90f); + Arc(_hull!, center, right, up, radius * 1.98f, Current.RadialFill(Current.HullRatio), -90f); + _shield!.enabled = Current.TargetVisible; + _hull!.enabled = Current.TargetVisible; + + var hitVisible = Time.unscaledTime < _hitUntil; + Cross(_hitA!, _hitB!, center, right, up, radius * 0.55f); + _hitA!.enabled = hitVisible; + _hitB!.enabled = hitVisible; + } + + private void PresentRetainedHit() + { + if (!_hasLastTarget || Time.unscaledTime >= _hitUntil) + { + SetVisible(false); + return; + } + EnsureVisuals(); + _reticle!.enabled = _lock!.enabled = _shield!.enabled = _hull!.enabled = false; + Cross(_hitA!, _hitB!, _lastCenter, _lastRight, _lastUp, _lastRadius * 0.55f); + _hitA!.enabled = _hitB!.enabled = true; + } + + private void LateUpdate() => RefreshNow(); + + private void OnShotAvailable(EveUnityShotReceipt receipt) + { + Current = EveUnityCombatPresentation.Find(_host?.ActiveProjection); + var presentation = Current; + if (presentation?.PresentsHit(receipt) != true) + presentation = _lastPresentation; + if (presentation?.PresentsHit(receipt) == true) + _hitUntil = Time.unscaledTime + presentation.HitMarkerDurationSeconds; + } + + private EveUnityPlayableWorldEntityMarker? FindMarker(string entityId) + { + if (_host == null) return null; + foreach (var marker in _host.SceneRoot.GetComponentsInChildren(true)) + if (string.Equals(marker.EntityId, entityId, StringComparison.Ordinal)) return marker; + return null; + } + + private void EnsureVisuals() + { + if (_visualRoot != null) return; + var root = new GameObject("Eve combat presentation"); + root.transform.SetParent(transform, false); + _visualRoot = root.transform; + _reticle = Line("Selected target reticle", reticleColor, width); + _lock = Line("Target lock", lockColor, width * 1.15f); + _shield = Line("Target shield", shieldColor, width * 0.75f); + _hull = Line("Target hull", hullColor, width * 0.75f); + _hitA = Line("Hit marker A", hitColor, width * 1.3f); + _hitB = Line("Hit marker B", hitColor, width * 1.3f); + } + + private LineRenderer Line(string name, Color color, float lineWidth) + { + var child = new GameObject(name); + child.transform.SetParent(_visualRoot, false); + var line = child.AddComponent(); + line.useWorldSpace = true; + line.loop = false; + line.material = SharedMaterial(); + line.startColor = line.endColor = color; + line.startWidth = line.endWidth = Math.Max(0.01f, lineWidth); + line.numCapVertices = 2; + return line; + } + + private Material SharedMaterial() + { + if (_material != null) return _material; + var shader = Shader.Find("Sprites/Default") ?? Shader.Find("Universal Render Pipeline/Unlit"); + if (shader == null) throw new InvalidOperationException("EveUnity requires an unlit shader for combat presentation."); + _material = new Material(shader) { name = "EveUnity combat presentation" }; + return _material; + } + + private static void PresentationAxes(out Vector3 right, out Vector3 up, out Vector3 towardCamera, out int cullingMask) + { + Camera? selected = Camera.main; + if (selected == null) + { + foreach (var candidate in Camera.allCameras) + { + if (!candidate.isActiveAndEnabled || candidate.orthographic) + continue; + selected = candidate; + break; + } + } + right = selected == null ? Vector3.right : selected.transform.right; + up = selected == null ? Vector3.up : selected.transform.up; + towardCamera = selected == null ? Vector3.back : -selected.transform.forward; + cullingMask = selected == null ? -1 : selected.cullingMask; + } + + private static Bounds? VisualBounds(Transform root, int cullingMask) + { + Bounds? bounds = null; + foreach (var renderer in root.GetComponentsInChildren(includeInactive: false)) + { + if (renderer == null || !renderer.enabled) + continue; + if ((cullingMask & (1 << renderer.gameObject.layer)) == 0) + continue; + if (renderer is LineRenderer || renderer is TrailRenderer || + string.Equals(renderer.GetType().Name, "ParticleSystemRenderer", StringComparison.Ordinal)) + continue; + if (bounds.HasValue) + { + var combined = bounds.Value; + combined.Encapsulate(renderer.bounds); + bounds = combined; + } + else + { + bounds = renderer.bounds; + } + } + return bounds; + } + + private static void Arc(LineRenderer line, Vector3 center, Vector3 right, Vector3 up, float radius, float fill, float rotationDegrees) + { + fill = Mathf.Clamp01(fill); + var segments = Math.Max(2, Mathf.CeilToInt(64 * fill) + 1); + line.positionCount = segments; + for (var index = 0; index < segments; index++) + { + var progress = segments <= 1 ? 0 : index / (float)(segments - 1); + var angle = (rotationDegrees + progress * 360f * fill) * Mathf.Deg2Rad; + line.SetPosition(index, center + right * (Mathf.Cos(angle) * radius) + up * (Mathf.Sin(angle) * radius)); + } + } + + private static void Cross(LineRenderer a, LineRenderer b, Vector3 center, Vector3 right, Vector3 up, float radius) + { + a.positionCount = b.positionCount = 2; + a.SetPosition(0, center - right * radius - up * radius); + a.SetPosition(1, center + right * radius + up * radius); + b.SetPosition(0, center - right * radius + up * radius); + b.SetPosition(1, center + right * radius - up * radius); + } + + private void SetVisible(bool visible) + { + if (_reticle != null) _reticle.enabled = visible; + if (_lock != null) _lock.enabled = visible; + if (_shield != null) _shield.enabled = visible; + if (_hull != null) _hull.enabled = visible; + if (_hitA != null) _hitA.enabled = visible && Time.unscaledTime < _hitUntil; + if (_hitB != null) _hitB.enabled = visible && Time.unscaledTime < _hitUntil; + } + + private void OnDestroy() + { + Unbind(); + if (_material != null) Destroy(_material); + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs.meta new file mode 100644 index 0000000..25e78e2 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5c953d99a7454322b2935b878295b357 diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index fafe3c9..4cf6d8f 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -71,7 +71,8 @@ namespace GameCult.Eve.UnityScene } var bounds = CalculateVisualBounds( player, - cameraComponent != null ? cameraComponent.cullingMask : -1); + cameraComponent != null ? cameraComponent.cullingMask : -1, + FindSelectedTarget(resolvedHost)); var target = bounds?.center ?? player.position; var radius = bounds?.extents.magnitude ?? 0f; var verticalFov = cameraComponent != null ? cameraComponent.fieldOfView : 60f; @@ -91,29 +92,47 @@ namespace GameCult.Eve.UnityScene return true; } - private static Bounds? CalculateVisualBounds(Transform player, int cullingMask) + private static Bounds? CalculateVisualBounds(Transform player, int cullingMask, Transform? selectedTarget) { - var renderers = player.GetComponentsInChildren(includeInactive: false); Bounds? bounds = null; - foreach (var visual in renderers) + foreach (var root in selectedTarget == null + ? new[] { player } + : new[] { player, selectedTarget! }) { - if (visual == null || !visual.enabled || - (cullingMask & (1 << visual.gameObject.layer)) == 0) - continue; - if (bounds.HasValue) + foreach (var visual in root.GetComponentsInChildren(includeInactive: false)) { - var combined = bounds.Value; - combined.Encapsulate(visual.bounds); - bounds = combined; - } - else - { - bounds = visual.bounds; + if (visual == null || !visual.enabled || + (cullingMask & (1 << visual.gameObject.layer)) == 0) + continue; + if (visual is LineRenderer || visual is TrailRenderer || + string.Equals(visual.GetType().Name, "ParticleSystemRenderer", System.StringComparison.Ordinal)) + continue; + if (bounds.HasValue) + { + var combined = bounds.Value; + combined.Encapsulate(visual.bounds); + bounds = combined; + } + else + { + bounds = visual.bounds; + } } } return bounds; } + private static Transform? FindSelectedTarget(EveUnityPlayableWorldClientHost host) + { + var combat = EveUnityCombatPresentation.Find(host.ActiveProjection); + if (combat == null || string.IsNullOrWhiteSpace(combat.SelectedTargetEntityId)) + return null; + if (host.PresentedEntities?.CurrentGeneration != null && + host.PresentedEntities.TryGetByEntityId(combat.SelectedTargetEntityId, out var presented)) + return presented.Transform; + return null; + } + private void LateUpdate() { if (driveInLateUpdate) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs index 60fefac..1323c80 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs @@ -33,6 +33,9 @@ namespace GameCult.Eve.UnityScene public EveUnitySceneCommandReceipt? LastReceipt => Runtime?.LastReceipt; public IEveUnityPresentedEntityRegistry? PresentedEntities => Runtime?.PresentedEntities; + public EveInputCapabilityDocument? InputCapability => + (providerSurfaceDocuments as IEveUnityInputCapabilitySource)?.CurrentInputCapability; + public long ActiveVersion => Runtime?.ActiveVersion ?? 0; public event Action? FeedbackAvailable; @@ -89,6 +92,9 @@ namespace GameCult.Eve.UnityScene var feedbackEffects = GetComponent(); if (feedbackEffects == null) feedbackEffects = gameObject.AddComponent(); feedbackEffects.Bind(this, Runtime.GameObjectAssetProvider); + var combatPresentation = GetComponent(); + if (combatPresentation == null) combatPresentation = gameObject.AddComponent(); + combatPresentation.Bind(this); var thermal = GetComponent(); var thermalHud = GetComponent(); if (thermalHud == null) thermalHud = gameObject.AddComponent(); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs index 03850e1..c321815 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using GameCult.Eve.Surface; using UnityEngine; @@ -6,6 +7,11 @@ using UnityEngine; namespace GameCult.Eve.UnityScene { + public interface IEveUnityInputCapabilitySource + { + EveInputCapabilityDocument CurrentInputCapability { get; } + } + public sealed class EveUnityPlayableWorldInputDriver : MonoBehaviour { [SerializeField] private EveUnityPlayableWorldClientHost? host; @@ -71,15 +77,41 @@ namespace GameCult.Eve.UnityScene issuedAt); } - public EveSurfaceCommandRequest? SubmitPrimaryAction(string actionId = "primary") + public EveSurfaceCommandRequest? SubmitPrimaryAction(string actionId = "") { var resolvedHost = ResolveHost(); if (resolvedHost?.ActiveWorld == null) return null; + var resolvedActionId = string.IsNullOrWhiteSpace(actionId) + ? ResolvePrimaryActionId(resolvedHost.InputCapability) + : actionId; + if (string.IsNullOrWhiteSpace(resolvedActionId)) + return null; + return resolvedHost.SubmitActionIntent( resolvedHost.ActiveWorld.PlayerEntityId, - string.IsNullOrWhiteSpace(actionId) ? "primary" : actionId); + resolvedActionId); + } + + public static string ResolvePrimaryActionId(EveInputCapabilityDocument? capability) + { + if (capability == null) + return ""; + var profile = (capability.DefaultProfiles ?? Array.Empty()) + .FirstOrDefault(value => string.Equals(value.DeviceClass, "keyboard-mouse", StringComparison.Ordinal)) + ?? (capability.DefaultProfiles ?? Array.Empty()).FirstOrDefault(); + var binding = (profile?.Bindings ?? Array.Empty()) + .FirstOrDefault(value => (value.Gesture?.Controls ?? Array.Empty()) + .Any(control => string.Equals(control, "mouse.primary", StringComparison.Ordinal))) + ?? (profile?.Bindings ?? Array.Empty()).FirstOrDefault(value => value.ActionBar); + if (binding == null) + return ""; + var action = (capability.Actions ?? Array.Empty()) + .FirstOrDefault(value => string.Equals(value.ActionId, binding.ActionId, StringComparison.Ordinal)); + return action != null && string.Equals(action.Availability, "available", StringComparison.OrdinalIgnoreCase) + ? action.ActionId + : ""; } private void Update() @@ -91,7 +123,7 @@ namespace GameCult.Eve.UnityScene SubmitMoveVectorInput(Input.GetAxisRaw(horizontalAxis), Input.GetAxisRaw(verticalAxis)); if (!string.IsNullOrWhiteSpace(actionButton) && Input.GetButtonDown(actionButton)) - SubmitPrimaryAction(actionButton); + SubmitPrimaryAction(); } private EveUnityPlayableWorldClientHost? ResolveHost() diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotReceiptPresenter.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotReceiptPresenter.cs index 5f853ce..9bd38fe 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotReceiptPresenter.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotReceiptPresenter.cs @@ -38,6 +38,9 @@ namespace GameCult.Eve.UnityScene ItemKey = Get(node, "itemKey"); ImpactKind = Get(node, "impactKind"); PresentationIntensity = Math.Max(0, Double(node, "presentationIntensity", 1)); + AppliedDamage = Math.Max(0, Double(node, "appliedDamage", 0)); + ShieldAbsorbedDamage = Math.Max(0, Double(node, "shieldAbsorbedDamage", 0)); + LockQuality = Math.Max(0, Math.Min(1, Double(node, "lockQuality", 0))); } public string ShotId { get; } @@ -54,6 +57,9 @@ namespace GameCult.Eve.UnityScene public string ItemKey { get; } public string ImpactKind { get; } public double PresentationIntensity { get; } + public double AppliedDamage { get; } + public double ShieldAbsorbedDamage { get; } + public double LockQuality { get; } private static string Get(EveUnitySceneNode node, string key) => node.Props.TryGetValue(key, out var value) ? value ?? "" : ""; diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs new file mode 100644 index 0000000..8a06221 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; + +#nullable enable + +namespace GameCult.Eve.UnityScene.Tests +{ + public sealed class EveUnityCombatPresentationTests + { + [Test] + public void ProjectionPreservesProviderSelectionLockMetersAndFossilTiming() + { + var projection = Projection(Combat()); + var combat = EveUnityCombatPresentation.Find(projection); + + Assert.That(combat, Is.Not.Null); + Assert.That(combat!.ControlledEntityIndex, Is.EqualTo(4)); + Assert.That(combat.SelectedTargetEntityId, Is.EqualTo("run.zone.0.entity.9")); + Assert.That(combat.TargetVisible, Is.True); + Assert.That(combat.TargetHostile, Is.True); + Assert.That(combat.LockProgress, Is.EqualTo(0.6f).Within(0.0001f)); + Assert.That(combat.RadialFill(0), Is.EqualTo(0.25f).Within(0.0001f)); + Assert.That(combat.RadialFill(1), Is.EqualTo(0.75f).Within(0.0001f)); + Assert.That(combat.HitMarkerDurationSeconds, Is.EqualTo(0.25f)); + } + + [Test] + public void HitMarkerRequiresControlledSourceSelectedTargetAndAuthoritativeDamage() + { + var combat = new EveUnityCombatPresentation(Combat()); + + Assert.That(combat.PresentsHit(Shot(4, 9, true, 12, 0)), Is.True); + Assert.That(combat.PresentsHit(Shot(3, 9, true, 12, 0)), Is.False); + Assert.That(combat.PresentsHit(Shot(4, 8, true, 12, 0)), Is.False); + Assert.That(combat.PresentsHit(Shot(4, 9, false, 0, 0)), Is.False); + Assert.That(combat.PresentsHit(Shot(4, 9, true, 0, 0)), Is.False); + Assert.That(combat.PresentsHit(Shot(4, 9, true, 0, 12)), Is.True); + } + + private static EveUnitySceneProjection Projection(params EveUnitySceneNode[] children) => + new EveUnitySceneProjection("provider", "surface", "world", "commands", "receipt", "provider", null, + Node("root", "surface", children)); + + private static EveUnitySceneNode Combat() => Node("combat", "combat.presentation", new Dictionary(StringComparer.Ordinal) + { + ["controlledEntityId"] = "run.zone.0.entity.4", + ["controlledEntityIndex"] = "4", + ["selectedTargetEntityId"] = "run.zone.0.entity.9", + ["selectedTargetEntityIndex"] = "9", + ["targetVisible"] = "true", + ["targetHostile"] = "true", + ["contactInformation"] = "0.8", + ["shieldRatio"] = "0.4", + ["hullRatio"] = "0.9", + ["lockProgress"] = "0.6", + ["hitMarkerDurationSeconds"] = "0.25", + ["radialFillMinimum"] = "0.25", + ["radialFillMaximum"] = "0.75" + }); + + private static EveUnityShotReceipt Shot(int source, int target, bool hit, double damage, double shield) => + new EveUnityShotReceipt(Node("shot", "shot.receipt", new Dictionary(StringComparer.Ordinal) + { + ["shotId"] = Guid.NewGuid().ToString("N"), + ["sourceEntityIndex"] = source.ToString(), + ["targetEntityIndex"] = target.ToString(), + ["hit"] = hit.ToString(), + ["appliedDamage"] = damage.ToString(System.Globalization.CultureInfo.InvariantCulture), + ["shieldAbsorbedDamage"] = shield.ToString(System.Globalization.CultureInfo.InvariantCulture) + })); + + private static EveUnitySceneNode Node(string id, string kind, params EveUnitySceneNode[] children) => + Node(id, kind, Empty(), children); + + private static EveUnitySceneNode Node(string id, string kind, IReadOnlyDictionary props, params EveUnitySceneNode[] children) => + new EveUnitySceneNode(id, kind, "scene-node", props, Empty(), Empty(), 0, 0, + Array.Empty(), null, children); + + private static IReadOnlyDictionary Empty() => + new Dictionary(StringComparer.Ordinal); + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs.meta b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs.meta new file mode 100644 index 0000000..6a4265c --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 30c8f313c1ea4b43858ed212909b7ce9 diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 6b3d0ff..4fe1fe4 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -978,6 +978,48 @@ namespace GameCult.Eve.UnityScene.Tests } } + [Test] + public void PlayableWorldInputDriverUsesAdvertisedPrimaryBinding() + { + var capability = new EveInputCapabilityDocument + { + Actions = new[] + { + new EveInputActionDocument + { + ActionId = "weapon-group.0.fire", + Availability = "available" + } + }, + DefaultProfiles = new[] + { + new EveInputProfileDocument + { + ProfileId = "keyboard-mouse", + DeviceClass = "keyboard-mouse", + Bindings = new[] + { + new EveInputBindingDocument + { + ActionId = "weapon-group.0.fire", + Gesture = new EveInputGestureDocument + { + Controls = new[] { "mouse.primary" } + }, + ActionBar = true + } + } + } + } + }; + + Assert.That( + EveUnityPlayableWorldInputDriver.ResolvePrimaryActionId(capability), + Is.EqualTo("weapon-group.0.fire")); + capability.Actions[0].Availability = "unavailable"; + Assert.That(EveUnityPlayableWorldInputDriver.ResolvePrimaryActionId(capability), Is.Empty); + } + [Test] public void PlayableWorldCameraRigFollowsAdvertisedPlayerEntityWithoutProviderTypes() { diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotReceiptPresenterTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotReceiptPresenterTests.cs index 98beaa5..02920dd 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotReceiptPresenterTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotReceiptPresenterTests.cs @@ -34,6 +34,8 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(emitted[0].ItemKey, Is.EqualTo("test-cannon")); Assert.That(emitted[0].ImpactKind, Is.EqualTo("shield")); Assert.That(emitted[0].PresentationIntensity, Is.EqualTo(9)); + Assert.That(emitted[0].AppliedDamage, Is.EqualTo(12)); + Assert.That(emitted[0].ShieldAbsorbedDamage, Is.EqualTo(8)); Assert.That(presenter.Apply(next), Is.Zero); } @@ -69,7 +71,9 @@ namespace GameCult.Eve.UnityScene.Tests ["presentationKind"] = "test-laser", ["itemKey"] = "test-cannon", ["impactKind"] = "shield", - ["presentationIntensity"] = "9" + ["presentationIntensity"] = "9", + ["appliedDamage"] = "12", + ["shieldAbsorbedDamage"] = "8" }, Empty(), Empty(), 0, 0, Array.Empty(), null, Array.Empty()); diff --git a/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json b/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json index 122968b..a7a7dd1 100644 --- a/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json +++ b/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json @@ -114,6 +114,8 @@ "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityFeedbackPresenter.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotReceiptPresenter.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs", + "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentation.cs", + "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityProviderRefreshSource.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldPresenter.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs", @@ -121,7 +123,8 @@ "packages/org.gamecult.eve.unity-scene/Runtime/SaiVisualNovelUnitySceneProjectionAdapter.cs", "packages/org.gamecult.eve.unity-scene/Runtime/NornGraphUnitySceneProjectionAdapter.cs", "packages/org.gamecult.eve.unity-scene/Runtime/TeXMathUnitySceneProjectionAdapter.cs", - "packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs" + "packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs", + "packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs" ] } ], diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 7d51868..b2c1856 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.18", + "version": "0.3.19", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 2fe6b32..5c58fdb 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -3,6 +3,7 @@ param( [string] $AetheriaRoot = "E:\Projects\Aetheria", [string] $CultLibRoot = "E:\Projects\CultLib", [string] $EveUnityRoot = "E:\Projects\EveUnity", + [string] $YmirRoot = "E:\Projects\Ymir", [string] $ClientProject = "ReleaseConsumerProject", [int] $Port = 3076, [string] $OutputDirectory = "artifacts\aetheria-daemon", @@ -31,7 +32,7 @@ $statePath = Join-Path $outputRoot "aetheria-witness-state.cc" $daemonProject = Join-Path $AetheriaRoot "Aetheria.State.Daemon\Aetheria.State.Daemon.csproj" $importProject = Join-Path $AetheriaRoot "Aetheria.State.Import\Aetheria.State.Import.csproj" -foreach ($required in @($UnityExe, (Join-Path $repoRoot $projectRoot), $daemonProject, $importProject)) { +foreach ($required in @($UnityExe, (Join-Path $repoRoot $projectRoot), $daemonProject, $importProject, (Join-Path $YmirRoot "src\Ymir.Core\Ymir.Core.csproj"))) { if (-not (Test-Path -LiteralPath $required)) { throw "Required world witness path not found: $required" } } $canonicalWitnessTest = Join-Path $repoRoot "TestProject\Assets\Tests\PlayMode\GenericWorldCaptureTests.cs" @@ -76,7 +77,7 @@ foreach ($ephemeralPath in @( $importArguments = @( "run", "--project", $importProject, - "-p:CultLibRoot=$CultLibRoot", "-p:EveUnityRoot=$EveUnityRoot", + "-p:CultLibRoot=$CultLibRoot", "-p:EveUnityRoot=$EveUnityRoot", "-p:YmirRoot=$YmirRoot", "--", $AetheriaRoot, $statePath ) $importLogPath = Join-Path $outputRoot "aetheria-import.log" @@ -114,7 +115,7 @@ if (-not $SkipAssetBundleBuild) { $daemonArguments = @( "run", "--project", $daemonProject, - "-p:CultLibRoot=$CultLibRoot", "-p:EveUnityRoot=$EveUnityRoot", + "-p:CultLibRoot=$CultLibRoot", "-p:EveUnityRoot=$EveUnityRoot", "-p:YmirRoot=$YmirRoot", "--", "--root", $AetheriaRoot, "--state", $statePath, @@ -135,13 +136,13 @@ Write-Host "Poll: Select-String -Path '$daemonLogPath' -Pattern 'Aetheria client try { $ready = $false - for ($attempt = 0; $attempt -lt 60; $attempt++) { + for ($attempt = 0; $attempt -lt 240; $attempt++) { if ($daemon.HasExited) { throw "Aetheria daemon exited before publishing CultMesh. See $daemonLogPath" } if ((Test-Path $daemonLogPath) -and (Select-String -Path $daemonLogPath -Pattern "Aetheria client CultMesh endpoint: rudp://127.0.0.1:$Port" -Quiet)) { $ready = $true; break } Start-Sleep -Milliseconds 500 } - if (-not $ready) { throw "Aetheria daemon did not open CultMesh port $Port. See $daemonLogPath" } + if (-not $ready) { throw "Aetheria daemon did not open CultMesh port $Port within 120 seconds. See $daemonLogPath" } $env:EVEUNITY_RENDEZVOUS_ENDPOINT = "rudp://127.0.0.1:$Port" $env:EVEUNITY_PROVIDER_ID = "aetheria" @@ -177,6 +178,10 @@ try { } } $facts = Get-Content -LiteralPath $factsPath -Raw | ConvertFrom-Json + if (-not $facts.combatPresentation -or [string]::IsNullOrWhiteSpace($facts.shotId) -or + [double]$facts.lockProgress -le 0.99) { + throw "Live witness did not observe daemon-owned selection, completed lock, and a resolved shot." + } $releaseLock = Get-Content -LiteralPath (Join-Path $repoRoot "ReleaseConsumerProject\Packages\packages-lock.json") -Raw | ConvertFrom-Json $releasedPackageClient = $projectRoot -eq "ReleaseConsumerProject" $sceneLock = $releaseLock.dependencies.'org.gamecult.eve.unity-scene' @@ -265,7 +270,7 @@ try { sizeBytes = $resultsArtifact.Length } ) - authority = "released-generic-runtime-observes-provider-advertisement-assets-command-receipts-and-republished-surface-versions" + authority = "released-generic-runtime-observes-provider-assets-authoritative-combat-receipts-and-camera-channel-separation" } $witness | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $witnessPath -Encoding UTF8 $stateWitnessPath = Join-Path $outputRoot "runtime-witness.$observedCacheState.json" diff --git a/scripts/run-interactive-aetheria.ps1 b/scripts/run-interactive-aetheria.ps1 new file mode 100644 index 0000000..3dae7e9 --- /dev/null +++ b/scripts/run-interactive-aetheria.ps1 @@ -0,0 +1,121 @@ +param( + [string] $UnityExe = "C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Unity.exe", + [string] $AetheriaRoot = "E:\Projects\Aetheria", + [string] $CultLibRoot = "E:\Projects\CultLib-codex-cultmesh-reliability", + [string] $EveUnityRoot = "E:\Projects\EveUnity", + [string] $YmirRoot = "E:\Projects\Ymir", + [int] $Port = 3076, + [string] $OutputDirectory = "artifacts\interactive", + [switch] $SkipBuild +) + +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot +$project = Join-Path $root "ReleaseConsumerProject" +$outputRoot = if ([IO.Path]::IsPathRooted($OutputDirectory)) { $OutputDirectory } else { Join-Path $root $OutputDirectory } +$buildLog = Join-Path $outputRoot "build.log" +$bundleBuildLog = Join-Path $outputRoot "asset-bundle-build.log" +$daemonLog = Join-Path $outputRoot "daemon.log" +$importLog = Join-Path $outputRoot "import.log" +$importErrorLog = Join-Path $outputRoot "import.error.log" +$state = Join-Path $outputRoot "aetheria.cc" +$replica = Join-Path $outputRoot "eve-unity-replica.cc" +$assetCache = Join-Path $outputRoot "asset-cache" +$exe = Join-Path $project "Build\Windows\EveUnity.exe" +$importProject = Join-Path $AetheriaRoot "Aetheria.State.Import\Aetheria.State.Import.csproj" +$daemonProject = Join-Path $AetheriaRoot "Aetheria.State.Daemon\Aetheria.State.Daemon.csproj" + +foreach ($required in @($UnityExe, $project, $importProject, $daemonProject, (Join-Path $YmirRoot "src\Ymir.Core\Ymir.Core.csproj"))) { + if (-not (Test-Path -LiteralPath $required)) { throw "Required interactive witness path not found: $required" } +} +if (Get-NetUDPEndpoint -LocalPort $Port -ErrorAction SilentlyContinue) { + throw "UDP port $Port is already occupied." +} +New-Item -ItemType Directory -Force $outputRoot, $assetCache | Out-Null +foreach ($ephemeralPath in @($state, "$state.records", "$state.cultmesh", $replica, "$replica.records", "$replica.cultmesh")) { + $resolved = [IO.Path]::GetFullPath($ephemeralPath) + if (-not $resolved.StartsWith([IO.Path]::GetFullPath($outputRoot), [StringComparison]::OrdinalIgnoreCase)) { + throw "Interactive cleanup escaped its artifact directory: $resolved" + } + if (Test-Path -LiteralPath $resolved) { Remove-Item -LiteralPath $resolved -Recurse -Force } +} + +if (-not $SkipBuild -or -not (Test-Path $exe)) { + $build = Start-Process $UnityExe -ArgumentList @("-batchmode", "-quit", "-projectPath", $project, "-executeMethod", "GenericEveUnityBuild.BuildWindows", "-logFile", $buildLog) -PassThru -WindowStyle Hidden + Write-Host "Build PID: $($build.Id)" + Write-Host "Build log: $buildLog" + if (-not $build.WaitForExit(300000)) { Stop-Process $build.Id -Force; throw "EveUnity build timed out." } + if ($build.ExitCode -ne 0 -or -not (Test-Path $exe) -or (Select-String $buildLog -Pattern "Scripts have compiler errors|Build completed with a result of 'Failed'" -Quiet)) { Get-Content $buildLog -Tail 120; throw "EveUnity build failed." } +} + +if (-not $SkipBuild) { + $bundleBuilder = Start-Process $UnityExe -ArgumentList @( + "-batchmode", "-quit", "-projectPath", ".", + "-executeMethod", "Aetheria.Editor.EveAssetBundleBuilder.BuildWindows", + "-logFile", $bundleBuildLog + ) -WorkingDirectory $AetheriaRoot -PassThru -WindowStyle Hidden + Write-Host "AssetBundle build PID: $($bundleBuilder.Id)" + Write-Host "AssetBundle build log: $bundleBuildLog" + if (-not $bundleBuilder.WaitForExit(240000)) { Stop-Process $bundleBuilder.Id -Force; throw "Aetheria AssetBundle build timed out." } + if ($bundleBuilder.ExitCode -ne 0) { Get-Content $bundleBuildLog -Tail 120; throw "Aetheria AssetBundle build failed." } +} + +$importArguments = @( + "run", "--project", $importProject, + "-p:CultLibRoot=$CultLibRoot", "-p:EveUnityRoot=$EveUnityRoot", "-p:YmirRoot=$YmirRoot", + "--", $AetheriaRoot, $state +) +$previousErrorActionPreference = $ErrorActionPreference +try { + $ErrorActionPreference = "Continue" + & dotnet @importArguments 1> $importLog 2> $importErrorLog + $importExitCode = $LASTEXITCODE +} +finally { + $ErrorActionPreference = $previousErrorActionPreference +} +if ($importExitCode -ne 0) { throw "Aetheria state import failed. See $importErrorLog" } + +$daemon = Start-Process dotnet -ArgumentList @( + "run", "--project", $daemonProject, + "-p:CultLibRoot=$CultLibRoot", "-p:EveUnityRoot=$EveUnityRoot", "-p:YmirRoot=$YmirRoot", + "--", + "--root", $AetheriaRoot, + "--state", $state, + "--client-cultmesh-host", "127.0.0.1", + "--client-cultmesh-advertise-host", "127.0.0.1", + "--client-cultmesh-port", $Port, + "--tick-interval-ms", 250, + "--fixed-delta-ms", 20, + "--no-odin-announcements" +) -PassThru -WindowStyle Hidden -RedirectStandardOutput $daemonLog -RedirectStandardError "$daemonLog.error" +Write-Host "Daemon PID: $($daemon.Id)" +Write-Host "Daemon log: $daemonLog" +try { + $ready = $false + for ($i=0; $i -lt 240; $i++) { + if ($daemon.HasExited) { throw "Aetheria daemon exited. See $daemonLog" } + if ((Test-Path $daemonLog) -and + (Select-String $daemonLog -Pattern "Aetheria client CultMesh endpoint: rudp://127.0.0.1:$Port" -Quiet)) { + $ready = $true + break + } + Start-Sleep -Milliseconds 500 + } + if (-not $ready) { throw "Aetheria daemon did not open CultMesh port $Port within 120 seconds. See $daemonLog" } + + $env:EVEUNITY_RENDEZVOUS_ENDPOINT = "rudp://127.0.0.1:$Port" + $env:EVEUNITY_PROVIDER_ID = "aetheria" + $env:EVEUNITY_REPLICA_PATH = $replica + $env:EVEUNITY_ASSET_CACHE_PATH = $assetCache + Remove-Item Env:EVEUNITY_SURFACE_ID -ErrorAction SilentlyContinue + $client = Start-Process $exe -ArgumentList "-force-d3d11" -PassThru + Write-Host "Client PID: $($client.Id)" + Write-Host "Asset cache: $assetCache" + Write-Host "Close the EveUnity window when finished." + $client.WaitForExit() +} +finally { + if ($null -ne $daemon -and -not $daemon.HasExited) { Stop-Process $daemon.Id -Force } + Remove-Item Env:EVEUNITY_RENDEZVOUS_ENDPOINT, Env:EVEUNITY_PROVIDER_ENDPOINT, Env:EVEUNITY_PROVIDER_ID, Env:EVEUNITY_SURFACE_ID, Env:EVEUNITY_REPLICA_PATH, Env:EVEUNITY_ASSET_CACHE_PATH -ErrorAction SilentlyContinue +} From 0c817dbffa5ba5fc222862dbe325642368ec5a6f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 03:51:46 +0100 Subject: [PATCH 042/292] Pin released EveUnity combat package --- ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 ++-- scripts/run-aetheria-daemon-world-witness.ps1 | 18 ++++++++++++------ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index f929590..faeed92 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.18", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.19", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 6023605..c6a2342 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -142,7 +142,7 @@ "hash": "d0aa50d31f3b6da96bffc194fdc3aa358f37e332" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.18", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.19", "depth": 0, "source": "git", "dependencies": { @@ -151,7 +151,7 @@ "org.gamecult.eve.surface": "0.2.1", "com.unity.mathematics": "1.3.2" }, - "hash": "ed21479e467faea8ff624f2d2f5a7d2fecf913f4" + "hash": "d9b762f429056676877d00955dc617e4d423b429" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0", diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 5c58fdb..df13657 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "ed21479e467faea8ff624f2d2f5a7d2fecf913f4" +$expectedEveUnityCommit = "d9b762f429056676877d00955dc617e4d423b429" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot $projectRoot = $ClientProject @@ -196,12 +196,18 @@ try { } $finalBodies = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.body -File -Recurse -ErrorAction SilentlyContinue) $finalPartials = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.partial -File -Recurse -ErrorAction SilentlyContinue) - if ($finalBodies.Count -ne 1 -or $finalPartials.Count -ne 0) { - throw "Content promotion is incomplete. bodies=$($finalBodies.Count) partials=$($finalPartials.Count)" + $providerBundlePath = Join-Path $AetheriaRoot "Build\EveAssets\StandaloneWindows64\aetheria-world" + if (-not (Test-Path -LiteralPath $providerBundlePath)) { + throw "Provider-owned Aetheria bundle is missing: $providerBundlePath" } - $bodyHash = (Get-FileHash -LiteralPath $finalBodies[0].FullName -Algorithm SHA256).Hash.ToLowerInvariant() - if ($finalBodies[0].BaseName -ne $bodyHash) { - throw "Promoted content body name does not match its SHA-256. name=$($finalBodies[0].BaseName) hash=$bodyHash" + $bodyHash = (Get-FileHash -LiteralPath $providerBundlePath -Algorithm SHA256).Hash.ToLowerInvariant() + $promotedBundleBodies = @($finalBodies | Where-Object { $_.BaseName -eq $bodyHash }) + if ($promotedBundleBodies.Count -ne 1 -or $finalPartials.Count -ne 0) { + throw "Provider bundle promotion is incomplete. matchingBodies=$($promotedBundleBodies.Count) bodies=$($finalBodies.Count) partials=$($finalPartials.Count) hash=$bodyHash" + } + $promotedBodyHash = (Get-FileHash -LiteralPath $promotedBundleBodies[0].FullName -Algorithm SHA256).Hash.ToLowerInvariant() + if ($promotedBodyHash -ne $bodyHash -or $promotedBundleBodies[0].Length -ne (Get-Item -LiteralPath $providerBundlePath).Length) { + throw "Promoted provider bundle does not match its authoritative body. expected=$bodyHash actual=$promotedBodyHash" } $facts | Add-Member -NotePropertyName releasedPackageClient -NotePropertyValue $releasedPackageClient $facts | Add-Member -NotePropertyName clientProject -NotePropertyValue $projectRoot From e5a43cffcddeaf6a492d8583408baa4172bd5796 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 04:18:13 +0100 Subject: [PATCH 043/292] Prefer provider-authored shot effects --- .../Runtime/EveUnityShotTrajectoryRenderer.cs | 34 +++++--- .../EveUnityShotTrajectoryRendererTests.cs | 85 +++++++++++++++++++ ...veUnityShotTrajectoryRendererTests.cs.meta | 11 +++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 4 files changed, 117 insertions(+), 15 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotTrajectoryRendererTests.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotTrajectoryRendererTests.cs.meta diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs index ecee9ca..2fc1532 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using UnityEngine; #nullable enable @@ -20,6 +21,7 @@ namespace GameCult.Eve.UnityScene private Material? _material; public int ActiveTrajectoryCount => _active.Count; + public int ActiveFallbackTrajectoryCount => _active.Count(trajectory => trajectory.Line != null); public void Bind(EveUnityPlayableWorldClientHost host, IEveUnityGameObjectAssetProvider? assetProvider = null) { @@ -43,19 +45,10 @@ namespace GameCult.Eve.UnityScene if (receipt == null) throw new ArgumentNullException(nameof(receipt)); var shot = new GameObject("Eve shot " + receipt.ShotId); shot.transform.SetParent(transform, false); - var line = shot.AddComponent(); - line.useWorldSpace = true; - line.positionCount = 2; - var intensityScale = Mathf.Clamp(Mathf.Sqrt((float)receipt.PresentationIntensity), 0.5f, 4f); - line.startWidth = Math.Max(0.01f, width * intensityScale); - line.endWidth = Math.Max(0.01f, width * 0.5f * intensityScale); - line.material = SharedMaterial(); - line.startColor = line.endColor = receipt.Hit ? hitColor : missColor; - var origin = Vector(receipt.Origin); - line.SetPosition(0, origin); var endpoint = Vector(receipt.Endpoint); var effectPrefab = ResolveEffectPrefab(receipt); + LineRenderer? line = null; if (effectPrefab != null) { var effect = Instantiate(effectPrefab, shot.transform); @@ -71,10 +64,23 @@ namespace GameCult.Eve.UnityScene lightning.StartAnimation(); } } + else + { + line = shot.AddComponent(); + line.useWorldSpace = true; + line.positionCount = 2; + var intensityScale = Mathf.Clamp(Mathf.Sqrt((float)receipt.PresentationIntensity), 0.5f, 4f); + line.startWidth = Math.Max(0.01f, width * intensityScale); + line.endWidth = Math.Max(0.01f, width * 0.5f * intensityScale); + line.material = SharedMaterial(); + line.startColor = line.endColor = receipt.Hit ? hitColor : missColor; + line.SetPosition(0, origin); + } var impactPrefab = ResolveRolePrefab("effect.impact." + receipt.ImpactKind); var travels = string.Equals(receipt.PresentationKind, "bolt", StringComparison.Ordinal) || string.Equals(receipt.PresentationKind, "guided", StringComparison.Ordinal); - line.SetPosition(1, travels ? origin : endpoint); + if (line != null) + line.SetPosition(1, travels ? origin : endpoint); _active.Add(new ActiveTrajectory( shot, line, @@ -93,7 +99,7 @@ namespace GameCult.Eve.UnityScene { var trajectory = _active[index]; var progress = Mathf.Clamp01((now - trajectory.StartedAt) / trajectory.Duration); - if (trajectory.Travels) + if (trajectory.Travels && trajectory.Line != null) trajectory.Line.SetPosition(1, Vector3.Lerp(trajectory.Origin, trajectory.Endpoint, progress)); if (progress >= 1 && !trajectory.ImpactPresented) { @@ -146,7 +152,7 @@ namespace GameCult.Eve.UnityScene private sealed class ActiveTrajectory { - public ActiveTrajectory(GameObject root, LineRenderer line, Vector3 origin, Vector3 endpoint, float startedAt, float duration, bool travels, GameObject? impactPrefab) + public ActiveTrajectory(GameObject root, LineRenderer? line, Vector3 origin, Vector3 endpoint, float startedAt, float duration, bool travels, GameObject? impactPrefab) { Root = root; Line = line; @@ -159,7 +165,7 @@ namespace GameCult.Eve.UnityScene } public GameObject Root { get; } - public LineRenderer Line { get; } + public LineRenderer? Line { get; } public Vector3 Origin { get; } public Vector3 Endpoint { get; } public float StartedAt { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotTrajectoryRendererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotTrajectoryRendererTests.cs new file mode 100644 index 0000000..8dff40b --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotTrajectoryRendererTests.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using UnityEngine; + +#nullable enable + +namespace GameCult.Eve.UnityScene.Tests +{ + public sealed class EveUnityShotTrajectoryRendererTests + { + [Test] + public void ProviderShotEffectReplacesGenericFallbackLine() + { + var root = new GameObject("shot-renderer-provider-test"); + var prefab = new GameObject("provider-bolt"); + try + { + var host = root.AddComponent(); + var renderer = root.AddComponent(); + renderer.Bind(host, new BoltProvider(prefab)); + + renderer.Present(Shot("provider-shot")); + + Assert.That(renderer.ActiveTrajectoryCount, Is.EqualTo(1)); + Assert.That(renderer.ActiveFallbackTrajectoryCount, Is.Zero); + Assert.That(root.GetComponentsInChildren(), Is.Empty); + Assert.That(root.transform.GetChild(0).Find("provider-bolt(Clone)"), Is.Not.Null); + } + finally + { + UnityEngine.Object.DestroyImmediate(root); + UnityEngine.Object.DestroyImmediate(prefab); + } + } + + [Test] + public void MissingProviderShotEffectUsesGenericFallbackLine() + { + var root = new GameObject("shot-renderer-fallback-test"); + try + { + var host = root.AddComponent(); + var renderer = root.AddComponent(); + renderer.Bind(host, new BoltProvider(null)); + + renderer.Present(Shot("fallback-shot")); + + Assert.That(renderer.ActiveTrajectoryCount, Is.EqualTo(1)); + Assert.That(renderer.ActiveFallbackTrajectoryCount, Is.EqualTo(1)); + Assert.That(root.GetComponentsInChildren(), Has.Length.EqualTo(1)); + } + finally + { + UnityEngine.Object.DestroyImmediate(root); + } + } + + private static EveUnityShotReceipt Shot(string id) => + new EveUnityShotReceipt(new EveUnitySceneNode(id, "shot.receipt", "scene-node", + new Dictionary(StringComparer.Ordinal) + { + ["shotId"] = id, + ["origin"] = "0,0", + ["endpoint"] = "10,0", + ["presentationDuration"] = "0.2", + ["presentationKind"] = "bolt", + ["impactKind"] = "armor", + ["presentationIntensity"] = "85", + ["hit"] = "true" + }, Empty(), Empty(), 0, 0, Array.Empty(), null, + Array.Empty())); + + private static IReadOnlyDictionary Empty() => + new Dictionary(StringComparer.Ordinal); + + private sealed class BoltProvider : IEveUnityGameObjectAssetProvider + { + private readonly GameObject? _prefab; + public BoltProvider(GameObject? prefab) { _prefab = prefab; } + public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) => + asset.EntityKind == "effect.shot.bolt" ? _prefab : null; + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotTrajectoryRendererTests.cs.meta b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotTrajectoryRendererTests.cs.meta new file mode 100644 index 0000000..9cf0c04 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotTrajectoryRendererTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a1d80f8e29164e23ad367feefb67c49e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index b2c1856..14285f7 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.19", + "version": "0.3.20", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From e62e9fc5b2f68900c66ce2a0385bfbc2b0cd17ff Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 04:19:49 +0100 Subject: [PATCH 044/292] Pin provider-authored shot effect release --- ReleaseConsumerProject/Packages/manifest.json | 2 +- ReleaseConsumerProject/Packages/packages-lock.json | 4 ++-- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index faeed92..70bae70 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.19", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.20", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index c6a2342..afdb4ad 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -142,7 +142,7 @@ "hash": "d0aa50d31f3b6da96bffc194fdc3aa358f37e332" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.19", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.20", "depth": 0, "source": "git", "dependencies": { @@ -151,7 +151,7 @@ "org.gamecult.eve.surface": "0.2.1", "com.unity.mathematics": "1.3.2" }, - "hash": "d9b762f429056676877d00955dc617e4d423b429" + "hash": "e5a43cffcddeaf6a492d8583408baa4172bd5796" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0", diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index df13657..f174291 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "d9b762f429056676877d00955dc617e4d423b429" +$expectedEveUnityCommit = "e5a43cffcddeaf6a492d8583408baa4172bd5796" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot $projectRoot = $ClientProject From c662f11618e4dbb2bf36b02c54c2932bba360a4b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 04:30:21 +0100 Subject: [PATCH 045/292] Bind provider impact effect lifetime --- .../Runtime/EveUnityShotTrajectoryRenderer.cs | 2 +- packages/org.gamecult.eve.unity-scene/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs index 2fc1532..4e2ca7f 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs @@ -106,7 +106,7 @@ namespace GameCult.Eve.UnityScene trajectory.ImpactPresented = true; if (trajectory.ImpactPrefab != null) { - var impact = Instantiate(trajectory.ImpactPrefab, transform); + var impact = Instantiate(trajectory.ImpactPrefab, trajectory.Root.transform); impact.transform.position = trajectory.Endpoint; } } diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 14285f7..bb3058d 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.20", + "version": "0.3.21", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 466cd10c719bc76ae5a7f7513747ea96057fbf49 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 04:58:50 +0100 Subject: [PATCH 046/292] Pin released native world witness --- .../Assets/DefaultVolumeProfile.asset | 15 + .../Assets/DefaultVolumeProfile.asset.meta | 8 + ReleaseConsumerProject/Assets/Editor.meta | 3 + .../Editor/GenericEveUnityRenderPipeline.cs | 32 ++ .../GenericEveUnityRenderPipeline.cs.meta | 2 + .../GenericEveUnityRenderPipeline.asset | 143 ++++++ .../GenericEveUnityRenderPipeline.asset.meta | 8 + .../Assets/GenericEveUnityRenderer.asset | 55 +++ .../Assets/GenericEveUnityRenderer.asset.meta | 8 + .../PlayMode/GenericWorldCaptureTests.cs | 14 + ...niversalRenderPipelineGlobalSettings.asset | 436 ++++++++++++++++++ ...salRenderPipelineGlobalSettings.asset.meta | 8 + ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 +- .../ProjectSettings/GraphicsSettings.asset | 5 +- .../ProjectSettings/QualitySettings.asset | 2 +- .../PlayMode/GenericWorldCaptureTests.cs | 14 + scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 18 files changed, 754 insertions(+), 7 deletions(-) create mode 100644 ReleaseConsumerProject/Assets/DefaultVolumeProfile.asset create mode 100644 ReleaseConsumerProject/Assets/DefaultVolumeProfile.asset.meta create mode 100644 ReleaseConsumerProject/Assets/Editor.meta create mode 100644 ReleaseConsumerProject/Assets/Editor/GenericEveUnityRenderPipeline.cs create mode 100644 ReleaseConsumerProject/Assets/Editor/GenericEveUnityRenderPipeline.cs.meta create mode 100644 ReleaseConsumerProject/Assets/GenericEveUnityRenderPipeline.asset create mode 100644 ReleaseConsumerProject/Assets/GenericEveUnityRenderPipeline.asset.meta create mode 100644 ReleaseConsumerProject/Assets/GenericEveUnityRenderer.asset create mode 100644 ReleaseConsumerProject/Assets/GenericEveUnityRenderer.asset.meta create mode 100644 ReleaseConsumerProject/Assets/UniversalRenderPipelineGlobalSettings.asset create mode 100644 ReleaseConsumerProject/Assets/UniversalRenderPipelineGlobalSettings.asset.meta diff --git a/ReleaseConsumerProject/Assets/DefaultVolumeProfile.asset b/ReleaseConsumerProject/Assets/DefaultVolumeProfile.asset new file mode 100644 index 0000000..5393bfe --- /dev/null +++ b/ReleaseConsumerProject/Assets/DefaultVolumeProfile.asset @@ -0,0 +1,15 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d7fd9488000d3734a9e00ee676215985, type: 3} + m_Name: DefaultVolumeProfile + m_EditorClassIdentifier: Unity.RenderPipelines.Core.Runtime::UnityEngine.Rendering.VolumeProfile + components: [] diff --git a/ReleaseConsumerProject/Assets/DefaultVolumeProfile.asset.meta b/ReleaseConsumerProject/Assets/DefaultVolumeProfile.asset.meta new file mode 100644 index 0000000..408ba9a --- /dev/null +++ b/ReleaseConsumerProject/Assets/DefaultVolumeProfile.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5b8b49b0bf6036a4a823021e69f80309 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/ReleaseConsumerProject/Assets/Editor.meta b/ReleaseConsumerProject/Assets/Editor.meta new file mode 100644 index 0000000..1d8dc57 --- /dev/null +++ b/ReleaseConsumerProject/Assets/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 42bb4a36b1c94282ba7a4cad2b073bbc +folderAsset: yes diff --git a/ReleaseConsumerProject/Assets/Editor/GenericEveUnityRenderPipeline.cs b/ReleaseConsumerProject/Assets/Editor/GenericEveUnityRenderPipeline.cs new file mode 100644 index 0000000..b41e95e --- /dev/null +++ b/ReleaseConsumerProject/Assets/Editor/GenericEveUnityRenderPipeline.cs @@ -0,0 +1,32 @@ +using UnityEditor; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.Rendering.Universal; + +public static class GenericEveUnityRenderPipeline +{ + private const string RendererPath = "Assets/GenericEveUnityRenderer.asset"; + private const string PipelinePath = "Assets/GenericEveUnityRenderPipeline.asset"; + + public static void Configure() + { + var renderer = AssetDatabase.LoadAssetAtPath(RendererPath); + if (renderer == null) + { + renderer = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(renderer, RendererPath); + } + + var pipeline = AssetDatabase.LoadAssetAtPath(PipelinePath); + if (pipeline == null) + { + pipeline = UniversalRenderPipelineAsset.Create(renderer); + AssetDatabase.CreateAsset(pipeline, PipelinePath); + } + + GraphicsSettings.defaultRenderPipeline = pipeline; + QualitySettings.renderPipeline = pipeline; + EditorUtility.SetDirty(pipeline); + AssetDatabase.SaveAssets(); + } +} diff --git a/ReleaseConsumerProject/Assets/Editor/GenericEveUnityRenderPipeline.cs.meta b/ReleaseConsumerProject/Assets/Editor/GenericEveUnityRenderPipeline.cs.meta new file mode 100644 index 0000000..4b499ca --- /dev/null +++ b/ReleaseConsumerProject/Assets/Editor/GenericEveUnityRenderPipeline.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c07b26ad876001846ba5ea9d3743352e \ No newline at end of file diff --git a/ReleaseConsumerProject/Assets/GenericEveUnityRenderPipeline.asset b/ReleaseConsumerProject/Assets/GenericEveUnityRenderPipeline.asset new file mode 100644 index 0000000..e93f394 --- /dev/null +++ b/ReleaseConsumerProject/Assets/GenericEveUnityRenderPipeline.asset @@ -0,0 +1,143 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bf2edee5c58d82540a51f03df9d42094, type: 3} + m_Name: GenericEveUnityRenderPipeline + m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalRenderPipelineAsset + k_AssetVersion: 13 + k_AssetPreviousVersion: 13 + m_RendererType: 1 + m_RendererData: {fileID: 0} + m_RendererDataList: + - {fileID: 11400000, guid: cbe5d2831ab649e42b9fb94644e029a9, type: 2} + m_DefaultRendererIndex: 0 + m_RequireDepthTexture: 0 + m_RequireOpaqueTexture: 0 + m_OpaqueDownsampling: 1 + m_SupportsTerrainHoles: 1 + m_SupportsHDR: 1 + m_HDRColorBufferPrecision: 0 + m_MSAA: 1 + m_RenderScale: 1 + m_UpscalingFilter: 0 + m_FsrOverrideSharpness: 0 + m_FsrSharpness: 0.92 + m_EnableLODCrossFade: 1 + m_LODCrossFadeDitheringType: 1 + m_ShEvalMode: 0 + m_LightProbeSystem: 0 + m_ProbeVolumeMemoryBudget: 1024 + m_ProbeVolumeBlendingMemoryBudget: 256 + m_SupportProbeVolumeGPUStreaming: 0 + m_SupportProbeVolumeDiskStreaming: 0 + m_SupportProbeVolumeScenarios: 0 + m_SupportProbeVolumeScenarioBlending: 0 + m_ProbeVolumeSHBands: 1 + m_MainLightRenderingMode: 1 + m_MainLightShadowsSupported: 1 + m_MainLightShadowmapResolution: 2048 + m_AdditionalLightsRenderingMode: 1 + m_AdditionalLightsPerObjectLimit: 4 + m_AdditionalLightShadowsSupported: 0 + m_AdditionalLightsShadowmapResolution: 2048 + m_AdditionalLightsShadowResolutionTierLow: 256 + m_AdditionalLightsShadowResolutionTierMedium: 512 + m_AdditionalLightsShadowResolutionTierHigh: 1024 + m_ReflectionProbeBlending: 0 + m_ReflectionProbeBoxProjection: 0 + m_ReflectionProbeAtlas: 1 + m_ShadowDistance: 50 + m_ShadowCascadeCount: 1 + m_Cascade2Split: 0.25 + m_Cascade3Split: {x: 0.1, y: 0.3} + m_Cascade4Split: {x: 0.067, y: 0.2, z: 0.467} + m_CascadeBorder: 0.2 + m_ShadowDepthBias: 1 + m_ShadowNormalBias: 1 + m_AnyShadowsSupported: 1 + m_SoftShadowsSupported: 0 + m_ConservativeEnclosingSphere: 1 + m_NumIterationsEnclosingSphere: 64 + m_SoftShadowQuality: 2 + m_AdditionalLightsCookieResolution: 2048 + m_AdditionalLightsCookieFormat: 3 + m_UseSRPBatcher: 1 + m_SupportsDynamicBatching: 0 + m_MixedLightingSupported: 1 + m_SupportsLightCookies: 1 + m_SupportsLightLayers: 0 + m_DebugLevel: 0 + m_StoreActionsOptimization: 0 + m_UseAdaptivePerformance: 1 + m_ColorGradingMode: 0 + m_ColorGradingLutSize: 32 + m_AllowPostProcessAlphaOutput: 0 + m_UseFastSRGBLinearConversion: 0 + m_SupportDataDrivenLensFlare: 1 + m_SupportScreenSpaceLensFlare: 1 + m_GPUResidentDrawerMode: 0 + m_SmallMeshScreenPercentage: 0 + m_GPUResidentDrawerEnableOcclusionCullingInCameras: 0 + m_ShadowType: 1 + m_LocalShadowsSupported: 0 + m_LocalShadowsAtlasResolution: 256 + m_MaxPixelLights: 0 + m_ShadowAtlasResolution: 256 + m_VolumeFrameworkUpdateMode: 0 + m_VolumeProfile: {fileID: 0} + apvScenesData: + obsoleteSceneBounds: + m_Keys: [] + m_Values: [] + obsoleteHasProbeVolumes: + m_Keys: [] + m_Values: + m_PrefilteringModeMainLightShadows: 1 + m_PrefilteringModeAdditionalLight: 4 + m_PrefilteringModeAdditionalLightShadows: 1 + m_PrefilterXRKeywords: 0 + m_PrefilteringModeForwardPlus: 1 + m_PrefilteringModeDeferredRendering: 1 + m_PrefilteringModeScreenSpaceOcclusion: 1 + m_PrefilterDebugKeywords: 0 + m_PrefilterWriteRenderingLayers: 0 + m_PrefilterHDROutput: 0 + m_PrefilterAlphaOutput: 0 + m_PrefilterSSAODepthNormals: 0 + m_PrefilterSSAOSourceDepthLow: 0 + m_PrefilterSSAOSourceDepthMedium: 0 + m_PrefilterSSAOSourceDepthHigh: 0 + m_PrefilterSSAOInterleaved: 0 + m_PrefilterSSAOBlueNoise: 0 + m_PrefilterSSAOSampleCountLow: 0 + m_PrefilterSSAOSampleCountMedium: 0 + m_PrefilterSSAOSampleCountHigh: 0 + m_PrefilterDBufferMRT1: 0 + m_PrefilterDBufferMRT2: 0 + m_PrefilterDBufferMRT3: 0 + m_PrefilterSoftShadowsQualityLow: 0 + m_PrefilterSoftShadowsQualityMedium: 0 + m_PrefilterSoftShadowsQualityHigh: 0 + m_PrefilterSoftShadows: 0 + m_PrefilterScreenCoord: 0 + m_PrefilterScreenSpaceIrradiance: 0 + m_PrefilterNativeRenderPass: 0 + m_PrefilterUseLegacyLightmaps: 0 + m_PrefilterBicubicLightmapSampling: 0 + m_PrefilterReflectionProbeRotation: 0 + m_PrefilterReflectionProbeBlending: 0 + m_PrefilterReflectionProbeBoxProjection: 0 + m_PrefilterReflectionProbeAtlas: 0 + m_ShaderVariantLogLevel: 0 + m_ShadowCascades: 0 + m_Textures: + blueNoise64LTex: {fileID: 2800000, guid: e3d24661c1e055f45a7560c033dbb837, type: 3} + bayerMatrixTex: {fileID: 2800000, guid: f9ee4ed84c1d10c49aabb9b210b0fc44, type: 3} diff --git a/ReleaseConsumerProject/Assets/GenericEveUnityRenderPipeline.asset.meta b/ReleaseConsumerProject/Assets/GenericEveUnityRenderPipeline.asset.meta new file mode 100644 index 0000000..266335c --- /dev/null +++ b/ReleaseConsumerProject/Assets/GenericEveUnityRenderPipeline.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 43f32b840bd83ce47af920dadd59d4b3 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/ReleaseConsumerProject/Assets/GenericEveUnityRenderer.asset b/ReleaseConsumerProject/Assets/GenericEveUnityRenderer.asset new file mode 100644 index 0000000..7b97449 --- /dev/null +++ b/ReleaseConsumerProject/Assets/GenericEveUnityRenderer.asset @@ -0,0 +1,55 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: de640fe3d0db1804a85f9fc8f5cadab6, type: 3} + m_Name: GenericEveUnityRenderer + m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalRendererData + debugShaders: + debugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7, type: 3} + hdrDebugViewPS: {fileID: 4800000, guid: 573620ae32aec764abd4d728906d2587, type: 3} + probeVolumeSamplingDebugComputeShader: {fileID: 7200000, guid: 53626a513ea68ce47b59dc1299fe3959, type: 3} + probeVolumeResources: + probeVolumeDebugShader: {fileID: 0} + probeVolumeFragmentationDebugShader: {fileID: 0} + probeVolumeOffsetDebugShader: {fileID: 0} + probeVolumeSamplingDebugShader: {fileID: 0} + probeSamplingDebugMesh: {fileID: 0} + probeSamplingDebugTexture: {fileID: 0} + probeVolumeBlendStatesCS: {fileID: 0} + m_RendererFeatures: [] + m_RendererFeatureMap: + m_UseNativeRenderPass: 0 + postProcessData: {fileID: 0} + m_AssetVersion: 3 + m_PrepassLayerMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_OpaqueLayerMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_TransparentLayerMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_DefaultStencilState: + overrideStencilState: 0 + stencilReference: 0 + stencilCompareFunction: 8 + passOperation: 2 + failOperation: 0 + zFailOperation: 0 + m_ShadowTransparentReceive: 1 + m_RenderingMode: 0 + m_DepthPrimingMode: 0 + m_CopyDepthMode: 1 + m_DepthAttachmentFormat: 0 + m_DepthTextureFormat: 0 + m_AccurateGbufferNormals: 0 + m_IntermediateTextureMode: 1 diff --git a/ReleaseConsumerProject/Assets/GenericEveUnityRenderer.asset.meta b/ReleaseConsumerProject/Assets/GenericEveUnityRenderer.asset.meta new file mode 100644 index 0000000..5837417 --- /dev/null +++ b/ReleaseConsumerProject/Assets/GenericEveUnityRenderer.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cbe5d2831ab649e42b9fb94644e029a9 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 088d8a4..25b2fa7 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -418,6 +418,20 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var playerMarker = FindMarker(root, playerId); var playerRenderers = playerMarker.GetComponentsInChildren(); Assert.That(playerRenderers, Is.Not.Empty, "The provider-authored player prefab has no renderable visual."); + Assert.That(playerMarker.GetComponentsInChildren(), Is.Empty, + "The provider-authored player presentation retained gameplay colliders."); + Assert.That(playerMarker.GetComponentsInChildren(), Is.Empty, + "The provider-authored player presentation retained gameplay rigid bodies."); + foreach (var renderer in playerRenderers) + foreach (var material in renderer.sharedMaterials) + { + Assert.That(material, Is.Not.Null, $"Provider renderer {renderer.name} has a null material."); + Assert.That(material.shader, Is.Not.Null, $"Provider material {material.name} has no shader."); + Assert.That(material.shader.isSupported, Is.True, + $"Provider material {material.name} uses unsupported shader {material.shader.name}."); + Assert.That(material.shader.name, Does.Not.Contain("FallbackError"), + $"Provider material {material.name} resolved to Unity's error shader."); + } Assert.That(provider.TryGetRenderChannelLayer("map", out var mapLayer), Is.True); var mapRenderers = new List(); foreach (var renderer in root.GetComponentsInChildren(includeInactive: true)) diff --git a/ReleaseConsumerProject/Assets/UniversalRenderPipelineGlobalSettings.asset b/ReleaseConsumerProject/Assets/UniversalRenderPipelineGlobalSettings.asset new file mode 100644 index 0000000..0c4c2e0 --- /dev/null +++ b/ReleaseConsumerProject/Assets/UniversalRenderPipelineGlobalSettings.asset @@ -0,0 +1,436 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2ec995e51a6e251468d2a3fd8a686257, type: 3} + m_Name: UniversalRenderPipelineGlobalSettings + m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalRenderPipelineGlobalSettings + m_ShaderStrippingSetting: + m_Version: 0 + m_ExportShaderVariants: 1 + m_ShaderVariantLogLevel: 0 + m_StripRuntimeDebugShaders: 1 + m_URPShaderStrippingSetting: + m_Version: 0 + m_StripUnusedPostProcessingVariants: 0 + m_StripUnusedVariants: 1 + m_StripScreenCoordOverrideVariants: 1 + m_ShaderVariantLogLevel: 0 + m_ExportShaderVariants: 1 + m_StripDebugVariants: 1 + m_StripUnusedPostProcessingVariants: 0 + m_StripUnusedVariants: 1 + m_StripScreenCoordOverrideVariants: 1 + supportRuntimeDebugDisplay: 0 + m_Settings: + m_SettingsList: + m_List: + - rid: 1680211313492492288 + - rid: 1680211313492492289 + - rid: 1680211313492492290 + - rid: 1680211313492492291 + - rid: 1680211313492492292 + - rid: 1680211313492492293 + - rid: 1680211313492492294 + - rid: 1680211313492492295 + - rid: 1680211313492492296 + - rid: 1680211313492492297 + - rid: 1680211313492492298 + - rid: 1680211313492492299 + - rid: 1680211313492492300 + - rid: 1680211313492492301 + - rid: 1680211313492492302 + - rid: 1680211313492492303 + - rid: 1680211313492492304 + - rid: 1680211313492492305 + - rid: 1680211313492492306 + - rid: 1680211313492492307 + - rid: 1680211313492492308 + - rid: 1680211313492492309 + - rid: 1680211313492492310 + - rid: 1680211313492492311 + - rid: 1680211313492492312 + - rid: 1680211313492492313 + - rid: 1680211313492492314 + - rid: 1680211313492492315 + - rid: 1680211313492492316 + - rid: 1680211313492492317 + - rid: 1680211313492492318 + - rid: 1680211313492492319 + - rid: 1680211313492492320 + - rid: 1680211313492492321 + - rid: 1680211313492492322 + m_RuntimeSettings: + m_List: [] + m_AssetVersion: 10 + m_ObsoleteDefaultVolumeProfile: {fileID: 0} + m_RenderingLayerNames: + - Default + m_ValidRenderingLayers: 0 + lightLayerName0: + lightLayerName1: + lightLayerName2: + lightLayerName3: + lightLayerName4: + lightLayerName5: + lightLayerName6: + lightLayerName7: + apvScenesData: + obsoleteSceneBounds: + m_Keys: [] + m_Values: [] + obsoleteHasProbeVolumes: + m_Keys: [] + m_Values: + references: + version: 2 + RefIds: + - rid: 1680211313492492288 + type: {class: RayTracingRenderPipelineResources, ns: UnityEngine.Rendering.UnifiedRayTracing, asm: Unity.UnifiedRayTracing.Runtime} + data: + m_Version: 1 + m_GeometryPoolKernels: {fileID: 7200000, guid: 98e3d58cae7210c4786f67f504c9e899, type: 3} + m_CopyBuffer: {fileID: 7200000, guid: 1b95b5dcf48d1914c9e1e7405c7660e3, type: 3} + m_CopyPositions: {fileID: 7200000, guid: 1ad53a96b58d3c3488dde4f14db1aaeb, type: 3} + m_BitHistogram: {fileID: 7200000, guid: 8670f7ce4b60cef43bed36148aa1b0a2, type: 3} + m_BlockReducePart: {fileID: 7200000, guid: 4e034cc8ea2635c4e9f063e5ddc7ea7a, type: 3} + m_BlockScan: {fileID: 7200000, guid: 4d6d5de35fa45ef4a92119397a045cc9, type: 3} + m_BuildHlbvh: {fileID: 7200000, guid: 2d70cd6be91bd7843a39a54b51c15b13, type: 3} + m_RestructureBvh: {fileID: 7200000, guid: 56641cb88dcb31a4398a4997ef7a7a8c, type: 3} + m_Scatter: {fileID: 7200000, guid: a2eaeefdac4637a44b734e85b7be9186, type: 3} + - rid: 1680211313492492289 + type: {class: UniversalRenderPipelineRuntimeShaders, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + m_Version: 0 + m_FallbackErrorShader: {fileID: 4800000, guid: e6e9a19c3678ded42a3bc431ebef7dbd, type: 3} + m_BlitHDROverlay: {fileID: 4800000, guid: a89bee29cffa951418fc1e2da94d1959, type: 3} + m_CoreBlitPS: {fileID: 4800000, guid: 93446b5c5339d4f00b85c159e1159b7c, type: 3} + m_CoreBlitColorAndDepthPS: {fileID: 4800000, guid: d104b2fc1ca6445babb8e90b0758136b, type: 3} + m_SamplingPS: {fileID: 4800000, guid: 04c410c9937594faa893a11dceb85f7e, type: 3} + m_TerrainDetailLit: {fileID: 0} + m_TerrainDetailGrassBillboard: {fileID: 0} + m_TerrainDetailGrass: {fileID: 0} + - rid: 1680211313492492290 + type: {class: URPTerrainShaderSetting, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + m_Version: 0 + m_IncludeTerrainShaders: 1 + - rid: 1680211313492492291 + type: {class: UniversalRenderPipelineRuntimeTerrainShaders, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + m_Version: 0 + m_TerrainDetailLit: {fileID: 4800000, guid: f6783ab646d374f94b199774402a5144, type: 3} + m_TerrainDetailGrassBillboard: {fileID: 4800000, guid: 29868e73b638e48ca99a19ea58c48d90, type: 3} + m_TerrainDetailGrass: {fileID: 4800000, guid: e507fdfead5ca47e8b9a768b51c291a1, type: 3} + - rid: 1680211313492492292 + type: {class: UniversalRendererResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + m_Version: 0 + m_CopyDepthPS: {fileID: 4800000, guid: d6dae50ee9e1bfa4db75f19f99355220, type: 3} + m_CameraMotionVector: {fileID: 4800000, guid: c56b7e0d4c7cb484e959caeeedae9bbf, type: 3} + m_StencilDeferredPS: {fileID: 4800000, guid: e9155b26e1bc55942a41e518703fe304, type: 3} + m_ClusterDeferred: {fileID: 4800000, guid: 222cce62363a44a380c36bf03b392608, type: 3} + m_StencilDitherMaskSeedPS: {fileID: 4800000, guid: 8c3ee818f2efa514c889881ccb2e95a2, type: 3} + m_DBufferClear: {fileID: 4800000, guid: f056d8bd2a1c7e44e9729144b4c70395, type: 3} + - rid: 1680211313492492293 + type: {class: UniversalRenderPipelineDebugShaders, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + m_DebugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7, type: 3} + m_HdrDebugViewPS: {fileID: 4800000, guid: 573620ae32aec764abd4d728906d2587, type: 3} + m_ProbeVolumeSamplingDebugComputeShader: {fileID: 7200000, guid: 53626a513ea68ce47b59dc1299fe3959, type: 3} + - rid: 1680211313492492294 + type: {class: PostProcessData/ShaderResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + stopNanPS: {fileID: 4800000, guid: 1121bb4e615ca3c48b214e79e841e823, type: 3} + subpixelMorphologicalAntialiasingPS: {fileID: 4800000, guid: 63eaba0ebfb82cc43bde059b4a8c65f6, type: 3} + gaussianDepthOfFieldPS: {fileID: 4800000, guid: 5e7134d6e63e0bc47a1dd2669cedb379, type: 3} + bokehDepthOfFieldPS: {fileID: 4800000, guid: 2aed67ad60045d54ba3a00c91e2d2631, type: 3} + cameraMotionBlurPS: {fileID: 4800000, guid: 1edcd131364091c46a17cbff0b1de97a, type: 3} + paniniProjectionPS: {fileID: 4800000, guid: a15b78cf8ca26ca4fb2090293153c62c, type: 3} + lutBuilderLdrPS: {fileID: 4800000, guid: 65df88701913c224d95fc554db28381a, type: 3} + lutBuilderHdrPS: {fileID: 4800000, guid: ec9fec698a3456d4fb18cf8bacb7a2bc, type: 3} + bloomPS: {fileID: 4800000, guid: 5f1864addb451f54bae8c86d230f736e, type: 3} + temporalAntialiasingPS: {fileID: 4800000, guid: 9c70c1a35ff15f340b38ea84842358bf, type: 3} + LensFlareDataDrivenPS: {fileID: 4800000, guid: 6cda457ac28612740adb23da5d39ea92, type: 3} + LensFlareScreenSpacePS: {fileID: 4800000, guid: 701880fecb344ea4c9cd0db3407ab287, type: 3} + scalingSetupPS: {fileID: 4800000, guid: e8ee25143a34b8c4388709ea947055d1, type: 3} + easuPS: {fileID: 4800000, guid: 562b7ae4f629f144aa97780546fce7c6, type: 3} + uberPostPS: {fileID: 4800000, guid: e7857e9d0c934dc4f83f270f8447b006, type: 3} + finalPostPassPS: {fileID: 4800000, guid: c49e63ed1bbcb334780a3bd19dfed403, type: 3} + m_ShaderResourcesVersion: 0 + - rid: 1680211313492492295 + type: {class: UniversalRenderPipelineEditorShaders, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + m_AutodeskInteractive: {fileID: 4800000, guid: 0e9d5a909a1f7e84882a534d0d11e49f, type: 3} + m_AutodeskInteractiveTransparent: {fileID: 4800000, guid: 5c81372d981403744adbdda4433c9c11, type: 3} + m_AutodeskInteractiveMasked: {fileID: 4800000, guid: 80aa867ac363ac043847b06ad71604cd, type: 3} + m_DefaultSpeedTree7Shader: {fileID: 4800000, guid: 0f4122b9a743b744abe2fb6a0a88868b, type: 3} + m_DefaultSpeedTree8Shader: {fileID: -6465566751694194690, guid: 9920c1f1781549a46ba081a2a15a16ec, type: 3} + m_DefaultSpeedTree9Shader: {fileID: -6465566751694194690, guid: cbd3e1cc4ae141c42a30e33b4d666a61, type: 3} + - rid: 1680211313492492296 + type: {class: RenderGraphSettings, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + m_Version: 0 + - rid: 1680211313492492297 + type: {class: ScreenSpaceAmbientOcclusionDynamicResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + m_BlueNoise256Textures: + - {fileID: 2800000, guid: 36f118343fc974119bee3d09e2111500, type: 3} + - {fileID: 2800000, guid: 4b7b083e6b6734e8bb2838b0b50a0bc8, type: 3} + - {fileID: 2800000, guid: c06cc21c692f94f5fb5206247191eeee, type: 3} + - {fileID: 2800000, guid: cb76dd40fa7654f9587f6a344f125c9a, type: 3} + - {fileID: 2800000, guid: e32226222ff144b24bf3a5a451de54bc, type: 3} + - {fileID: 2800000, guid: 3302065f671a8450b82c9ddf07426f3a, type: 3} + - {fileID: 2800000, guid: 56a77a3e8d64f47b6afe9e3c95cb57d5, type: 3} + m_Version: 0 + - rid: 1680211313492492298 + type: {class: OnTilePostProcessResource, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + m_Version: 0 + m_UberPostShader: {fileID: 4800000, guid: fe4f13c1004a07d4ea1e30bfd0326d9e, type: 3} + - rid: 1680211313492492299 + type: {class: UniversalRenderPipelineRuntimeXRResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + m_xrOcclusionMeshPS: {fileID: 4800000, guid: 4431b1f1f743fbf4eb310a967890cbea, type: 3} + m_xrMirrorViewPS: {fileID: 4800000, guid: d5a307c014552314b9f560906d708772, type: 3} + m_xrMotionVector: {fileID: 4800000, guid: f89aac1e4f84468418fe30e611dff395, type: 3} + - rid: 1680211313492492300 + type: {class: URPReflectionProbeSettings, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Universal.Runtime} + data: + version: 1 + useReflectionProbeRotation: 1 + - rid: 1680211313492492301 + type: {class: UniversalRenderPipelineRuntimeTextures, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + m_Version: 1 + m_BlueNoise64LTex: {fileID: 2800000, guid: e3d24661c1e055f45a7560c033dbb837, type: 3} + m_BayerMatrixTex: {fileID: 2800000, guid: f9ee4ed84c1d10c49aabb9b210b0fc44, type: 3} + m_DebugFontTex: {fileID: 2800000, guid: 26a413214480ef144b2915d6ff4d0beb, type: 3} + - rid: 1680211313492492302 + type: {class: ScreenSpaceAmbientOcclusionPersistentResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + m_Shader: {fileID: 4800000, guid: 0849e84e3d62649e8882e9d6f056a017, type: 3} + m_Version: 0 + - rid: 1680211313492492303 + type: {class: UniversalRenderPipelineEditorMaterials, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + m_DefaultMaterial: {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} + m_DefaultParticleMaterial: {fileID: 2100000, guid: e823cd5b5d27c0f4b8256e7c12ee3e6d, type: 2} + m_DefaultLineMaterial: {fileID: 2100000, guid: e823cd5b5d27c0f4b8256e7c12ee3e6d, type: 2} + m_DefaultTerrainMaterial: {fileID: 2100000, guid: 594ea882c5a793440b60ff72d896021e, type: 2} + m_DefaultDecalMaterial: {fileID: 2100000, guid: 31d0dcc6f2dd4e4408d18036a2c93862, type: 2} + m_DefaultSpriteMaterial: {fileID: 2100000, guid: 9dfc825aed78fcd4ba02077103263b40, type: 2} + - rid: 1680211313492492304 + type: {class: PostProcessData/TextureResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + blueNoise16LTex: + - {fileID: 2800000, guid: 81200413a40918d4d8702e94db29911c, type: 3} + - {fileID: 2800000, guid: d50c5e07c9911a74982bddf7f3075e7b, type: 3} + - {fileID: 2800000, guid: 1134690bf9216164dbc75050e35b7900, type: 3} + - {fileID: 2800000, guid: 7ce2118f74614a94aa8a0cdf2e6062c3, type: 3} + - {fileID: 2800000, guid: 2ca97df9d1801e84a8a8f2c53cb744f0, type: 3} + - {fileID: 2800000, guid: e63eef8f54aa9dc4da9a5ac094b503b5, type: 3} + - {fileID: 2800000, guid: 39451254daebd6d40b52899c1f1c0c1b, type: 3} + - {fileID: 2800000, guid: c94ad916058dff743b0f1c969ddbe660, type: 3} + - {fileID: 2800000, guid: ed5ea7ce59ca8ec4f9f14bf470a30f35, type: 3} + - {fileID: 2800000, guid: 071e954febf155243a6c81e48f452644, type: 3} + - {fileID: 2800000, guid: 96aaab9cc247d0b4c98132159688c1af, type: 3} + - {fileID: 2800000, guid: fc3fa8f108657e14486697c9a84ccfc5, type: 3} + - {fileID: 2800000, guid: bfed3e498947fcb4890b7f40f54d85b9, type: 3} + - {fileID: 2800000, guid: d512512f4af60a442ab3458489412954, type: 3} + - {fileID: 2800000, guid: 47a45908f6db0cb44a0d5e961143afec, type: 3} + - {fileID: 2800000, guid: 4dcc0502f8586f941b5c4a66717205e8, type: 3} + - {fileID: 2800000, guid: 9d92991794bb5864c8085468b97aa067, type: 3} + - {fileID: 2800000, guid: 14381521ff11cb74abe3fe65401c23be, type: 3} + - {fileID: 2800000, guid: d36f0fe53425e08499a2333cf423634c, type: 3} + - {fileID: 2800000, guid: d4044ea2490d63b43aa1765f8efbf8a9, type: 3} + - {fileID: 2800000, guid: c9bd74624d8070f429e3f46d161f9204, type: 3} + - {fileID: 2800000, guid: d5c9b274310e5524ebe32a4e4da3df1f, type: 3} + - {fileID: 2800000, guid: f69770e54f2823f43badf77916acad83, type: 3} + - {fileID: 2800000, guid: 10b6c6d22e73dea46a8ab36b6eebd629, type: 3} + - {fileID: 2800000, guid: a2ec5cbf5a9b64345ad3fab0912ddf7b, type: 3} + - {fileID: 2800000, guid: 1c3c6d69a645b804fa232004b96b7ad3, type: 3} + - {fileID: 2800000, guid: d18a24d7b4ed50f4387993566d9d3ae2, type: 3} + - {fileID: 2800000, guid: c989e1ed85cf7154caa922fec53e6af6, type: 3} + - {fileID: 2800000, guid: ff47e5a0f105eb34883b973e51f4db62, type: 3} + - {fileID: 2800000, guid: fa042edbfc40fbd4bad0ab9d505b1223, type: 3} + - {fileID: 2800000, guid: 896d9004736809c4fb5973b7c12eb8b9, type: 3} + - {fileID: 2800000, guid: 179f794063d2a66478e6e726f84a65bc, type: 3} + filmGrainTex: + - {fileID: 2800000, guid: 654c582f7f8a5a14dbd7d119cbde215d, type: 3} + - {fileID: 2800000, guid: dd77ffd079630404e879388999033049, type: 3} + - {fileID: 2800000, guid: 1097e90e1306e26439701489f391a6c0, type: 3} + - {fileID: 2800000, guid: f0b67500f7fad3b4c9f2b13e8f41ba6e, type: 3} + - {fileID: 2800000, guid: 9930fb4528622b34687b00bbe6883de7, type: 3} + - {fileID: 2800000, guid: bd9e8c758250ef449a4b4bfaad7a2133, type: 3} + - {fileID: 2800000, guid: 510a2f57334933e4a8dbabe4c30204e4, type: 3} + - {fileID: 2800000, guid: b4db8180660810945bf8d55ab44352ad, type: 3} + - {fileID: 2800000, guid: fd2fd78b392986e42a12df2177d3b89c, type: 3} + - {fileID: 2800000, guid: 5cdee82a77d13994f83b8fdabed7c301, type: 3} + smaaAreaTex: {fileID: 2800000, guid: d1f1048909d55cd4fa1126ab998f617e, type: 3} + smaaSearchTex: {fileID: 2800000, guid: 51eee22c2a633ef4aada830eed57c3fd, type: 3} + m_TexturesResourcesVersion: 0 + - rid: 1680211313492492305 + type: {class: URPDefaultVolumeProfileSettings, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + m_Version: 0 + m_VolumeProfile: {fileID: 11400000, guid: 5b8b49b0bf6036a4a823021e69f80309, type: 2} + - rid: 1680211313492492306 + type: {class: URPShaderStrippingSetting, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + m_Version: 0 + m_StripUnusedPostProcessingVariants: 0 + m_StripUnusedVariants: 1 + m_StripScreenCoordOverrideVariants: 1 + - rid: 1680211313492492307 + type: {class: Renderer2DResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + m_Version: 0 + m_LightShader: {fileID: 4800000, guid: 3f6c848ca3d7bca4bbe846546ac701a1, type: 3} + m_ProjectedShadowShader: {fileID: 4800000, guid: ce09d4a80b88c5a4eb9768fab4f1ee00, type: 3} + m_SpriteShadowShader: {fileID: 4800000, guid: 44fc62292b65ab04eabcf310e799ccf6, type: 3} + m_SpriteUnshadowShader: {fileID: 4800000, guid: de02b375720b5c445afe83cd483bedf3, type: 3} + m_GeometryShadowShader: {fileID: 4800000, guid: 19349a0f9a7ed4c48a27445bcf92e5e1, type: 3} + m_GeometryUnshadowShader: {fileID: 4800000, guid: 77774d9009bb81447b048c907d4c6273, type: 3} + m_CopyDepthPS: {fileID: 4800000, guid: d6dae50ee9e1bfa4db75f19f99355220, type: 3} + m_DefaultLitMaterial: {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_DefaultUnlitMaterial: {fileID: 2100000, guid: 9dfc825aed78fcd4ba02077103263b40, type: 2} + m_DefaultMaskMaterial: {fileID: 2100000, guid: 15d0c3709176029428a0da2f8cecf0b5, type: 2} + m_DefaultMesh2DLitMaterial: {fileID: 2100000, guid: 9452ae1262a74094f8a68013fbcd1834, type: 2} + - rid: 1680211313492492308 + type: {class: UniversalRenderPipelineEditorAssets, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} + data: + m_DefaultSettingsVolumeProfile: {fileID: 11400000, guid: eda47df5b85f4f249abf7abd73db2cb2, type: 2} + - rid: 1680211313492492309 + type: {class: GPUResidentDrawerResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.GPUDriven.Runtime} + data: + m_Version: 0 + m_InstanceDataBufferCopyKernels: {fileID: 7200000, guid: f984aeb540ded8b4fbb8a2047ab5b2e2, type: 3} + m_InstanceDataBufferUploadKernels: {fileID: 7200000, guid: 53864816eb00f2343b60e1a2c5a262ef, type: 3} + m_TransformUpdaterKernels: {fileID: 7200000, guid: 2a567b9b2733f8d47a700c3c85bed75b, type: 3} + m_WindDataUpdaterKernels: {fileID: 7200000, guid: fde76746e4fd0ed418c224f6b4084114, type: 3} + m_OccluderDepthPyramidKernels: {fileID: 7200000, guid: 08b2b5fb307b0d249860612774a987da, type: 3} + m_InstanceOcclusionCullingKernels: {fileID: 7200000, guid: f6d223acabc2f974795a5a7864b50e6c, type: 3} + m_OcclusionCullingDebugKernels: {fileID: 7200000, guid: b23e766bcf50ca4438ef186b174557df, type: 3} + m_DebugOcclusionTestPS: {fileID: 4800000, guid: d3f0849180c2d0944bc71060693df100, type: 3} + m_DebugOccluderPS: {fileID: 4800000, guid: b3c92426a88625841ab15ca6a7917248, type: 3} + - rid: 1680211313492492310 + type: {class: VrsRenderPipelineRuntimeResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} + data: + m_TextureComputeShader: {fileID: 7200000, guid: cacb30de6c40c7444bbc78cb0a81fd2a, type: 3} + m_VisualizationShader: {fileID: 4800000, guid: 620b55b8040a88d468e94abe55bed5ba, type: 3} + m_VisualizationLookupTable: + m_Data: + - {r: 0.785, g: 0.23, b: 0.2, a: 1} + - {r: 1, g: 0.8, b: 0.8, a: 1} + - {r: 0.4, g: 0.2, b: 0.2, a: 1} + - {r: 0.51, g: 0.8, b: 0.6, a: 1} + - {r: 0.6, g: 0.8, b: 1, a: 1} + - {r: 0.2, g: 0.4, b: 0.6, a: 1} + - {r: 0.8, g: 1, b: 0.8, a: 1} + - {r: 0.2, g: 0.4, b: 0.2, a: 1} + - {r: 0.125, g: 0.22, b: 0.36, a: 1} + m_ConversionLookupTable: + m_Data: + - {r: 0.785, g: 0.23, b: 0.2, a: 1} + - {r: 1, g: 0.8, b: 0.8, a: 1} + - {r: 0.4, g: 0.2, b: 0.2, a: 1} + - {r: 0.51, g: 0.8, b: 0.6, a: 1} + - {r: 0.6, g: 0.8, b: 1, a: 1} + - {r: 0.2, g: 0.4, b: 0.6, a: 1} + - {r: 0.8, g: 1, b: 0.8, a: 1} + - {r: 0.2, g: 0.4, b: 0.2, a: 1} + - {r: 0.125, g: 0.22, b: 0.36, a: 1} + - rid: 1680211313492492311 + type: {class: STP/RuntimeResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} + data: + m_setupCS: {fileID: 7200000, guid: 33be2e9a5506b2843bdb2bdff9cad5e1, type: 3} + m_preTaaCS: {fileID: 7200000, guid: a679dba8ec4d9ce45884a270b0e22dda, type: 3} + m_taaCS: {fileID: 7200000, guid: 3923900e2b41b5e47bc25bfdcbcdc9e6, type: 3} + - rid: 1680211313492492312 + type: {class: RenderingDebuggerRuntimeResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} + data: + m_version: 0 + - rid: 1680211313492492313 + type: {class: RenderGraphUtilsResources, ns: UnityEngine.Rendering.RenderGraphModule.Util, asm: Unity.RenderPipelines.Core.Runtime} + data: + m_Version: 0 + m_CoreCopyPS: {fileID: 4800000, guid: 12dc59547ea167a4ab435097dd0f9add, type: 3} + - rid: 1680211313492492314 + type: {class: ProbeVolumeBakingResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} + data: + m_Version: 1 + dilationShader: {fileID: 7200000, guid: 6bb382f7de370af41b775f54182e491d, type: 3} + subdivideSceneCS: {fileID: 7200000, guid: bb86f1f0af829fd45b2ebddda1245c22, type: 3} + voxelizeSceneShader: {fileID: 4800000, guid: c8b6a681c7b4e2e4785ffab093907f9e, type: 3} + traceVirtualOffsetCS: {fileID: -6772857160820960102, guid: ff2cbab5da58bf04d82c5f34037ed123, type: 3} + traceVirtualOffsetRT: {fileID: -5126288278712620388, guid: ff2cbab5da58bf04d82c5f34037ed123, type: 3} + skyOcclusionCS: {fileID: -6772857160820960102, guid: 5a2a534753fbdb44e96c3c78b5a6999d, type: 3} + skyOcclusionRT: {fileID: -5126288278712620388, guid: 5a2a534753fbdb44e96c3c78b5a6999d, type: 3} + renderingLayerCS: {fileID: -6772857160820960102, guid: 94a070d33e408384bafc1dea4a565df9, type: 3} + renderingLayerRT: {fileID: -5126288278712620388, guid: 94a070d33e408384bafc1dea4a565df9, type: 3} + - rid: 1680211313492492315 + type: {class: LightmapSamplingSettings, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} + data: + m_Version: 1 + m_UseBicubicLightmapSampling: 0 + - rid: 1680211313492492316 + type: {class: RenderGraphGlobalSettings, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} + data: + m_version: 0 + m_EnableCompilationCaching: 1 + m_EnableValidityChecks: 1 + - rid: 1680211313492492317 + type: {class: IncludeAdditionalRPAssets, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} + data: + m_version: 0 + m_IncludeReferencedInScenes: 0 + m_IncludeAssetsByLabel: 0 + m_LabelToInclude: + - rid: 1680211313492492318 + type: {class: ProbeVolumeDebugResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} + data: + m_Version: 1 + probeVolumeDebugShader: {fileID: 4800000, guid: 3b21275fd12d65f49babb5286f040f2d, type: 3} + probeVolumeFragmentationDebugShader: {fileID: 4800000, guid: 3a80877c579b9144ebdcc6d923bca303, type: 3} + probeVolumeSamplingDebugShader: {fileID: 4800000, guid: bf54e6528c79a224e96346799064c393, type: 3} + probeVolumeOffsetDebugShader: {fileID: 4800000, guid: db8bd7436dc2c5f4c92655307d198381, type: 3} + probeSamplingDebugMesh: {fileID: -3555484719484374845, guid: 20be25aac4e22ee49a7db76fb3df6de2, type: 3} + numbersDisplayTex: {fileID: 2800000, guid: 73fe53b428c5b3440b7e87ee830b608a, type: 3} + - rid: 1680211313492492319 + type: {class: ShaderStrippingSetting, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} + data: + m_Version: 0 + m_ExportShaderVariants: 1 + m_ShaderVariantLogLevel: 0 + m_StripRuntimeDebugShaders: 1 + - rid: 1680211313492492320 + type: {class: ProbeVolumeGlobalSettings, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} + data: + m_Version: 1 + m_ProbeVolumeDisableStreamingAssets: 0 + - rid: 1680211313492492321 + type: {class: ProbeVolumeRuntimeResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} + data: + m_Version: 1 + probeVolumeBlendStatesCS: {fileID: 7200000, guid: a3f7b8c99de28a94684cb1daebeccf5d, type: 3} + probeVolumeUploadDataCS: {fileID: 7200000, guid: 0951de5992461754fa73650732c4954c, type: 3} + probeVolumeUploadDataL2CS: {fileID: 7200000, guid: 6196f34ed825db14b81fb3eb0ea8d931, type: 3} + - rid: 1680211313492492322 + type: {class: WorldRenderPipelineResources, ns: UnityEngine.PathTracing.Core, asm: Unity.PathTracing.Runtime} + data: + _version: 3 + _blitCubemap: {fileID: 7200000, guid: 5a992812cb320d146a66cc600200cce7, type: 3} + _blitGrayScaleCookie: {fileID: 7200000, guid: 557fa399e33bf7647bda5697c5c158df, type: 3} + _setAlphaChannelShader: {fileID: 7200000, guid: 5efaea0e81c66334aa9d062d6573e6fd, type: 3} + _environmentImportanceSamplingBuild: {fileID: 7200000, guid: 5bb2534d2411d344cbc54f880232640f, type: 3} + _skyBoxMesh: {fileID: 4300000, guid: 0529e6c5f6dea8c4a8c2835ed7de57cb, type: 2} + _sixFaceSkyBoxMesh: {fileID: 4300000, guid: a80925ceebd011741b42509226cefc74, type: 2} + _buildLightGridShader: {fileID: 7200000, guid: 16e47c1641bd0104e92b624601457bb0, type: 3} diff --git a/ReleaseConsumerProject/Assets/UniversalRenderPipelineGlobalSettings.asset.meta b/ReleaseConsumerProject/Assets/UniversalRenderPipelineGlobalSettings.asset.meta new file mode 100644 index 0000000..b0ee24c --- /dev/null +++ b/ReleaseConsumerProject/Assets/UniversalRenderPipelineGlobalSettings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9374f1d73c9ee8940895d44ff5dd560e +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 70bae70..b5d1e9e 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.20", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.21", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index afdb4ad..9c46abe 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -142,7 +142,7 @@ "hash": "d0aa50d31f3b6da96bffc194fdc3aa358f37e332" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.20", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.21", "depth": 0, "source": "git", "dependencies": { @@ -151,7 +151,7 @@ "org.gamecult.eve.surface": "0.2.1", "com.unity.mathematics": "1.3.2" }, - "hash": "e5a43cffcddeaf6a492d8583408baa4172bd5796" + "hash": "c662f11618e4dbb2bf36b02c54c2932bba360a4b" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0", diff --git a/ReleaseConsumerProject/ProjectSettings/GraphicsSettings.asset b/ReleaseConsumerProject/ProjectSettings/GraphicsSettings.asset index 4b152c0..8b7efdf 100644 --- a/ReleaseConsumerProject/ProjectSettings/GraphicsSettings.asset +++ b/ReleaseConsumerProject/ProjectSettings/GraphicsSettings.asset @@ -37,7 +37,7 @@ GraphicsSettings: m_PreloadedShaders: [] m_PreloadShadersBatchTimeLimit: -1 m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0} - m_CustomRenderPipeline: {fileID: 0} + m_CustomRenderPipeline: {fileID: 11400000, guid: 43f32b840bd83ce47af920dadd59d4b3, type: 2} m_TransparencySortMode: 0 m_TransparencySortAxis: {x: 0, y: 0, z: 1} m_DefaultRenderingPath: 1 @@ -57,7 +57,8 @@ GraphicsSettings: m_FogKeepExp: 1 m_FogKeepExp2: 1 m_AlbedoSwatchInfos: [] - m_RenderPipelineGlobalSettingsMap: {} + m_RenderPipelineGlobalSettingsMap: + UnityEngine.Rendering.Universal.UniversalRenderPipeline: {fileID: 11400000, guid: 9374f1d73c9ee8940895d44ff5dd560e, type: 2} m_ShaderBuildSettings: keywordDeclarationOverrides: [] m_LightsUseLinearIntensity: 0 diff --git a/ReleaseConsumerProject/ProjectSettings/QualitySettings.asset b/ReleaseConsumerProject/ProjectSettings/QualitySettings.asset index 64f8aba..55e71a7 100644 --- a/ReleaseConsumerProject/ProjectSettings/QualitySettings.asset +++ b/ReleaseConsumerProject/ProjectSettings/QualitySettings.asset @@ -313,7 +313,7 @@ QualitySettings: asyncUploadBufferSize: 16 asyncUploadPersistentBuffer: 1 resolutionScalingFixedDPIFactor: 1 - customRenderPipeline: {fileID: 0} + customRenderPipeline: {fileID: 11400000, guid: 43f32b840bd83ce47af920dadd59d4b3, type: 2} terrainQualityOverrides: 0 terrainPixelError: 1 terrainDetailDensityScale: 1 diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 088d8a4..25b2fa7 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -418,6 +418,20 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var playerMarker = FindMarker(root, playerId); var playerRenderers = playerMarker.GetComponentsInChildren(); Assert.That(playerRenderers, Is.Not.Empty, "The provider-authored player prefab has no renderable visual."); + Assert.That(playerMarker.GetComponentsInChildren(), Is.Empty, + "The provider-authored player presentation retained gameplay colliders."); + Assert.That(playerMarker.GetComponentsInChildren(), Is.Empty, + "The provider-authored player presentation retained gameplay rigid bodies."); + foreach (var renderer in playerRenderers) + foreach (var material in renderer.sharedMaterials) + { + Assert.That(material, Is.Not.Null, $"Provider renderer {renderer.name} has a null material."); + Assert.That(material.shader, Is.Not.Null, $"Provider material {material.name} has no shader."); + Assert.That(material.shader.isSupported, Is.True, + $"Provider material {material.name} uses unsupported shader {material.shader.name}."); + Assert.That(material.shader.name, Does.Not.Contain("FallbackError"), + $"Provider material {material.name} resolved to Unity's error shader."); + } Assert.That(provider.TryGetRenderChannelLayer("map", out var mapLayer), Is.True); var mapRenderers = new List(); foreach (var renderer in root.GetComponentsInChildren(includeInactive: true)) diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index f174291..a4edd57 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "e5a43cffcddeaf6a492d8583408baa4172bd5796" +$expectedEveUnityCommit = "c662f11618e4dbb2bf36b02c54c2932bba360a4b" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot $projectRoot = $ClientProject From ab51d72c5ede381a4edae76a1fd8504bbbcde842 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 05:06:29 +0100 Subject: [PATCH 047/292] Lower provider-owned planar camera composition --- .../Runtime/EveUnityPlayableWorldCameraRig.cs | 67 +++++++++++++------ .../Runtime/EveUnitySceneSurfaceLowerer.cs | 50 +++++++++++++- .../EveUnitySceneSurfaceLowererTests.cs | 46 +++++++++++-- .../org.gamecult.eve.unity-scene/package.json | 2 +- 4 files changed, 136 insertions(+), 29 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index 4cf6d8f..d7134ed 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -1,4 +1,5 @@ using UnityEngine; +using UnityEngine.Rendering; #nullable enable @@ -46,6 +47,7 @@ namespace GameCult.Eve.UnityScene return false; if (!string.IsNullOrWhiteSpace(activeWorld.CameraRig) && + activeWorld.CameraRig != "planar.top-down-follow.v1" && activeWorld.CameraRig != "arpg.orbital-follow.v1" && activeWorld.CameraRig != "third-person-orbit") { @@ -53,7 +55,10 @@ namespace GameCult.Eve.UnityScene } var camera = cameraTransform != null ? cameraTransform : transform; - var player = FindPlayer(resolvedHost, activeWorld.PlayerEntityId); + var targetEntityId = string.IsNullOrWhiteSpace(activeWorld.CameraTargetEntityId) + ? activeWorld.PlayerEntityId + : activeWorld.CameraTargetEntityId; + var player = FindEntity(resolvedHost, targetEntityId); if (player == null) return false; @@ -69,10 +74,12 @@ namespace GameCult.Eve.UnityScene } cameraComponent.cullingMask = cullingMask; } + if (activeWorld.CameraRig == "planar.top-down-follow.v1") + return ApplyPlanarTopDown(activeWorld, camera, cameraComponent, player.position, deltaTime); + var bounds = CalculateVisualBounds( player, - cameraComponent != null ? cameraComponent.cullingMask : -1, - FindSelectedTarget(resolvedHost)); + cameraComponent != null ? cameraComponent.cullingMask : -1); var target = bounds?.center ?? player.position; var radius = bounds?.extents.magnitude ?? 0f; var verticalFov = cameraComponent != null ? cameraComponent.fieldOfView : 60f; @@ -92,12 +99,45 @@ namespace GameCult.Eve.UnityScene return true; } - private static Bounds? CalculateVisualBounds(Transform player, int cullingMask, Transform? selectedTarget) + private static bool ApplyPlanarTopDown( + EveUnityPlayableWorldProjection world, + Transform camera, + Camera? cameraComponent, + Vector3 target, + float deltaTime) + { + var resolvedDistance = Mathf.Max(0.1f, world.CameraDistance); + var verticalFov = Mathf.Clamp(world.CameraVerticalFieldOfViewDegrees, 1f, 179f); + if (cameraComponent != null) + cameraComponent.fieldOfView = verticalFov; + var aspect = cameraComponent != null ? cameraComponent.aspect : 16f / 9f; + var verticalHalfExtent = resolvedDistance * Mathf.Tan(verticalFov * 0.5f * Mathf.Deg2Rad); + var horizontalHalfExtent = verticalHalfExtent * Mathf.Max(0.01f, aspect); + var screenOffset = new Vector3( + (Mathf.Clamp01(world.CameraTargetScreenX) - 0.5f) * 2f * horizontalHalfExtent, + 0f, + (Mathf.Clamp01(world.CameraTargetScreenY) - 0.5f) * 2f * verticalHalfExtent); + var desiredPosition = target + (Vector3.up * resolvedDistance) - screenOffset; + var damping = Mathf.Max(0f, world.CameraPositionDamping); + var t = deltaTime <= 0f || damping <= 0f + ? 1f + : 1f - Mathf.Exp(-damping * deltaTime); + camera.position = Vector3.Lerp(camera.position, desiredPosition, t); + camera.rotation = Quaternion.LookRotation(Vector3.down, Vector3.forward); + + RenderSettings.ambientMode = AmbientMode.Flat; + RenderSettings.ambientLight = new Color( + Mathf.Max(0f, world.AmbientLightR * world.AmbientLightIntensity), + Mathf.Max(0f, world.AmbientLightG * world.AmbientLightIntensity), + Mathf.Max(0f, world.AmbientLightB * world.AmbientLightIntensity), + 1f); + return true; + } + + private static Bounds? CalculateVisualBounds(Transform player, int cullingMask) { Bounds? bounds = null; - foreach (var root in selectedTarget == null - ? new[] { player } - : new[] { player, selectedTarget! }) + foreach (var root in new[] { player }) { foreach (var visual in root.GetComponentsInChildren(includeInactive: false)) { @@ -122,17 +162,6 @@ namespace GameCult.Eve.UnityScene return bounds; } - private static Transform? FindSelectedTarget(EveUnityPlayableWorldClientHost host) - { - var combat = EveUnityCombatPresentation.Find(host.ActiveProjection); - if (combat == null || string.IsNullOrWhiteSpace(combat.SelectedTargetEntityId)) - return null; - if (host.PresentedEntities?.CurrentGeneration != null && - host.PresentedEntities.TryGetByEntityId(combat.SelectedTargetEntityId, out var presented)) - return presented.Transform; - return null; - } - private void LateUpdate() { if (driveInLateUpdate) @@ -148,7 +177,7 @@ namespace GameCult.Eve.UnityScene return host; } - private static Transform? FindPlayer( + private static Transform? FindEntity( EveUnityPlayableWorldClientHost host, string playerEntityId) { diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index c6d5c88..5dd3f2e 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -126,7 +126,15 @@ namespace GameCult.Eve.UnityScene worldRoot.GetProp("entityViewSchema"), worldRoot.GetProp("zoneRenderPointerId"), worldRoot.GetProp("zoneRenderSchema"), - ParseStringList(worldRoot.GetProp("excludedRenderChannels"))); + ParseStringList(worldRoot.GetProp("excludedRenderChannels")), + worldRoot.GetProp("cameraTargetEntityId"), + ParseFloat(worldRoot.GetProp("cameraDistance"), 0f), + ParseFloat(worldRoot.GetProp("cameraVerticalFieldOfViewDegrees"), 0f), + ParseFloat(worldRoot.GetProp("cameraTargetScreenX"), 0.5f), + ParseFloat(worldRoot.GetProp("cameraTargetScreenY"), 0.5f), + ParseFloat(worldRoot.GetProp("cameraPositionDamping"), 0f), + ParseVector3(worldRoot.GetProp("ambientLightColor")), + ParseFloat(worldRoot.GetProp("ambientLightIntensity"), 1f)); } private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component) @@ -337,7 +345,15 @@ namespace GameCult.Eve.UnityScene string entityViewSchema = "", string zoneRenderPointerId = "", string zoneRenderSchema = "", - IReadOnlyList? excludedRenderChannels = null) + IReadOnlyList? excludedRenderChannels = null, + string cameraTargetEntityId = "", + float cameraDistance = 0f, + float cameraVerticalFieldOfViewDegrees = 0f, + float cameraTargetScreenX = 0.5f, + float cameraTargetScreenY = 0.5f, + float cameraPositionDamping = 0f, + (float r, float g, float b) ambientLightColor = default, + float ambientLightIntensity = 1f) { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -356,6 +372,16 @@ namespace GameCult.Eve.UnityScene ZoneRenderPointerId = zoneRenderPointerId ?? ""; ZoneRenderSchema = zoneRenderSchema ?? ""; ExcludedRenderChannels = excludedRenderChannels ?? Array.Empty(); + CameraTargetEntityId = cameraTargetEntityId ?? ""; + CameraDistance = cameraDistance; + CameraVerticalFieldOfViewDegrees = cameraVerticalFieldOfViewDegrees; + CameraTargetScreenX = cameraTargetScreenX; + CameraTargetScreenY = cameraTargetScreenY; + CameraPositionDamping = cameraPositionDamping; + AmbientLightR = ambientLightColor.r; + AmbientLightG = ambientLightColor.g; + AmbientLightB = ambientLightColor.b; + AmbientLightIntensity = ambientLightIntensity; } public string WorldRootId { get; } @@ -376,6 +402,26 @@ namespace GameCult.Eve.UnityScene public string CameraRig { get; } + public string CameraTargetEntityId { get; } + + public float CameraDistance { get; } + + public float CameraVerticalFieldOfViewDegrees { get; } + + public float CameraTargetScreenX { get; } + + public float CameraTargetScreenY { get; } + + public float CameraPositionDamping { get; } + + public float AmbientLightR { get; } + + public float AmbientLightG { get; } + + public float AmbientLightB { get; } + + public float AmbientLightIntensity { get; } + public string ViewId { get; } public string PlayerEntityId { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 4fe1fe4..53b90cf 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -398,7 +398,17 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(projection.PlayableWorld.ZoneRenderPointerId, Is.EqualTo("cultmesh://aetheria/world/zone-render")); Assert.That(projection.PlayableWorld.AssetManifest, Is.EqualTo("cultmesh://aetheria/assets/manifest")); Assert.That(projection.PlayableWorld.InputProfile, Is.EqualTo("arpg-third-person")); - Assert.That(projection.PlayableWorld.CameraRig, Is.EqualTo("third-person-orbit")); + Assert.That(projection.PlayableWorld.CameraRig, Is.EqualTo("planar.top-down-follow.v1")); + Assert.That(projection.PlayableWorld.CameraTargetEntityId, Is.EqualTo("player-vanguard")); + Assert.That(projection.PlayableWorld.CameraDistance, Is.EqualTo(150f)); + Assert.That(projection.PlayableWorld.CameraVerticalFieldOfViewDegrees, Is.EqualTo(60f)); + Assert.That(projection.PlayableWorld.CameraTargetScreenX, Is.EqualTo(0.9f)); + Assert.That(projection.PlayableWorld.CameraTargetScreenY, Is.EqualTo(0.55f)); + Assert.That(projection.PlayableWorld.CameraPositionDamping, Is.EqualTo(5f)); + Assert.That(projection.PlayableWorld.AmbientLightR, Is.EqualTo(0.2f)); + Assert.That(projection.PlayableWorld.AmbientLightG, Is.EqualTo(0.2f)); + Assert.That(projection.PlayableWorld.AmbientLightB, Is.EqualTo(0.2f)); + Assert.That(projection.PlayableWorld.AmbientLightIntensity, Is.EqualTo(1f)); Assert.That(projection.PlayableWorld.ExcludedRenderChannels, Is.EqualTo(new[] { "map" })); Assert.That(projection.PlayableWorld.PlayerEntityId, Is.EqualTo("player-vanguard")); Assert.That(projection.PlayableWorld.MovementCommand, Is.EqualTo("aetheria.daemon.move_intent")); @@ -1027,6 +1037,8 @@ namespace GameCult.Eve.UnityScene.Tests var hostObject = new GameObject("generic-eve-client"); var cameraObject = new GameObject("generic-eve-camera"); hostObject.SetActive(false); + var ambientMode = RenderSettings.ambientMode; + var ambientLight = RenderSettings.ambientLight; try { @@ -1051,6 +1063,7 @@ namespace GameCult.Eve.UnityScene.Tests rig.CameraTransform = cameraObject.transform; rig.RenderPolicySource = new FixedRenderChannelPolicy("map", 14); var camera = cameraObject.AddComponent(); + camera.aspect = 16f / 9f; var player = rootObject.GetComponentInChildren(); Assert.That(player, Is.Not.Null); @@ -1061,14 +1074,25 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(rig.ApplyRig(0f), Is.True); Assert.That(camera.cullingMask & (1 << 14), Is.Zero); - Assert.That(cameraObject.transform.position.y, Is.GreaterThan(0f)); + Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); + Assert.That(cameraObject.transform.position.y - player.transform.position.y, Is.EqualTo(150f).Within(0.001f)); + var viewport = camera.WorldToViewportPoint(player.transform.position); + Assert.That(viewport.x, Is.EqualTo(0.9f).Within(0.001f)); + Assert.That(viewport.y, Is.EqualTo(0.55f).Within(0.001f)); + Assert.That(RenderSettings.ambientMode, Is.EqualTo(UnityEngine.Rendering.AmbientMode.Flat)); + Assert.That(RenderSettings.ambientLight.r, Is.EqualTo(0.2f).Within(0.001f)); + + var beforeFollow = cameraObject.transform.position; + player.transform.position += Vector3.right * 10f; + Assert.That(rig.ApplyRig(0.1f), Is.True); Assert.That( - Vector3.Distance(cameraObject.transform.position, player.transform.position), - Is.LessThan(50f), - "An excluded map glyph changed pilot-camera framing."); + cameraObject.transform.position.x - beforeFollow.x, + Is.EqualTo(10f * (1f - Mathf.Exp(-0.5f))).Within(0.001f)); } finally { + RenderSettings.ambientMode = ambientMode; + RenderSettings.ambientLight = ambientLight; UnityEngine.Object.DestroyImmediate(cameraObject); UnityEngine.Object.DestroyImmediate(hostObject); UnityEngine.Object.DestroyImmediate(rootObject); @@ -1122,7 +1146,7 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(firstPresentation.WorldRootId, Is.EqualTo("aetheria.daemon.game.playable")); Assert.That(firstPresentation.PlayerEntityId, Is.EqualTo("player-vanguard")); Assert.That(firstPresentation.InputProfile, Is.EqualTo("arpg-third-person")); - Assert.That(firstPresentation.CameraRig, Is.EqualTo("third-person-orbit")); + Assert.That(firstPresentation.CameraRig, Is.EqualTo("planar.top-down-follow.v1")); Assert.That(firstPresentation.UpsertedEntities, Is.EqualTo(3)); Assert.That(firstPresentation.RemovedEntities, Is.EqualTo(0)); Assert.That(firstPresentation.ActiveEntities, Is.EqualTo(3)); @@ -1678,7 +1702,15 @@ namespace GameCult.Eve.UnityScene.Tests ["zoneRenderSchema"] = "gamecult.aetheria.zone_render.v1", ["assetManifest"] = "cultmesh://aetheria/assets/manifest", ["inputProfile"] = "arpg-third-person", - ["cameraRig"] = "third-person-orbit", + ["cameraRig"] = "planar.top-down-follow.v1", + ["cameraTargetEntityId"] = "player-vanguard", + ["cameraDistance"] = "150", + ["cameraVerticalFieldOfViewDegrees"] = "60", + ["cameraTargetScreenX"] = "0.9", + ["cameraTargetScreenY"] = "0.55", + ["cameraPositionDamping"] = "5", + ["ambientLightColor"] = "0.2,0.2,0.2", + ["ambientLightIntensity"] = "1", ["excludedRenderChannels"] = "map", ["playerEntityId"] = "player-vanguard", ["movementCommand"] = "aetheria.daemon.move_intent", diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index bb3058d..3589d71 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.21", + "version": "0.3.22", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From e43f2d74557dcbf2cc408893c7b7d7ac708a44a8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 05:15:50 +0100 Subject: [PATCH 048/292] Complete planar camera lifecycle and lens --- .../Runtime/EveUnityPlayableWorldCameraRig.cs | 62 ++++++++++++++++++- .../Runtime/EveUnitySceneSurfaceLowerer.cs | 14 ++++- .../EveUnitySceneSurfaceLowererTests.cs | 40 +++++++++++- .../org.gamecult.eve.unity-scene/package.json | 2 +- 4 files changed, 111 insertions(+), 7 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index d7134ed..69d717d 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -20,6 +20,9 @@ namespace GameCult.Eve.UnityScene [SerializeField] private float yawDegrees = 35f; [SerializeField] private float followDamping = 12f; private IEveUnityCameraRenderPolicySource? _renderPolicySource; + private bool _ownsAmbientEnvironment; + private AmbientMode _previousAmbientMode; + private Color _previousAmbientLight; public EveUnityPlayableWorldClientHost? Host { @@ -39,12 +42,27 @@ namespace GameCult.Eve.UnityScene set => _renderPolicySource = value; } + public void ReleaseRig() => RestoreAmbientEnvironment(); + public bool ApplyRig(float deltaTime) { var resolvedHost = ResolveHost(); var activeWorld = resolvedHost?.ActiveWorld; if (resolvedHost == null || activeWorld == null) + { + RestoreAmbientEnvironment(); return false; + } + + if (activeWorld.CameraRig == "planar.top-down-follow.v1" && !HasValidPlanarContract(activeWorld)) + { + RestoreAmbientEnvironment(); + return false; + } + if (activeWorld.CameraRig == "planar.top-down-follow.v1") + ApplyAmbientEnvironment(activeWorld); + else + RestoreAmbientEnvironment(); if (!string.IsNullOrWhiteSpace(activeWorld.CameraRig) && activeWorld.CameraRig != "planar.top-down-follow.v1" && @@ -106,10 +124,16 @@ namespace GameCult.Eve.UnityScene Vector3 target, float deltaTime) { - var resolvedDistance = Mathf.Max(0.1f, world.CameraDistance); - var verticalFov = Mathf.Clamp(world.CameraVerticalFieldOfViewDegrees, 1f, 179f); + var resolvedDistance = world.CameraDistance; + var verticalFov = world.CameraVerticalFieldOfViewDegrees; if (cameraComponent != null) + { + cameraComponent.orthographic = false; + cameraComponent.lensShift = Vector2.zero; cameraComponent.fieldOfView = verticalFov; + cameraComponent.nearClipPlane = world.CameraNearClipPlane; + cameraComponent.farClipPlane = world.CameraFarClipPlane; + } var aspect = cameraComponent != null ? cameraComponent.aspect : 16f / 9f; var verticalHalfExtent = resolvedDistance * Mathf.Tan(verticalFov * 0.5f * Mathf.Deg2Rad); var horizontalHalfExtent = verticalHalfExtent * Mathf.Max(0.01f, aspect); @@ -125,13 +149,41 @@ namespace GameCult.Eve.UnityScene camera.position = Vector3.Lerp(camera.position, desiredPosition, t); camera.rotation = Quaternion.LookRotation(Vector3.down, Vector3.forward); + return true; + } + + private static bool HasValidPlanarContract(EveUnityPlayableWorldProjection world) => + world.CameraDistance > 0f && + world.CameraVerticalFieldOfViewDegrees > 0f && + world.CameraVerticalFieldOfViewDegrees < 180f && + world.CameraTargetScreenX >= 0f && world.CameraTargetScreenX <= 1f && + world.CameraTargetScreenY >= 0f && world.CameraTargetScreenY <= 1f && + world.CameraPositionDamping >= 0f && + world.CameraNearClipPlane > 0f && + world.CameraFarClipPlane > world.CameraNearClipPlane; + + private void ApplyAmbientEnvironment(EveUnityPlayableWorldProjection world) + { + if (!_ownsAmbientEnvironment) + { + _previousAmbientMode = RenderSettings.ambientMode; + _previousAmbientLight = RenderSettings.ambientLight; + _ownsAmbientEnvironment = true; + } RenderSettings.ambientMode = AmbientMode.Flat; RenderSettings.ambientLight = new Color( Mathf.Max(0f, world.AmbientLightR * world.AmbientLightIntensity), Mathf.Max(0f, world.AmbientLightG * world.AmbientLightIntensity), Mathf.Max(0f, world.AmbientLightB * world.AmbientLightIntensity), 1f); - return true; + } + + private void RestoreAmbientEnvironment() + { + if (!_ownsAmbientEnvironment) return; + RenderSettings.ambientMode = _previousAmbientMode; + RenderSettings.ambientLight = _previousAmbientLight; + _ownsAmbientEnvironment = false; } private static Bounds? CalculateVisualBounds(Transform player, int cullingMask) @@ -168,6 +220,10 @@ namespace GameCult.Eve.UnityScene ApplyRig(Time.deltaTime); } + private void OnDisable() => ReleaseRig(); + + private void OnDestroy() => ReleaseRig(); + private EveUnityPlayableWorldClientHost? ResolveHost() { if (host != null) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index 5dd3f2e..b8a33a0 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -134,7 +134,9 @@ namespace GameCult.Eve.UnityScene ParseFloat(worldRoot.GetProp("cameraTargetScreenY"), 0.5f), ParseFloat(worldRoot.GetProp("cameraPositionDamping"), 0f), ParseVector3(worldRoot.GetProp("ambientLightColor")), - ParseFloat(worldRoot.GetProp("ambientLightIntensity"), 1f)); + ParseFloat(worldRoot.GetProp("ambientLightIntensity"), 1f), + ParseFloat(worldRoot.GetProp("cameraNearClipPlane"), 0f), + ParseFloat(worldRoot.GetProp("cameraFarClipPlane"), 0f)); } private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component) @@ -353,7 +355,9 @@ namespace GameCult.Eve.UnityScene float cameraTargetScreenY = 0.5f, float cameraPositionDamping = 0f, (float r, float g, float b) ambientLightColor = default, - float ambientLightIntensity = 1f) + float ambientLightIntensity = 1f, + float cameraNearClipPlane = 0f, + float cameraFarClipPlane = 0f) { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -382,6 +386,8 @@ namespace GameCult.Eve.UnityScene AmbientLightG = ambientLightColor.g; AmbientLightB = ambientLightColor.b; AmbientLightIntensity = ambientLightIntensity; + CameraNearClipPlane = cameraNearClipPlane; + CameraFarClipPlane = cameraFarClipPlane; } public string WorldRootId { get; } @@ -414,6 +420,10 @@ namespace GameCult.Eve.UnityScene public float CameraPositionDamping { get; } + public float CameraNearClipPlane { get; } + + public float CameraFarClipPlane { get; } + public float AmbientLightR { get; } public float AmbientLightG { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 53b90cf..c1f7051 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -389,7 +389,7 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(projection.Root.Children[0].SceneObjectKind, Is.EqualTo("playable-world-root")); Assert.That(projection.Root.Children[0].Children[0].SceneObjectKind, Is.EqualTo("playable-world-entity")); - Assert.That(projection.Root.Children[0].Children[3].SceneObjectKind, Is.EqualTo("world-field-3d")); + Assert.That(projection.Root.Children[0].Children.Any(child => child.SceneObjectKind == "world-field-3d"), Is.True); Assert.That(projection.PlayableWorld, Is.Not.Null); Assert.That(projection.PlayableWorld!.WorldRootId, Is.EqualTo("aetheria.daemon.game.playable")); Assert.That(projection.PlayableWorld.StatePointerId, Is.EqualTo("cultmesh://aetheria/run/current")); @@ -405,6 +405,8 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(projection.PlayableWorld.CameraTargetScreenX, Is.EqualTo(0.9f)); Assert.That(projection.PlayableWorld.CameraTargetScreenY, Is.EqualTo(0.55f)); Assert.That(projection.PlayableWorld.CameraPositionDamping, Is.EqualTo(5f)); + Assert.That(projection.PlayableWorld.CameraNearClipPlane, Is.EqualTo(0.3f)); + Assert.That(projection.PlayableWorld.CameraFarClipPlane, Is.EqualTo(4096f)); Assert.That(projection.PlayableWorld.AmbientLightR, Is.EqualTo(0.2f)); Assert.That(projection.PlayableWorld.AmbientLightG, Is.EqualTo(0.2f)); Assert.That(projection.PlayableWorld.AmbientLightB, Is.EqualTo(0.2f)); @@ -1057,6 +1059,7 @@ namespace GameCult.Eve.UnityScene.Tests var host = hostObject.AddComponent(); host.Configure(rootObject.transform, provider, provider, provider, provider); host.Connect(); + hostObject.SetActive(true); var rig = hostObject.AddComponent(); rig.Host = host; @@ -1064,6 +1067,8 @@ namespace GameCult.Eve.UnityScene.Tests rig.RenderPolicySource = new FixedRenderChannelPolicy("map", 14); var camera = cameraObject.AddComponent(); camera.aspect = 16f / 9f; + camera.orthographic = true; + camera.lensShift = new Vector2(0.25f, -0.2f); var player = rootObject.GetComponentInChildren(); Assert.That(player, Is.Not.Null); @@ -1075,6 +1080,10 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(rig.ApplyRig(0f), Is.True); Assert.That(camera.cullingMask & (1 << 14), Is.Zero); Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); + Assert.That(camera.orthographic, Is.False); + Assert.That(camera.lensShift, Is.EqualTo(Vector2.zero)); + Assert.That(camera.nearClipPlane, Is.EqualTo(0.3f).Within(0.001f)); + Assert.That(camera.farClipPlane, Is.EqualTo(4096f).Within(0.001f)); Assert.That(cameraObject.transform.position.y - player.transform.position.y, Is.EqualTo(150f).Within(0.001f)); var viewport = camera.WorldToViewportPoint(player.transform.position); Assert.That(viewport.x, Is.EqualTo(0.9f).Within(0.001f)); @@ -1082,12 +1091,27 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(RenderSettings.ambientMode, Is.EqualTo(UnityEngine.Rendering.AmbientMode.Flat)); Assert.That(RenderSettings.ambientLight.r, Is.EqualTo(0.2f).Within(0.001f)); + var selectedTarget = rootObject.GetComponentsInChildren() + .Single(marker => marker.EntityId == "raider-scout"); + Assert.That(EveUnityCombatPresentation.Find(host.ActiveProjection)?.SelectedTargetEntityId, + Is.EqualTo("raider-scout")); + var poseWithoutSelectedTarget = cameraObject.transform.position; + selectedTarget.transform.position = new Vector3(10000f, 0f, -10000f); + Assert.That(rig.ApplyRig(0f), Is.True); + Assert.That(cameraObject.transform.position, Is.EqualTo(poseWithoutSelectedTarget)); + viewport = camera.WorldToViewportPoint(player.transform.position); + Assert.That(viewport.x, Is.EqualTo(0.9f).Within(0.001f)); + Assert.That(viewport.y, Is.EqualTo(0.55f).Within(0.001f)); + var beforeFollow = cameraObject.transform.position; player.transform.position += Vector3.right * 10f; Assert.That(rig.ApplyRig(0.1f), Is.True); Assert.That( cameraObject.transform.position.x - beforeFollow.x, Is.EqualTo(10f * (1f - Mathf.Exp(-0.5f))).Within(0.001f)); + rig.ReleaseRig(); + Assert.That(RenderSettings.ambientMode, Is.EqualTo(ambientMode)); + Assert.That(RenderSettings.ambientLight, Is.EqualTo(ambientLight)); } finally { @@ -1664,6 +1688,18 @@ namespace GameCult.Eve.UnityScene.Tests "", "")); + playableChildren.Add(new EveSurfaceComponent( + "aetheria.daemon.game.combat", + "combat.presentation", + new Dictionary(StringComparer.Ordinal) + { + ["controlledEntityId"] = "player-vanguard", + ["selectedTargetEntityId"] = "raider-scout", + ["targetVisible"] = "true", + ["targetHostile"] = "true" + }, + Array.Empty())); + playableChildren.Add(new EveSurfaceComponent( "aetheria.daemon.game.flow", "field.vector3d", @@ -1709,6 +1745,8 @@ namespace GameCult.Eve.UnityScene.Tests ["cameraTargetScreenX"] = "0.9", ["cameraTargetScreenY"] = "0.55", ["cameraPositionDamping"] = "5", + ["cameraNearClipPlane"] = "0.3", + ["cameraFarClipPlane"] = "4096", ["ambientLightColor"] = "0.2,0.2,0.2", ["ambientLightIntensity"] = "1", ["excludedRenderChannels"] = "map", diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 3589d71..d6f6274 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.22", + "version": "0.3.23", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 4b1f0be60507b6c913148a4ed558aa2c3b18cc20 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 05:20:46 +0100 Subject: [PATCH 049/292] Pin released native camera witness --- .../Assets/Tests/PlayMode/GenericWorldCaptureTests.cs | 8 ++++++-- ReleaseConsumerProject/Packages/manifest.json | 2 +- ReleaseConsumerProject/Packages/packages-lock.json | 4 ++-- .../Assets/Tests/PlayMode/GenericWorldCaptureTests.cs | 8 ++++++-- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 25b2fa7..9f58eac 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -441,18 +441,22 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } Assert.That(mapRenderers, Is.Not.Empty, "The live provider prefab has no renderer assigned to its advertised map channel."); + target = new RenderTexture(640, 360, 24, RenderTextureFormat.ARGB32); + camera.targetTexture = target; var cameraRig = cameraObject.AddComponent(); cameraRig.Host = host; cameraRig.CameraTransform = camera.transform; cameraRig.RenderPolicySource = provider; Assert.That(cameraRig.ApplyRig(0f), Is.True); Assert.That(camera.cullingMask & (1 << mapLayer), Is.Zero); + Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); + var playerViewport = camera.WorldToViewportPoint(playerMarker.transform.position); + Assert.That(playerViewport.x, Is.EqualTo(0.9f).Within(0.01f)); + Assert.That(playerViewport.y, Is.EqualTo(0.55f).Within(0.01f)); var mapCamera = mapCameraObject.AddComponent(); mapCamera.cullingMask = 1 << mapLayer; Assert.That(mapCamera.cullingMask, Is.EqualTo(1 << mapLayer)); - target = new RenderTexture(640, 360, 24, RenderTextureFormat.ARGB32); - camera.targetTexture = target; yield return null; camera.Render(); RenderTexture.active = target; diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index b5d1e9e..f146c06 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.21", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.23", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 9c46abe..bae8c86 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -142,7 +142,7 @@ "hash": "d0aa50d31f3b6da96bffc194fdc3aa358f37e332" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.21", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.23", "depth": 0, "source": "git", "dependencies": { @@ -151,7 +151,7 @@ "org.gamecult.eve.surface": "0.2.1", "com.unity.mathematics": "1.3.2" }, - "hash": "c662f11618e4dbb2bf36b02c54c2932bba360a4b" + "hash": "e43f2d74557dcbf2cc408893c7b7d7ac708a44a8" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 25b2fa7..9f58eac 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -441,18 +441,22 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } Assert.That(mapRenderers, Is.Not.Empty, "The live provider prefab has no renderer assigned to its advertised map channel."); + target = new RenderTexture(640, 360, 24, RenderTextureFormat.ARGB32); + camera.targetTexture = target; var cameraRig = cameraObject.AddComponent(); cameraRig.Host = host; cameraRig.CameraTransform = camera.transform; cameraRig.RenderPolicySource = provider; Assert.That(cameraRig.ApplyRig(0f), Is.True); Assert.That(camera.cullingMask & (1 << mapLayer), Is.Zero); + Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); + var playerViewport = camera.WorldToViewportPoint(playerMarker.transform.position); + Assert.That(playerViewport.x, Is.EqualTo(0.9f).Within(0.01f)); + Assert.That(playerViewport.y, Is.EqualTo(0.55f).Within(0.01f)); var mapCamera = mapCameraObject.AddComponent(); mapCamera.cullingMask = 1 << mapLayer; Assert.That(mapCamera.cullingMask, Is.EqualTo(1 << mapLayer)); - target = new RenderTexture(640, 360, 24, RenderTextureFormat.ARGB32); - camera.targetTexture = target; yield return null; camera.Render(); RenderTexture.active = target; diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index a4edd57..213d96f 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "c662f11618e4dbb2bf36b02c54c2932bba360a4b" +$expectedEveUnityCommit = "e43f2d74557dcbf2cc408893c7b7d7ac708a44a8" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot $projectRoot = $ClientProject From 3075f71ece0f395aecb770a890ec1b1728e4573c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 05:55:53 +0100 Subject: [PATCH 050/292] Lower authoritative pilot aim in generic worlds --- .../Runtime/EveUnityAimPresentation.cs | 49 +++++++ .../Runtime/EveUnityAimPresentation.cs.meta | 2 + .../EveUnityAimPresentationRenderer.cs | 136 ++++++++++++++++++ .../EveUnityAimPresentationRenderer.cs.meta | 2 + .../EveUnityPlayableWorldClientHost.cs | 13 ++ .../EveUnityPlayableWorldInputDriver.cs | 57 ++++++++ .../EveUnityPlayableWorldLiveClient.cs | 10 ++ .../Runtime/EveUnityPlayableWorldRuntime.cs | 10 ++ .../Runtime/EveUnitySceneClientSession.cs | 20 +++ .../EveUnitySceneProviderConnection.cs | 10 ++ .../Runtime/EveUnitySceneSurfaceLowerer.cs | 14 +- .../Editor/EveUnityAimPresentationTests.cs | 57 ++++++++ .../EveUnityAimPresentationTests.cs.meta | 2 + .../EveUnitySceneSurfaceLowererTests.cs | 24 ++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 15 files changed, 405 insertions(+), 3 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs.meta create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs.meta create mode 100644 packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAimPresentationTests.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAimPresentationTests.cs.meta diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs new file mode 100644 index 0000000..fbe8720 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs @@ -0,0 +1,49 @@ +using System; +using System.Globalization; + +#nullable enable + +namespace GameCult.Eve.UnityScene +{ + public sealed class EveUnityAimPresentation + { + public EveUnityAimPresentation(EveUnitySceneNode node) + { + if (node == null) throw new ArgumentNullException(nameof(node)); + ControlledEntityId = Get(node, "controlledEntityId"); + ConvergenceTargetEntityId = Get(node, "convergenceTargetEntityId"); + ViewDotRole = Get(node, "viewDotRole"); + MinimumConvergenceDistance = Math.Max(0.01f, Number(node, "minimumConvergenceDistance", 50f)); + ViewDotRadius = Math.Max(0.01f, Number(node, "viewDotRadius", 0.8f)); + } + + public string ControlledEntityId { get; } + public string ConvergenceTargetEntityId { get; } + public string ViewDotRole { get; } + public float MinimumConvergenceDistance { get; } + public float ViewDotRadius { get; } + + public static EveUnityAimPresentation? Find(EveUnitySceneProjection? projection) => + projection == null ? null : Find(projection.Root); + + private static EveUnityAimPresentation? Find(EveUnitySceneNode node) + { + if (string.Equals(node.ComponentKind, "aim.presentation", StringComparison.Ordinal)) + return new EveUnityAimPresentation(node); + foreach (var child in node.Children) + { + var found = Find(child); + if (found != null) return found; + } + return null; + } + + private static string Get(EveUnitySceneNode node, string key) => + node.Props.TryGetValue(key, out var value) ? value ?? "" : ""; + + private static float Number(EveUnitySceneNode node, string key, float fallback) => + float.TryParse(Get(node, key), NumberStyles.Float, CultureInfo.InvariantCulture, out var value) + ? value + : fallback; + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs.meta new file mode 100644 index 0000000..8a50d5a --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f859926d39664cefa5aa96ed3c2d8903 diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs new file mode 100644 index 0000000..25b8ce8 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs @@ -0,0 +1,136 @@ +using System; +using UnityEngine; + +#nullable enable + +namespace GameCult.Eve.UnityScene +{ + public sealed class EveUnityAimPresentationRenderer : MonoBehaviour + { + [SerializeField] private Color viewDotColor = new Color(0.35f, 0.95f, 1f, 0.95f); + [SerializeField] private float width = 0.1f; + + private EveUnityPlayableWorldClientHost? _host; + private LineRenderer? _ring; + private LineRenderer? _horizontal; + private LineRenderer? _vertical; + private Material? _material; + + public EveUnityAimPresentation? Current { get; private set; } + public bool ViewDotVisible => _ring != null && _ring.enabled; + public Vector3 ViewDotPosition { get; private set; } + + public void Bind(EveUnityPlayableWorldClientHost host) + { + _host = host != null ? host : throw new ArgumentNullException(nameof(host)); + } + + public void RefreshNow() + { + Current = EveUnityAimPresentation.Find(_host?.ActiveProjection); + if (Current == null || string.IsNullOrWhiteSpace(Current.ControlledEntityId)) + { + SetVisible(false); + return; + } + + var controlled = FindMarker(Current.ControlledEntityId); + if (controlled == null) + { + SetVisible(false); + return; + } + + var distance = Current.MinimumConvergenceDistance; + var target = FindMarker(Current.ConvergenceTargetEntityId); + if (target != null) + distance = Mathf.Max(distance, Vector3.Distance(controlled.transform.position, target.transform.position)); + var forward = controlled.transform.forward; + forward.y = 0f; + if (forward.sqrMagnitude <= 0.0001f) forward = Vector3.forward; + forward.Normalize(); + ViewDotPosition = controlled.transform.position + forward * distance; + + EnsureVisuals(); + var camera = Camera.main; + var right = camera == null ? Vector3.right : camera.transform.right; + var up = camera == null ? Vector3.up : camera.transform.up; + var center = ViewDotPosition + (camera == null ? Vector3.zero : -camera.transform.forward * 0.1f); + Ring(_ring!, center, right, up, Current.ViewDotRadius); + Segment(_horizontal!, center, right, Current.ViewDotRadius * 1.6f); + Segment(_vertical!, center, up, Current.ViewDotRadius * 1.6f); + SetVisible(true); + } + + private void LateUpdate() => RefreshNow(); + + private EveUnityPlayableWorldEntityMarker? FindMarker(string entityId) + { + if (_host == null || string.IsNullOrWhiteSpace(entityId)) return null; + foreach (var marker in _host.SceneRoot.GetComponentsInChildren(true)) + if (string.Equals(marker.EntityId, entityId, StringComparison.Ordinal)) return marker; + return null; + } + + private void EnsureVisuals() + { + if (_ring != null) return; + var root = new GameObject("Eve aim presentation"); + root.transform.SetParent(transform, false); + _ring = Line(root.transform, "View direction ring"); + _horizontal = Line(root.transform, "View direction horizontal"); + _vertical = Line(root.transform, "View direction vertical"); + } + + private LineRenderer Line(Transform parent, string name) + { + var child = new GameObject(name); + child.transform.SetParent(parent, false); + var line = child.AddComponent(); + line.useWorldSpace = true; + line.material = SharedMaterial(); + line.startColor = line.endColor = viewDotColor; + line.startWidth = line.endWidth = Mathf.Max(0.01f, width); + line.numCapVertices = 2; + return line; + } + + private Material SharedMaterial() + { + if (_material != null) return _material; + var shader = Shader.Find("Sprites/Default") ?? Shader.Find("Universal Render Pipeline/Unlit"); + if (shader == null) throw new InvalidOperationException("EveUnity requires an unlit shader for aim presentation."); + return _material = new Material(shader) { name = "EveUnity aim presentation" }; + } + + private static void Ring(LineRenderer line, Vector3 center, Vector3 right, Vector3 up, float radius) + { + const int segments = 33; + line.positionCount = segments; + for (var index = 0; index < segments; index++) + { + var angle = index / (float)(segments - 1) * Mathf.PI * 2f; + line.SetPosition(index, center + right * (Mathf.Cos(angle) * radius) + up * (Mathf.Sin(angle) * radius)); + } + } + + private static void Segment(LineRenderer line, Vector3 center, Vector3 axis, float radius) + { + line.positionCount = 2; + line.SetPosition(0, center - axis * radius); + line.SetPosition(1, center + axis * radius); + } + + private void SetVisible(bool visible) + { + if (_ring != null) _ring.enabled = visible; + if (_horizontal != null) _horizontal.enabled = visible; + if (_vertical != null) _vertical.enabled = visible; + } + + private void OnDestroy() + { + if (_material != null) Destroy(_material); + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs.meta new file mode 100644 index 0000000..66058c7 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 97c7b809756a4350a6e61c67bba69b82 diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs index 1323c80..56ed0ef 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs @@ -95,6 +95,9 @@ namespace GameCult.Eve.UnityScene var combatPresentation = GetComponent(); if (combatPresentation == null) combatPresentation = gameObject.AddComponent(); combatPresentation.Bind(this); + var aimPresentation = GetComponent(); + if (aimPresentation == null) aimPresentation = gameObject.AddComponent(); + aimPresentation.Bind(this); var thermal = GetComponent(); var thermalHud = GetComponent(); if (thermalHud == null) thermalHud = gameObject.AddComponent(); @@ -135,6 +138,16 @@ namespace GameCult.Eve.UnityScene return RequireRuntime().SubmitMoveVectorIntent(entityId, directionX, directionY, scalarValue, issuedAt); } + public EveSurfaceCommandRequest SubmitLookDirectionIntent( + string entityId, + float directionX, + float directionY, + float directionZ, + DateTimeOffset? issuedAt = null) + { + return RequireRuntime().SubmitLookDirectionIntent(entityId, directionX, directionY, directionZ, issuedAt); + } + public EveSurfaceCommandRequest SubmitFocusIntent( string entityId, DateTimeOffset? issuedAt = null) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs index c321815..ffd3f75 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs @@ -20,12 +20,16 @@ namespace GameCult.Eve.UnityScene [SerializeField] private string horizontalAxis = "Horizontal"; [SerializeField] private string verticalAxis = "Vertical"; [SerializeField] private string actionButton = "Fire1"; + [SerializeField] private string lookHorizontalAxis = "Mouse X"; + [SerializeField] private float defaultLookSensitivityRadians = 0.001f; [SerializeField] private float deadZone = 0.01f; [SerializeField] private float commandIntervalSeconds = 0.05f; [SerializeField] private bool submitZeroOnRelease = true; private float _nextCommandAt; private bool _wasMoving; + private float _lookYawRadians; + private bool _hasLookYaw; public EveUnityPlayableWorldClientHost? Host { @@ -94,6 +98,34 @@ namespace GameCult.Eve.UnityScene resolvedActionId); } + public EveSurfaceCommandRequest? SubmitLookInput( + float horizontalDelta, + DateTimeOffset? issuedAt = null) + { + var resolvedHost = ResolveHost(); + var world = resolvedHost?.ActiveWorld; + if (world == null || string.IsNullOrWhiteSpace(world.LookCommand) || + Mathf.Abs(horizontalDelta) <= 0.0001f) + return null; + + if (!_hasLookYaw) + { + _lookYawRadians = ResolveControlledYawRadians(resolvedHost!, world.PlayerEntityId); + _hasLookYaw = true; + } + var sensitivity = Mathf.Abs(world.LookSensitivityRadians) > 0.000001f + ? world.LookSensitivityRadians + : defaultLookSensitivityRadians; + _lookYawRadians += horizontalDelta * sensitivity; + var direction = EveUnityPlayableWorldLookDirection.FromPlanarYaw(_lookYawRadians); + return resolvedHost!.SubmitLookDirectionIntent( + world.PlayerEntityId, + direction.DirectionX, + 0f, + direction.DirectionZ, + issuedAt); + } + public static string ResolvePrimaryActionId(EveInputCapabilityDocument? capability) { if (capability == null) @@ -121,6 +153,8 @@ namespace GameCult.Eve.UnityScene _nextCommandAt = Time.unscaledTime + Math.Max(0.01f, commandIntervalSeconds); SubmitMoveVectorInput(Input.GetAxisRaw(horizontalAxis), Input.GetAxisRaw(verticalAxis)); + if (!string.IsNullOrWhiteSpace(lookHorizontalAxis)) + SubmitLookInput(Input.GetAxisRaw(lookHorizontalAxis)); if (!string.IsNullOrWhiteSpace(actionButton) && Input.GetButtonDown(actionButton)) SubmitPrimaryAction(); @@ -134,6 +168,29 @@ namespace GameCult.Eve.UnityScene host = GetComponent(); return host; } + + private static float ResolveControlledYawRadians(EveUnityPlayableWorldClientHost host, string entityId) + { + foreach (var marker in host.SceneRoot.GetComponentsInChildren(true)) + if (string.Equals(marker.EntityId, entityId, StringComparison.Ordinal)) + return marker.transform.eulerAngles.y * Mathf.Deg2Rad; + return 0f; + } + } + + public readonly struct EveUnityPlayableWorldLookDirection + { + public EveUnityPlayableWorldLookDirection(float directionX, float directionZ) + { + DirectionX = directionX; + DirectionZ = directionZ; + } + + public float DirectionX { get; } + public float DirectionZ { get; } + + public static EveUnityPlayableWorldLookDirection FromPlanarYaw(float yawRadians) => + new EveUnityPlayableWorldLookDirection(Mathf.Sin(yawRadians), Mathf.Cos(yawRadians)); } public readonly struct EveUnityPlayableWorldMoveVector diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs index bf44ee2..adbb76e 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs @@ -88,6 +88,16 @@ namespace GameCult.Eve.UnityScene return _connection.SubmitMoveVectorIntent(entityId, directionX, directionY, scalarValue, issuedAt); } + public EveSurfaceCommandRequest SubmitLookDirectionIntent( + string entityId, + float directionX, + float directionY, + float directionZ, + DateTimeOffset? issuedAt = null) + { + return _connection.SubmitLookDirectionIntent(entityId, directionX, directionY, directionZ, issuedAt); + } + public EveSurfaceCommandRequest SubmitFocusIntent( string entityId, DateTimeOffset? issuedAt = null) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs index a5d7e5c..c81e6b6 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs @@ -143,6 +143,16 @@ namespace GameCult.Eve.UnityScene return _client.SubmitMoveVectorIntent(entityId, directionX, directionY, scalarValue, issuedAt); } + public EveSurfaceCommandRequest SubmitLookDirectionIntent( + string entityId, + float directionX, + float directionY, + float directionZ, + DateTimeOffset? issuedAt = null) + { + return _client.SubmitLookDirectionIntent(entityId, directionX, directionY, directionZ, issuedAt); + } + public EveSurfaceCommandRequest SubmitFocusIntent( string entityId, DateTimeOffset? issuedAt = null) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneClientSession.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneClientSession.cs index e82cf56..473670d 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneClientSession.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneClientSession.cs @@ -83,6 +83,26 @@ namespace GameCult.Eve.UnityScene issuedAt); } + public EveSurfaceCommandRequest CreateLookDirectionIntent( + string entityId, + float directionX, + float directionY, + float directionZ, + DateTimeOffset? issuedAt = null) + { + var playableWorld = RequirePlayableWorld(); + return CreatePlayableWorldIntent( + playableWorld.LookCommand, + new Dictionary(StringComparer.Ordinal) + { + ["entityId"] = entityId ?? "", + ["directionX"] = FormatFloat(directionX), + ["directionY"] = FormatFloat(directionY), + ["directionZ"] = FormatFloat(directionZ) + }, + issuedAt); + } + public EveSurfaceCommandRequest CreateFocusIntent( string entityId, DateTimeOffset? issuedAt = null) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneProviderConnection.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneProviderConnection.cs index e21292c..3d4f18c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneProviderConnection.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneProviderConnection.cs @@ -221,6 +221,16 @@ namespace GameCult.Eve.UnityScene return Submit(_session.CreateMoveVectorIntent(entityId, directionX, directionY, scalarValue, issuedAt)); } + public EveSurfaceCommandRequest SubmitLookDirectionIntent( + string entityId, + float directionX, + float directionY, + float directionZ, + DateTimeOffset? issuedAt = null) + { + return Submit(_session.CreateLookDirectionIntent(entityId, directionX, directionY, directionZ, issuedAt)); + } + public EveSurfaceCommandRequest SubmitFocusIntent( string entityId, DateTimeOffset? issuedAt = null) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index b8a33a0..1b67179 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -136,7 +136,9 @@ namespace GameCult.Eve.UnityScene ParseVector3(worldRoot.GetProp("ambientLightColor")), ParseFloat(worldRoot.GetProp("ambientLightIntensity"), 1f), ParseFloat(worldRoot.GetProp("cameraNearClipPlane"), 0f), - ParseFloat(worldRoot.GetProp("cameraFarClipPlane"), 0f)); + ParseFloat(worldRoot.GetProp("cameraFarClipPlane"), 0f), + worldRoot.GetProp("lookCommand"), + ParseFloat(worldRoot.GetProp("lookSensitivityRadians"), 0f)); } private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component) @@ -357,7 +359,9 @@ namespace GameCult.Eve.UnityScene (float r, float g, float b) ambientLightColor = default, float ambientLightIntensity = 1f, float cameraNearClipPlane = 0f, - float cameraFarClipPlane = 0f) + float cameraFarClipPlane = 0f, + string lookCommand = "", + float lookSensitivityRadians = 0f) { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -388,6 +392,8 @@ namespace GameCult.Eve.UnityScene AmbientLightIntensity = ambientLightIntensity; CameraNearClipPlane = cameraNearClipPlane; CameraFarClipPlane = cameraFarClipPlane; + LookCommand = lookCommand ?? ""; + LookSensitivityRadians = lookSensitivityRadians; } public string WorldRootId { get; } @@ -438,6 +444,10 @@ namespace GameCult.Eve.UnityScene public string MovementCommand { get; } + public string LookCommand { get; } + + public float LookSensitivityRadians { get; } + public string FocusCommand { get; } public string TargetCommand { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAimPresentationTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAimPresentationTests.cs new file mode 100644 index 0000000..fbff51e --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAimPresentationTests.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using UnityEngine; + +#nullable enable + +namespace GameCult.Eve.UnityScene.Tests +{ + public sealed class EveUnityAimPresentationTests + { + [Test] + public void ProjectionPreservesProviderAimConvergenceSemantic() + { + var projection = new EveUnitySceneProjection( + "provider", "surface", "world", "commands", "receipt", "provider", null, + Node("root", "surface", Node("aim", "aim.presentation", new Dictionary + { + ["controlledEntityId"] = "run.zone.0.entity.4", + ["convergenceTargetEntityId"] = "run.zone.0.entity.9", + ["viewDotRole"] = "aim.marker.view-direction", + ["minimumConvergenceDistance"] = "50", + ["viewDotRadius"] = "0.8" + }))); + + var aim = EveUnityAimPresentation.Find(projection); + + Assert.That(aim, Is.Not.Null); + Assert.That(aim!.ControlledEntityId, Is.EqualTo("run.zone.0.entity.4")); + Assert.That(aim.ConvergenceTargetEntityId, Is.EqualTo("run.zone.0.entity.9")); + Assert.That(aim.MinimumConvergenceDistance, Is.EqualTo(50f)); + Assert.That(aim.ViewDotRadius, Is.EqualTo(0.8f)); + } + + [Test] + public void PlanarYawProducesPortableUnitLookDirection() + { + var forward = EveUnityPlayableWorldLookDirection.FromPlanarYaw(0f); + var right = EveUnityPlayableWorldLookDirection.FromPlanarYaw(Mathf.PI * 0.5f); + + Assert.That(forward.DirectionX, Is.EqualTo(0f).Within(0.0001f)); + Assert.That(forward.DirectionZ, Is.EqualTo(1f).Within(0.0001f)); + Assert.That(right.DirectionX, Is.EqualTo(1f).Within(0.0001f)); + Assert.That(right.DirectionZ, Is.EqualTo(0f).Within(0.0001f)); + } + + private static EveUnitySceneNode Node(string id, string kind, params EveUnitySceneNode[] children) => + Node(id, kind, Empty(), children); + + private static EveUnitySceneNode Node(string id, string kind, IReadOnlyDictionary props, params EveUnitySceneNode[] children) => + new EveUnitySceneNode(id, kind, "scene-node", props, Empty(), Empty(), 0, 0, + Array.Empty(), null, children); + + private static IReadOnlyDictionary Empty() => + new Dictionary(StringComparer.Ordinal); + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAimPresentationTests.cs.meta b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAimPresentationTests.cs.meta new file mode 100644 index 0000000..c2b439c --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAimPresentationTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2bcc030e48554a49accc4efc105c36b4 diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index c1f7051..3ebb32b 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -512,6 +512,18 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(moveVectorIntent.Payload.GetString("directionX"), Is.EqualTo("0.25")); Assert.That(moveVectorIntent.Payload.GetString("directionY"), Is.EqualTo("0.75")); Assert.That(moveVectorIntent.Payload.GetString("scalarValue"), Is.EqualTo("0.8")); + + var lookIntent = session.CreateLookDirectionIntent( + "player-vanguard", + 0.6f, + 0f, + 0.8f, + DateTimeOffset.Parse("2026-07-09T00:00:02Z")); + Assert.That(lookIntent.Payload.GetString("commandId"), Is.EqualTo("aetheria.daemon.look_intent")); + Assert.That(lookIntent.Payload.GetString("entityId"), Is.EqualTo("player-vanguard")); + Assert.That(lookIntent.Payload.GetString("directionX"), Is.EqualTo("0.6")); + Assert.That(lookIntent.Payload.GetString("directionY"), Is.EqualTo("0")); + Assert.That(lookIntent.Payload.GetString("directionZ"), Is.EqualTo("0.8")); } [Test] @@ -978,6 +990,16 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(request.Payload.GetDouble("directionY", 0), Is.EqualTo(0f).Within(0.0001f)); Assert.That(request.Payload.GetDouble("scalarValue", 0), Is.EqualTo(1f).Within(0.0001f)); + var controlledYaw = rootObject.GetComponentsInChildren(true) + .Single(marker => marker.EntityId == "player-vanguard") + .transform.eulerAngles.y * Mathf.Deg2Rad; + var look = driver.SubmitLookInput(1000f); + Assert.That(look, Is.Not.Null); + Assert.That(provider.Submitted.Count, Is.EqualTo(2)); + Assert.That(look!.Payload.GetString("commandId"), Is.EqualTo("aetheria.daemon.look_intent")); + Assert.That(look.Payload.GetDouble("directionX", 0), Is.EqualTo(Mathf.Sin(controlledYaw - 1f)).Within(0.0001f)); + Assert.That(look.Payload.GetDouble("directionZ", 0), Is.EqualTo(Mathf.Cos(controlledYaw - 1f)).Within(0.0001f)); + var movement = EveUnityPlayableWorldMoveVector.FromCameraRelativeInput(1f, 1f, null); Assert.That(movement.HasInput, Is.True); Assert.That(movement.ScalarValue, Is.EqualTo(1f).Within(0.0001f)); @@ -1752,6 +1774,8 @@ namespace GameCult.Eve.UnityScene.Tests ["excludedRenderChannels"] = "map", ["playerEntityId"] = "player-vanguard", ["movementCommand"] = "aetheria.daemon.move_intent", + ["lookCommand"] = "aetheria.daemon.look_intent", + ["lookSensitivityRadians"] = "-0.001", ["focusCommand"] = "aetheria.daemon.focus", ["targetCommand"] = "aetheria.daemon.target", ["actionCommand"] = "aetheria.daemon.use_equipment" diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index d6f6274..895bac3 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.23", + "version": "0.3.24", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 6d31f92fa350ccfc85575f31ed375afe074b99ca Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 05:57:28 +0100 Subject: [PATCH 051/292] Pin released authoritative aim lowerer --- ReleaseConsumerProject/Packages/manifest.json | 2 +- ReleaseConsumerProject/Packages/packages-lock.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index f146c06..db6b58e 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.23", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.24", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index bae8c86..a412205 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -142,7 +142,7 @@ "hash": "d0aa50d31f3b6da96bffc194fdc3aa358f37e332" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.23", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.24", "depth": 0, "source": "git", "dependencies": { From 58d87c628a261751439584263c18e93a7d8d8708 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 06:08:45 +0100 Subject: [PATCH 052/292] Harden generic authoritative aim lifecycle --- .../EveUnityAimPresentationRenderer.cs | 13 ++++-- ...veUnityGameObjectPlayableWorldSceneSink.cs | 14 +++++- .../Runtime/EveUnityPlayableWorldCameraRig.cs | 28 +++++++++++- .../EveUnityPlayableWorldClientHost.cs | 19 ++++++++ .../EveUnityPlayableWorldInputDriver.cs | 43 +++++++++++++++--- .../Runtime/EveUnityPlayableWorldRuntime.cs | 4 ++ .../Runtime/EveUnitySceneSurfaceLowerer.cs | 9 +++- .../EveUnitySceneSurfaceLowererTests.cs | 45 ++++++++++++++++++- .../org.gamecult.eve.unity-scene/package.json | 2 +- 9 files changed, 160 insertions(+), 17 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs index 25b8ce8..78c7788 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs @@ -52,10 +52,15 @@ namespace GameCult.Eve.UnityScene ViewDotPosition = controlled.transform.position + forward * distance; EnsureVisuals(); - var camera = Camera.main; - var right = camera == null ? Vector3.right : camera.transform.right; - var up = camera == null ? Vector3.up : camera.transform.up; - var center = ViewDotPosition + (camera == null ? Vector3.zero : -camera.transform.forward * 0.1f); + var camera = _host?.ActiveCameraTransform; + if (camera == null) + { + SetVisible(false); + return; + } + var right = camera.right; + var up = camera.up; + var center = ViewDotPosition - camera.forward * 0.1f; Ring(_ring!, center, right, up, Current.ViewDotRadius); Segment(_horizontal!, center, right, Current.ViewDotRadius * 1.6f); Segment(_vertical!, center, up, Current.ViewDotRadius * 1.6f); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs index 0035a83..84b7e8c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs @@ -20,7 +20,8 @@ namespace GameCult.Eve.UnityScene public sealed class EveUnityGameObjectPlayableWorldSceneSink : IEveUnityPlayableWorldSceneSink, IEveUnityEntityGenerationSink, - IEveUnityPresentedEntityRegistry + IEveUnityPresentedEntityRegistry, + IDisposable { private readonly Transform _root; private readonly IEveUnityGameObjectAssetProvider _assetProvider; @@ -179,6 +180,17 @@ namespace GameCult.Eve.UnityScene DestroyInstance(instance); } + public void Dispose() + { + foreach (var instance in _instances.Values) + if (instance != null) + DestroyInstance(instance); + _instances.Clear(); + _presentedById = new Dictionary(StringComparer.Ordinal); + _presentedByIndex = new Dictionary(); + CurrentGeneration = null; + } + private GameObject InstantiateEntity( EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset, diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index 69d717d..7b1334b 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -20,6 +20,7 @@ namespace GameCult.Eve.UnityScene [SerializeField] private float yawDegrees = 35f; [SerializeField] private float followDamping = 12f; private IEveUnityCameraRenderPolicySource? _renderPolicySource; + private EveUnityPlayableWorldClientHost? _cameraHost; private bool _ownsAmbientEnvironment; private AmbientMode _previousAmbientMode; private Color _previousAmbientLight; @@ -27,7 +28,12 @@ namespace GameCult.Eve.UnityScene public EveUnityPlayableWorldClientHost? Host { get => host; - set => host = value; + set + { + if (ReferenceEquals(host, value)) return; + ReleaseCamera(); + host = value; + } } public Transform? CameraTransform @@ -42,7 +48,11 @@ namespace GameCult.Eve.UnityScene set => _renderPolicySource = value; } - public void ReleaseRig() => RestoreAmbientEnvironment(); + public void ReleaseRig() + { + ReleaseCamera(); + RestoreAmbientEnvironment(); + } public bool ApplyRig(float deltaTime) { @@ -50,12 +60,14 @@ namespace GameCult.Eve.UnityScene var activeWorld = resolvedHost?.ActiveWorld; if (resolvedHost == null || activeWorld == null) { + ReleaseCamera(); RestoreAmbientEnvironment(); return false; } if (activeWorld.CameraRig == "planar.top-down-follow.v1" && !HasValidPlanarContract(activeWorld)) { + ReleaseCamera(); RestoreAmbientEnvironment(); return false; } @@ -69,6 +81,7 @@ namespace GameCult.Eve.UnityScene activeWorld.CameraRig != "arpg.orbital-follow.v1" && activeWorld.CameraRig != "third-person-orbit") { + ReleaseCamera(); return false; } @@ -78,7 +91,12 @@ namespace GameCult.Eve.UnityScene : activeWorld.CameraTargetEntityId; var player = FindEntity(resolvedHost, targetEntityId); if (player == null) + { + ReleaseCamera(); return false; + } + resolvedHost.ClaimWorldCamera(this, camera); + _cameraHost = resolvedHost; var cameraComponent = camera.GetComponent(); if (cameraComponent != null && _renderPolicySource != null) @@ -186,6 +204,12 @@ namespace GameCult.Eve.UnityScene _ownsAmbientEnvironment = false; } + private void ReleaseCamera() + { + _cameraHost?.ReleaseWorldCamera(this); + _cameraHost = null; + } + private static Bounds? CalculateVisualBounds(Transform player, int cullingMask) { Bounds? bounds = null; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs index 56ed0ef..bdc2469 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs @@ -21,6 +21,7 @@ namespace GameCult.Eve.UnityScene [SerializeField] private bool renderShotTrajectories = true; private float _nextRefreshAt; + private object? _cameraOwner; public EveUnityPlayableWorldRuntime? Runtime { get; private set; } @@ -37,6 +38,8 @@ namespace GameCult.Eve.UnityScene (providerSurfaceDocuments as IEveUnityInputCapabilitySource)?.CurrentInputCapability; public long ActiveVersion => Runtime?.ActiveVersion ?? 0; + public int ConnectionEpoch { get; private set; } + public Transform? ActiveCameraTransform { get; private set; } public event Action? FeedbackAvailable; public event Action? ShotAvailable; @@ -105,10 +108,24 @@ namespace GameCult.Eve.UnityScene thermal.Bind(this, Runtime.GameObjectAssetProvider as IEveUnityNativeAssetProvider); var presentation = Runtime.Connect(); + ConnectionEpoch++; _nextRefreshAt = Time.unscaledTime + Math.Max(0.01f, refreshIntervalSeconds); return presentation; } + internal void ClaimWorldCamera(object owner, Transform camera) + { + _cameraOwner = owner ?? throw new ArgumentNullException(nameof(owner)); + ActiveCameraTransform = camera != null ? camera : throw new ArgumentNullException(nameof(camera)); + } + + internal void ReleaseWorldCamera(object owner) + { + if (!ReferenceEquals(_cameraOwner, owner)) return; + _cameraOwner = null; + ActiveCameraTransform = null; + } + public EveUnityPlayableWorldPresentation Refresh() { RefreshProviderSources(); @@ -180,6 +197,8 @@ namespace GameCult.Eve.UnityScene } Runtime?.Dispose(); Runtime = null; + _cameraOwner = null; + ActiveCameraTransform = null; } private void OnFeedbackAvailable(EveUnityFeedbackEvent value) => FeedbackAvailable?.Invoke(value); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs index ffd3f75..9abd10a 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs @@ -30,11 +30,18 @@ namespace GameCult.Eve.UnityScene private bool _wasMoving; private float _lookYawRadians; private bool _hasLookYaw; + private float _pendingLookHorizontal; + private string _lookOwnerKey = ""; public EveUnityPlayableWorldClientHost? Host { get => host; - set => host = value; + set + { + if (ReferenceEquals(host, value)) return; + host = value; + ResetLookState(); + } } public Transform? CameraTransform @@ -104,14 +111,17 @@ namespace GameCult.Eve.UnityScene { var resolvedHost = ResolveHost(); var world = resolvedHost?.ActiveWorld; - if (world == null || string.IsNullOrWhiteSpace(world.LookCommand) || + if (world == null || !string.Equals(world.LookModel, "planar-yaw.v1", StringComparison.Ordinal) || + string.IsNullOrWhiteSpace(world.LookCommand) || Mathf.Abs(horizontalDelta) <= 0.0001f) return null; - if (!_hasLookYaw) + var ownerKey = $"{resolvedHost!.ConnectionEpoch}\n{world.WorldRootId}\n{world.PlayerEntityId}"; + if (!_hasLookYaw || !string.Equals(_lookOwnerKey, ownerKey, StringComparison.Ordinal)) { _lookYawRadians = ResolveControlledYawRadians(resolvedHost!, world.PlayerEntityId); _hasLookYaw = true; + _lookOwnerKey = ownerKey; } var sensitivity = Mathf.Abs(world.LookSensitivityRadians) > 0.000001f ? world.LookSensitivityRadians @@ -126,6 +136,15 @@ namespace GameCult.Eve.UnityScene issuedAt); } + public void QueueLookInput(float horizontalDelta) => _pendingLookHorizontal += horizontalDelta; + + public EveSurfaceCommandRequest? SubmitPendingLookInput(DateTimeOffset? issuedAt = null) + { + var pending = _pendingLookHorizontal; + _pendingLookHorizontal = 0f; + return SubmitLookInput(pending, issuedAt); + } + public static string ResolvePrimaryActionId(EveInputCapabilityDocument? capability) { if (capability == null) @@ -148,13 +167,17 @@ namespace GameCult.Eve.UnityScene private void Update() { - if (!driveInUpdate || Time.unscaledTime < _nextCommandAt) + if (!driveInUpdate) + return; + + if (!string.IsNullOrWhiteSpace(lookHorizontalAxis)) + QueueLookInput(Input.GetAxisRaw(lookHorizontalAxis)); + if (Time.unscaledTime < _nextCommandAt) return; _nextCommandAt = Time.unscaledTime + Math.Max(0.01f, commandIntervalSeconds); SubmitMoveVectorInput(Input.GetAxisRaw(horizontalAxis), Input.GetAxisRaw(verticalAxis)); - if (!string.IsNullOrWhiteSpace(lookHorizontalAxis)) - SubmitLookInput(Input.GetAxisRaw(lookHorizontalAxis)); + SubmitPendingLookInput(); if (!string.IsNullOrWhiteSpace(actionButton) && Input.GetButtonDown(actionButton)) SubmitPrimaryAction(); @@ -176,6 +199,14 @@ namespace GameCult.Eve.UnityScene return marker.transform.eulerAngles.y * Mathf.Deg2Rad; return 0f; } + + private void ResetLookState() + { + _lookYawRadians = 0f; + _hasLookYaw = false; + _pendingLookHorizontal = 0f; + _lookOwnerKey = ""; + } } public readonly struct EveUnityPlayableWorldLookDirection diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs index c81e6b6..28100fb 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs @@ -15,6 +15,7 @@ namespace GameCult.Eve.UnityScene private readonly IEveUnityEntitySoaViewDocumentSource? _entityViews; private readonly EveUnityEntitySoaPresenter _entityPresenter; private readonly IEveUnityPresentedEntityRegistry? _presentedEntities; + private IDisposable? _ownedSceneSink; private bool _entityViewsConnected; private bool _assetManifestConnected; @@ -106,6 +107,7 @@ namespace GameCult.Eve.UnityScene new EveUnityAssetRefResolver(), assetManifests); runtime.GameObjectAssetProvider = liveAssetProvider; + runtime._ownedSceneSink = sceneSink; return runtime; } @@ -198,6 +200,8 @@ namespace GameCult.Eve.UnityScene Disconnect(); _client.Dispose(); _assetManifestSource?.Dispose(); + _ownedSceneSink?.Dispose(); + _ownedSceneSink = null; } private void EnsureAssetManifestConnected() diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index 1b67179..2ef7006 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -138,7 +138,8 @@ namespace GameCult.Eve.UnityScene ParseFloat(worldRoot.GetProp("cameraNearClipPlane"), 0f), ParseFloat(worldRoot.GetProp("cameraFarClipPlane"), 0f), worldRoot.GetProp("lookCommand"), - ParseFloat(worldRoot.GetProp("lookSensitivityRadians"), 0f)); + ParseFloat(worldRoot.GetProp("lookSensitivityRadians"), 0f), + worldRoot.GetProp("lookModel")); } private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component) @@ -361,7 +362,8 @@ namespace GameCult.Eve.UnityScene float cameraNearClipPlane = 0f, float cameraFarClipPlane = 0f, string lookCommand = "", - float lookSensitivityRadians = 0f) + float lookSensitivityRadians = 0f, + string lookModel = "") { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -394,6 +396,7 @@ namespace GameCult.Eve.UnityScene CameraFarClipPlane = cameraFarClipPlane; LookCommand = lookCommand ?? ""; LookSensitivityRadians = lookSensitivityRadians; + LookModel = lookModel ?? ""; } public string WorldRootId { get; } @@ -448,6 +451,8 @@ namespace GameCult.Eve.UnityScene public float LookSensitivityRadians { get; } + public string LookModel { get; } + public string FocusCommand { get; } public string TargetCommand { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 3ebb32b..60d6a5b 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -993,13 +993,28 @@ namespace GameCult.Eve.UnityScene.Tests var controlledYaw = rootObject.GetComponentsInChildren(true) .Single(marker => marker.EntityId == "player-vanguard") .transform.eulerAngles.y * Mathf.Deg2Rad; - var look = driver.SubmitLookInput(1000f); + driver.QueueLookInput(250f); + driver.QueueLookInput(250f); + driver.QueueLookInput(500f); + var look = driver.SubmitPendingLookInput(); Assert.That(look, Is.Not.Null); Assert.That(provider.Submitted.Count, Is.EqualTo(2)); Assert.That(look!.Payload.GetString("commandId"), Is.EqualTo("aetheria.daemon.look_intent")); Assert.That(look.Payload.GetDouble("directionX", 0), Is.EqualTo(Mathf.Sin(controlledYaw - 1f)).Within(0.0001f)); Assert.That(look.Payload.GetDouble("directionZ", 0), Is.EqualTo(Mathf.Cos(controlledYaw - 1f)).Within(0.0001f)); + host.Connect(); + var rebasedYaw = rootObject.GetComponentsInChildren(true) + .Single(marker => marker.EntityId == "player-vanguard") + .transform.eulerAngles.y * Mathf.Deg2Rad; + var rebasedLook = driver.SubmitLookInput(1000f); + Assert.That(rebasedLook, Is.Not.Null); + Assert.That(provider.Submitted.Count, Is.EqualTo(3)); + Assert.That(rebasedLook!.Payload.GetDouble("directionX", 0), + Is.EqualTo(Mathf.Sin(rebasedYaw - 1f)).Within(0.0001f)); + Assert.That(rebasedLook.Payload.GetDouble("directionZ", 0), + Is.EqualTo(Mathf.Cos(rebasedYaw - 1f)).Within(0.0001f)); + var movement = EveUnityPlayableWorldMoveVector.FromCameraRelativeInput(1f, 1f, null); Assert.That(movement.HasInput, Is.True); Assert.That(movement.ScalarValue, Is.EqualTo(1f).Within(0.0001f)); @@ -1100,6 +1115,7 @@ namespace GameCult.Eve.UnityScene.Tests largeVisual.layer = 14; Assert.That(rig.ApplyRig(0f), Is.True); + Assert.That(host.ActiveCameraTransform, Is.SameAs(cameraObject.transform)); Assert.That(camera.cullingMask & (1 << 14), Is.Zero); Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); Assert.That(camera.orthographic, Is.False); @@ -1113,6 +1129,16 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(RenderSettings.ambientMode, Is.EqualTo(UnityEngine.Rendering.AmbientMode.Flat)); Assert.That(RenderSettings.ambientLight.r, Is.EqualTo(0.2f).Within(0.001f)); + var aim = hostObject.GetComponent(); + Assert.That(aim, Is.Not.Null); + var playerPosition = player.transform.position; + var playerRotation = player.transform.rotation; + aim.RefreshNow(); + Assert.That(aim.ViewDotVisible, Is.True); + Assert.That(Vector3.Distance(playerPosition, aim.ViewDotPosition), Is.EqualTo(50f).Within(0.001f)); + Assert.That(player.transform.position, Is.EqualTo(playerPosition)); + Assert.That(player.transform.rotation, Is.EqualTo(playerRotation)); + var selectedTarget = rootObject.GetComponentsInChildren() .Single(marker => marker.EntityId == "raider-scout"); Assert.That(EveUnityCombatPresentation.Find(host.ActiveProjection)?.SelectedTargetEntityId, @@ -1132,6 +1158,9 @@ namespace GameCult.Eve.UnityScene.Tests cameraObject.transform.position.x - beforeFollow.x, Is.EqualTo(10f * (1f - Mathf.Exp(-0.5f))).Within(0.001f)); rig.ReleaseRig(); + Assert.That(host.ActiveCameraTransform, Is.Null); + aim.RefreshNow(); + Assert.That(aim.ViewDotVisible, Is.False); Assert.That(RenderSettings.ambientMode, Is.EqualTo(ambientMode)); Assert.That(RenderSettings.ambientLight, Is.EqualTo(ambientLight)); } @@ -1710,6 +1739,19 @@ namespace GameCult.Eve.UnityScene.Tests "", "")); + playableChildren.Add(new EveSurfaceComponent( + "aetheria.daemon.game.aim", + "aim.presentation", + new Dictionary(StringComparer.Ordinal) + { + ["controlledEntityId"] = "player-vanguard", + ["convergenceTargetEntityId"] = "", + ["viewDotRole"] = "pilot.view-direction", + ["minimumConvergenceDistance"] = "50", + ["viewDotRadius"] = "0.8" + }, + Array.Empty())); + playableChildren.Add(new EveSurfaceComponent( "aetheria.daemon.game.combat", "combat.presentation", @@ -1775,6 +1817,7 @@ namespace GameCult.Eve.UnityScene.Tests ["playerEntityId"] = "player-vanguard", ["movementCommand"] = "aetheria.daemon.move_intent", ["lookCommand"] = "aetheria.daemon.look_intent", + ["lookModel"] = "planar-yaw.v1", ["lookSensitivityRadians"] = "-0.001", ["focusCommand"] = "aetheria.daemon.focus", ["targetCommand"] = "aetheria.daemon.target", diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 895bac3..609948c 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.24", + "version": "0.3.25", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 68aba5f67b4aef8cf1be734bd73e7f298e318e5c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 06:14:49 +0100 Subject: [PATCH 053/292] Prove released authoritative aim and camera channels --- .../PlayMode/GenericWorldCaptureTests.cs | 42 ++++++++++++++----- ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 +- .../PlayMode/GenericWorldCaptureTests.cs | 42 ++++++++++++++----- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 5 files changed, 66 insertions(+), 26 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 9f58eac..4723fe8 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -188,6 +188,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests EveUnityPlayableWorldClientHost host = null; EveUnityPlayableWorldRuntime runtime = null; WitnessReceipt movementReceipt = null; + WitnessReceipt lookReceipt = null; WitnessReceipt focusReceipt = null; WitnessReceipt actionReceipt = null; EveUnityShotReceipt observedShot = null; @@ -196,6 +197,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float hullRatioBefore = 0f; long initialVersion = 0; float movementDistance = 0f; + float aimDotDistance = 0f; try { @@ -344,18 +346,24 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests aim.y = 0; Assert.That(aim.sqrMagnitude, Is.GreaterThan(0.0001f)); aim.Normalize(); - var steerVersion = runtime.ActiveVersion; - var steer = runtime.SubmitMoveVectorIntent(playerId, aim.x, aim.z, 0.25f); - var steerDeadline = Time.realtimeSinceStartup + 12f; - while (Time.realtimeSinceStartup < steerDeadline && - (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != steer.CommandId || - !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase))) + var lookVersion = runtime.ActiveVersion; + var look = runtime.SubmitLookDirectionIntent(playerId, aim.x, 0f, aim.z); + var lookDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < lookDeadline && + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != look.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || + runtime.ActiveVersion < runtime.LastReceipt.SourceVersion || + Vector3.Dot(FindMarker(root, playerId).transform.forward, aim) < 0.99f)) { yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); } - AssertReconciledProviderReceipt(runtime.LastReceipt, steer.CommandId, - provider.Selection.ProviderId, provider.Selection.SurfaceId, steerVersion); + AssertReconciledProviderReceipt(runtime.LastReceipt, look.CommandId, + provider.Selection.ProviderId, provider.Selection.SurfaceId, lookVersion); + Assert.That(Vector3.Dot(FindMarker(root, playerId).transform.forward, aim), Is.GreaterThan(0.99f)); + lookReceipt = WitnessReceipt.From("look", runtime.LastReceipt); + var aimRenderer = root.GetComponent(); + Assert.That(aimRenderer, Is.Not.Null); var focusVersion = runtime.ActiveVersion; var inputDriver = root.GetComponent() ?? @@ -448,6 +456,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests cameraRig.CameraTransform = camera.transform; cameraRig.RenderPolicySource = provider; Assert.That(cameraRig.ApplyRig(0f), Is.True); + aimRenderer.RefreshNow(); + Assert.That(aimRenderer.ViewDotVisible, Is.True); + aimDotDistance = Vector3.Distance(playerMarker.transform.position, aimRenderer.ViewDotPosition); + Assert.That(aimDotDistance, Is.GreaterThanOrEqualTo(49.9f)); Assert.That(camera.cullingMask & (1 << mapLayer), Is.Zero); Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); var playerViewport = camera.WorldToViewportPoint(playerMarker.transform.position); @@ -504,6 +516,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests (camera.cullingMask & (1 << mapLayer)) == 0, (mapCamera.cullingMask & (1 << mapLayer)) != 0, movementReceipt, + lookReceipt, focusReceipt, actionReceipt, observedShot, @@ -511,7 +524,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests observedCombat?.ShieldRatio ?? shieldRatioBefore, hullRatioBefore, observedCombat?.HullRatio ?? hullRatioBefore, - (float)observedShot.LockQuality); + (float)observedShot.LockQuality, + aimDotDistance); } finally { @@ -541,6 +555,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests bool pilotCameraExcludesMapChannel, bool mapCameraIncludesMapChannel, WitnessReceipt movement, + WitnessReceipt look, WitnessReceipt targeting, WitnessReceipt action, EveUnityShotReceipt shot, @@ -548,7 +563,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float shieldRatioAfter, float hullRatioBefore, float hullRatioAfter, - float lockProgress) + float lockProgress, + float aimDotDistance) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); if (string.IsNullOrWhiteSpace(path)) return; @@ -557,6 +573,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests providerAdvertisement = true, providerAssets = true, movement = movement != null, + aimPresentation = look != null && aimDotDistance >= 49.9f, targeting = targeting != null, action = action != null, combatPresentation = shot != null, @@ -573,13 +590,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests initialVersion = initialVersion, finalVersion = finalVersion, movementDistance = movementDistance, + aimDotDistance = aimDotDistance, playerRendererCount = playerRendererCount, pilotChangedPixels = pilotChangedPixels, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, mapCameraIncludesMapChannel = mapCameraIncludesMapChannel, - receipts = new[] { movement, targeting, action } + receipts = new[] { movement, look, targeting, action } }; Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); File.WriteAllText(path, JsonUtility.ToJson(document, true)); @@ -591,6 +609,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public bool providerAdvertisement; public bool providerAssets; public bool movement; + public bool aimPresentation; public bool targeting; public bool action; public bool combatPresentation; @@ -607,6 +626,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public long initialVersion; public long finalVersion; public float movementDistance; + public float aimDotDistance; public int playerRendererCount; public int pilotChangedPixels; public int mapChannelRendererCount; diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index db6b58e..089ad45 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.24", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.25", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index a412205..c2e7ebf 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -142,7 +142,7 @@ "hash": "d0aa50d31f3b6da96bffc194fdc3aa358f37e332" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.24", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.25", "depth": 0, "source": "git", "dependencies": { @@ -151,7 +151,7 @@ "org.gamecult.eve.surface": "0.2.1", "com.unity.mathematics": "1.3.2" }, - "hash": "e43f2d74557dcbf2cc408893c7b7d7ac708a44a8" + "hash": "58d87c628a261751439584263c18e93a7d8d8708" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 9f58eac..4723fe8 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -188,6 +188,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests EveUnityPlayableWorldClientHost host = null; EveUnityPlayableWorldRuntime runtime = null; WitnessReceipt movementReceipt = null; + WitnessReceipt lookReceipt = null; WitnessReceipt focusReceipt = null; WitnessReceipt actionReceipt = null; EveUnityShotReceipt observedShot = null; @@ -196,6 +197,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float hullRatioBefore = 0f; long initialVersion = 0; float movementDistance = 0f; + float aimDotDistance = 0f; try { @@ -344,18 +346,24 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests aim.y = 0; Assert.That(aim.sqrMagnitude, Is.GreaterThan(0.0001f)); aim.Normalize(); - var steerVersion = runtime.ActiveVersion; - var steer = runtime.SubmitMoveVectorIntent(playerId, aim.x, aim.z, 0.25f); - var steerDeadline = Time.realtimeSinceStartup + 12f; - while (Time.realtimeSinceStartup < steerDeadline && - (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != steer.CommandId || - !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase))) + var lookVersion = runtime.ActiveVersion; + var look = runtime.SubmitLookDirectionIntent(playerId, aim.x, 0f, aim.z); + var lookDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < lookDeadline && + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != look.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || + runtime.ActiveVersion < runtime.LastReceipt.SourceVersion || + Vector3.Dot(FindMarker(root, playerId).transform.forward, aim) < 0.99f)) { yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); } - AssertReconciledProviderReceipt(runtime.LastReceipt, steer.CommandId, - provider.Selection.ProviderId, provider.Selection.SurfaceId, steerVersion); + AssertReconciledProviderReceipt(runtime.LastReceipt, look.CommandId, + provider.Selection.ProviderId, provider.Selection.SurfaceId, lookVersion); + Assert.That(Vector3.Dot(FindMarker(root, playerId).transform.forward, aim), Is.GreaterThan(0.99f)); + lookReceipt = WitnessReceipt.From("look", runtime.LastReceipt); + var aimRenderer = root.GetComponent(); + Assert.That(aimRenderer, Is.Not.Null); var focusVersion = runtime.ActiveVersion; var inputDriver = root.GetComponent() ?? @@ -448,6 +456,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests cameraRig.CameraTransform = camera.transform; cameraRig.RenderPolicySource = provider; Assert.That(cameraRig.ApplyRig(0f), Is.True); + aimRenderer.RefreshNow(); + Assert.That(aimRenderer.ViewDotVisible, Is.True); + aimDotDistance = Vector3.Distance(playerMarker.transform.position, aimRenderer.ViewDotPosition); + Assert.That(aimDotDistance, Is.GreaterThanOrEqualTo(49.9f)); Assert.That(camera.cullingMask & (1 << mapLayer), Is.Zero); Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); var playerViewport = camera.WorldToViewportPoint(playerMarker.transform.position); @@ -504,6 +516,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests (camera.cullingMask & (1 << mapLayer)) == 0, (mapCamera.cullingMask & (1 << mapLayer)) != 0, movementReceipt, + lookReceipt, focusReceipt, actionReceipt, observedShot, @@ -511,7 +524,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests observedCombat?.ShieldRatio ?? shieldRatioBefore, hullRatioBefore, observedCombat?.HullRatio ?? hullRatioBefore, - (float)observedShot.LockQuality); + (float)observedShot.LockQuality, + aimDotDistance); } finally { @@ -541,6 +555,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests bool pilotCameraExcludesMapChannel, bool mapCameraIncludesMapChannel, WitnessReceipt movement, + WitnessReceipt look, WitnessReceipt targeting, WitnessReceipt action, EveUnityShotReceipt shot, @@ -548,7 +563,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float shieldRatioAfter, float hullRatioBefore, float hullRatioAfter, - float lockProgress) + float lockProgress, + float aimDotDistance) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); if (string.IsNullOrWhiteSpace(path)) return; @@ -557,6 +573,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests providerAdvertisement = true, providerAssets = true, movement = movement != null, + aimPresentation = look != null && aimDotDistance >= 49.9f, targeting = targeting != null, action = action != null, combatPresentation = shot != null, @@ -573,13 +590,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests initialVersion = initialVersion, finalVersion = finalVersion, movementDistance = movementDistance, + aimDotDistance = aimDotDistance, playerRendererCount = playerRendererCount, pilotChangedPixels = pilotChangedPixels, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, mapCameraIncludesMapChannel = mapCameraIncludesMapChannel, - receipts = new[] { movement, targeting, action } + receipts = new[] { movement, look, targeting, action } }; Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); File.WriteAllText(path, JsonUtility.ToJson(document, true)); @@ -591,6 +609,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public bool providerAdvertisement; public bool providerAssets; public bool movement; + public bool aimPresentation; public bool targeting; public bool action; public bool combatPresentation; @@ -607,6 +626,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public long initialVersion; public long finalVersion; public float movementDistance; + public float aimDotDistance; public int playerRendererCount; public int pilotChangedPixels; public int mapChannelRendererCount; diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 213d96f..94d4ce2 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "e43f2d74557dcbf2cc408893c7b7d7ac708a44a8" +$expectedEveUnityCommit = "58d87c628a261751439584263c18e93a7d8d8708" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot $projectRoot = $ClientProject From 7f43cd4085df9414a9a249704b4e1f617837d9da Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 06:15:54 +0100 Subject: [PATCH 054/292] Record current Aetheria witness boundary --- docs/aetheria-released-package-witness.md | 80 ++++++++++++----------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index dbb9fd3..71ed37e 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -1,49 +1,55 @@ # Aetheria released-package witness -The combined cold-cache witness passes against the released generic runtime: +## Current result -- `org.gamecult.eve.unity-scene` `0.3.18`, commit - `ed21479e467faea8ff624f2d2f5a7d2fecf913f4` +The warm-cache released-package witness passes with: + +- `org.gamecult.eve.unity-scene` `0.3.25`, commit + `58d87c628a261751439584263c18e93a7d8d8708`; - `org.gamecult.cultlib` `1.0.13`, commit - `feb5c71513e71d681699f462fe3682b3168c6f73` + `feb5c71513e71d681699f462fe3682b3168c6f73`; +- the generic `ReleaseConsumerProject` client connected directly to the + Aetheria daemon. -Evidence is in `artifacts/render-channel-witness-gravity-cold-4`. The run used -`ReleaseConsumerProject`, resolved both dependencies from tagged Git URLs, and -finished under the unchanged 300-second Unity limit. - -The cache began with zero `.body` and `.partial` files. Managed -`cultmesh.content.v1` delivery atomically promoted exactly one body: - -- path: `asset-cache/0d5b575a18ecb2e8f4f1c309a7fa009262bdbb2ff671c14a4bf610d5052685dd.body` -- bytes: `13,006,384` -- SHA-256: `0d5b575a18ecb2e8f4f1c309a7fa009262bdbb2ff671c14a4bf610d5052685dd` -- final partial files: zero - -Snapshot delivery indexes the artifact manifest only. Aetheria's legacy -snapshot `asset_blob` schema, byte injection, path resolver, and Electron client -API are deleted; managed content sessions are the sole bundle-body transport. - -The released generic client instantiated the provider-authored prefab rather -than its primitive fallback. Movement, explicit targeting, and action each -reached a provider-owned `reconciled` receipt with matching command, -provider/surface identity, receipt identity, owner, authority, and observed -source version. +Evidence is in `artifacts/aetheria-daemon-native-0325-warm`. The PlayMode test +passed and recorded provider-owned reconciled movement, look, targeting, and +action receipts. The daemon-owned look direction reached the SoA body rotation, +and the generic aim presentation rendered at the advertised 50-unit minimum +convergence distance. Camera-channel facts: -- provider-authored player renderers: `12` -- pilot changed pixels: `6,265` -- map-channel renderers: `11` -- map changed pixels: `3,413` -- pilot camera excludes the advertised map layer -- map camera culls exactly the advertised map layer +- provider-authored player renderers: `12`; +- pilot changed pixels: `4,967`; +- map-channel renderers: `10`; +- map changed pixels: `4,766`; +- the pilot camera excludes the advertised map layer; +- the map camera renders exactly the advertised map layer. -Visual inspection confirms provider-authored ship geometry in the pilot view -without map glyphs, and only provider-authored glyphs in the map view. +Visual inspection confirms that map glyphs are absent from the pilot frame and +present in the map-only frame. The current material fallback is visually dark; +this is authority, package, asset, command, and camera-channel evidence, not a +claim that the native art pass is complete. Primary artifacts: -- `results.xml`: one passing PlayMode witness -- `runtime-witness.cold.json`: package, content, receipt, camera, and hash facts -- `aetheria-daemon-world.png`: fresh pilot-camera capture -- `aetheria-daemon-map.png`: fresh map-only capture +- `results.xml`: one passing PlayMode witness; +- `runtime-witness.warm.json`: package, content, receipt, aim, combat, and + camera facts; +- `aetheria-daemon-world.png`: pilot-camera capture; +- `aetheria-daemon-map.png`: map-only capture. + +## Cold delivery is not currently proven + +The current 13 MB Unity bundle times out from an empty cache. Its bytes are +still transported through batched snapshot records rather than the intended +mapped/network body transport. Increasing the Unity timeout would hide the +transport fault and is not an accepted fix. + +A cold witness may be claimed only when an empty cache receives the bundle over +the intended body transport, atomically promotes the hash-addressed `.body`, +leaves no `.partial` file, and completes the same released-client assertions. + +`artifacts/render-channel-witness-gravity-cold-4` records a historical 0.3.18 +cold run. It remains useful regression evidence, but it does not describe the +health of the current transport path. From 2945512e2a6e07e3b81362680f303ef6e6f01d2b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 06:34:39 +0100 Subject: [PATCH 055/292] Lower provider-owned native environments --- ...veUnityGameObjectPlayableWorldSceneSink.cs | 4 + .../Runtime/EveUnityPlayableWorldCameraRig.cs | 130 ++++++++++++++++-- .../EveUnityPlayableWorldClientHost.cs | 15 +- .../Runtime/EveUnitySceneSurfaceLowerer.cs | 19 ++- .../EveUnitySceneSurfaceLowererTests.cs | 90 ++++++++++-- .../org.gamecult.eve.unity-scene/package.json | 2 +- 6 files changed, 231 insertions(+), 29 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs index 84b7e8c..1fafbe0 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs @@ -184,7 +184,11 @@ namespace GameCult.Eve.UnityScene { foreach (var instance in _instances.Values) if (instance != null) + { + instance.transform.SetParent(null, false); + instance.SetActive(false); DestroyInstance(instance); + } _instances.Clear(); _presentedById = new Dictionary(StringComparer.Ordinal); _presentedByIndex = new Dictionary(); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index 7b1334b..e82a86a 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -24,6 +24,13 @@ namespace GameCult.Eve.UnityScene private bool _ownsAmbientEnvironment; private AmbientMode _previousAmbientMode; private Color _previousAmbientLight; + private float _previousAmbientIntensity; + private Material? _previousSkybox; + private DefaultReflectionMode _previousReflectionMode; + private Texture? _previousCustomReflection; + private float _previousReflectionIntensity; + private Camera? _environmentCamera; + private CameraClearFlags _previousCameraClearFlags; public EveUnityPlayableWorldClientHost? Host { @@ -31,7 +38,7 @@ namespace GameCult.Eve.UnityScene set { if (ReferenceEquals(host, value)) return; - ReleaseCamera(); + ReleaseRig(); host = value; } } @@ -39,7 +46,12 @@ namespace GameCult.Eve.UnityScene public Transform? CameraTransform { get => cameraTransform; - set => cameraTransform = value; + set + { + if (ReferenceEquals(cameraTransform, value)) return; + ReleaseRig(); + cameraTransform = value; + } } public IEveUnityCameraRenderPolicySource? RenderPolicySource @@ -71,11 +83,6 @@ namespace GameCult.Eve.UnityScene RestoreAmbientEnvironment(); return false; } - if (activeWorld.CameraRig == "planar.top-down-follow.v1") - ApplyAmbientEnvironment(activeWorld); - else - RestoreAmbientEnvironment(); - if (!string.IsNullOrWhiteSpace(activeWorld.CameraRig) && activeWorld.CameraRig != "planar.top-down-follow.v1" && activeWorld.CameraRig != "arpg.orbital-follow.v1" && @@ -95,10 +102,20 @@ namespace GameCult.Eve.UnityScene ReleaseCamera(); return false; } - resolvedHost.ClaimWorldCamera(this, camera); + if (!TryResolveEnvironment(resolvedHost, activeWorld, out var skybox, out var reflection)) + { + ReleaseRig(); + return false; + } + if (!resolvedHost.TryClaimWorldCamera(this, camera)) + { + RestoreAmbientEnvironment(); + return false; + } _cameraHost = resolvedHost; var cameraComponent = camera.GetComponent(); + ApplyAmbientEnvironment(activeWorld, cameraComponent, skybox, reflection); if (cameraComponent != null && _renderPolicySource != null) { var cullingMask = cameraComponent.cullingMask; @@ -180,20 +197,93 @@ namespace GameCult.Eve.UnityScene world.CameraNearClipPlane > 0f && world.CameraFarClipPlane > world.CameraNearClipPlane; - private void ApplyAmbientEnvironment(EveUnityPlayableWorldProjection world) + private static bool TryResolveEnvironment( + EveUnityPlayableWorldClientHost host, + EveUnityPlayableWorldProjection world, + out Material? skybox, + out Cubemap? reflection) + { + skybox = null; + reflection = null; + var assets = host.NativeAssetProvider; + if (!string.IsNullOrWhiteSpace(world.SkyboxAssetRef)) + { + skybox = assets?.ResolveAsset( + new EveUnityPlayableWorldAssetBinding( + world.SkyboxAssetRef, + "environment.skybox", + "provider-asset-ref"), + typeof(Material)) as Material; + if (skybox == null || skybox.shader == null || !skybox.shader.isSupported) + return false; + } + if (!string.IsNullOrWhiteSpace(world.ReflectionAssetRef)) + { + reflection = assets?.ResolveAsset( + new EveUnityPlayableWorldAssetBinding( + world.ReflectionAssetRef, + "environment.reflection", + "provider-asset-ref"), + typeof(Cubemap)) as Cubemap; + if (reflection == null) + return false; + } + return true; + } + + private void ApplyAmbientEnvironment( + EveUnityPlayableWorldProjection world, + Camera? camera, + Material? skybox, + Cubemap? reflection) { if (!_ownsAmbientEnvironment) { _previousAmbientMode = RenderSettings.ambientMode; _previousAmbientLight = RenderSettings.ambientLight; + _previousAmbientIntensity = RenderSettings.ambientIntensity; + _previousSkybox = RenderSettings.skybox; + _previousReflectionMode = RenderSettings.defaultReflectionMode; + _previousCustomReflection = RenderSettings.customReflectionTexture; + _previousReflectionIntensity = RenderSettings.reflectionIntensity; + _environmentCamera = camera; + if (camera != null) + _previousCameraClearFlags = camera.clearFlags; _ownsAmbientEnvironment = true; } - RenderSettings.ambientMode = AmbientMode.Flat; - RenderSettings.ambientLight = new Color( - Mathf.Max(0f, world.AmbientLightR * world.AmbientLightIntensity), - Mathf.Max(0f, world.AmbientLightG * world.AmbientLightIntensity), - Mathf.Max(0f, world.AmbientLightB * world.AmbientLightIntensity), - 1f); + if (skybox != null) + { + RenderSettings.skybox = skybox; + RenderSettings.ambientMode = AmbientMode.Skybox; + RenderSettings.ambientIntensity = Mathf.Max(0f, world.AmbientLightIntensity); + if (camera != null) + camera.clearFlags = CameraClearFlags.Skybox; + } + else + { + RenderSettings.skybox = _previousSkybox; + RenderSettings.ambientMode = AmbientMode.Flat; + RenderSettings.ambientIntensity = _previousAmbientIntensity; + RenderSettings.ambientLight = new Color( + Mathf.Max(0f, world.AmbientLightR * world.AmbientLightIntensity), + Mathf.Max(0f, world.AmbientLightG * world.AmbientLightIntensity), + Mathf.Max(0f, world.AmbientLightB * world.AmbientLightIntensity), + 1f); + if (_environmentCamera != null) + _environmentCamera.clearFlags = _previousCameraClearFlags; + } + if (reflection != null) + { + RenderSettings.defaultReflectionMode = DefaultReflectionMode.Custom; + RenderSettings.customReflectionTexture = reflection; + RenderSettings.reflectionIntensity = Mathf.Max(0f, world.ReflectionIntensity); + } + else + { + RenderSettings.defaultReflectionMode = _previousReflectionMode; + RenderSettings.customReflectionTexture = _previousCustomReflection; + RenderSettings.reflectionIntensity = _previousReflectionIntensity; + } } private void RestoreAmbientEnvironment() @@ -201,6 +291,16 @@ namespace GameCult.Eve.UnityScene if (!_ownsAmbientEnvironment) return; RenderSettings.ambientMode = _previousAmbientMode; RenderSettings.ambientLight = _previousAmbientLight; + RenderSettings.ambientIntensity = _previousAmbientIntensity; + RenderSettings.skybox = _previousSkybox; + RenderSettings.defaultReflectionMode = _previousReflectionMode; + RenderSettings.customReflectionTexture = _previousCustomReflection; + RenderSettings.reflectionIntensity = _previousReflectionIntensity; + if (_environmentCamera != null) + _environmentCamera.clearFlags = _previousCameraClearFlags; + _environmentCamera = null; + _previousSkybox = null; + _previousCustomReflection = null; _ownsAmbientEnvironment = false; } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs index bdc2469..17741f4 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs @@ -40,6 +40,8 @@ namespace GameCult.Eve.UnityScene public long ActiveVersion => Runtime?.ActiveVersion ?? 0; public int ConnectionEpoch { get; private set; } public Transform? ActiveCameraTransform { get; private set; } + public IEveUnityNativeAssetProvider? NativeAssetProvider => + Runtime?.GameObjectAssetProvider as IEveUnityNativeAssetProvider; public event Action? FeedbackAvailable; public event Action? ShotAvailable; @@ -113,10 +115,15 @@ namespace GameCult.Eve.UnityScene return presentation; } - internal void ClaimWorldCamera(object owner, Transform camera) + internal bool TryClaimWorldCamera(object owner, Transform camera) { - _cameraOwner = owner ?? throw new ArgumentNullException(nameof(owner)); - ActiveCameraTransform = camera != null ? camera : throw new ArgumentNullException(nameof(camera)); + if (owner == null) throw new ArgumentNullException(nameof(owner)); + if (camera == null) throw new ArgumentNullException(nameof(camera)); + if (_cameraOwner != null && !ReferenceEquals(_cameraOwner, owner)) + return false; + _cameraOwner = owner; + ActiveCameraTransform = camera; + return true; } internal void ReleaseWorldCamera(object owner) @@ -190,6 +197,8 @@ namespace GameCult.Eve.UnityScene public void Disconnect() { + if (_cameraOwner is EveUnityPlayableWorldCameraRig rig) + rig.ReleaseRig(); if (Runtime != null) { Runtime.FeedbackAvailable -= OnFeedbackAvailable; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index 2ef7006..acb92b1 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -139,7 +139,10 @@ namespace GameCult.Eve.UnityScene ParseFloat(worldRoot.GetProp("cameraFarClipPlane"), 0f), worldRoot.GetProp("lookCommand"), ParseFloat(worldRoot.GetProp("lookSensitivityRadians"), 0f), - worldRoot.GetProp("lookModel")); + worldRoot.GetProp("lookModel"), + worldRoot.GetProp("skyboxAssetRef"), + worldRoot.GetProp("reflectionAssetRef"), + ParseFloat(worldRoot.GetProp("reflectionIntensity"), 1f)); } private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component) @@ -363,7 +366,10 @@ namespace GameCult.Eve.UnityScene float cameraFarClipPlane = 0f, string lookCommand = "", float lookSensitivityRadians = 0f, - string lookModel = "") + string lookModel = "", + string skyboxAssetRef = "", + string reflectionAssetRef = "", + float reflectionIntensity = 1f) { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -397,6 +403,9 @@ namespace GameCult.Eve.UnityScene LookCommand = lookCommand ?? ""; LookSensitivityRadians = lookSensitivityRadians; LookModel = lookModel ?? ""; + SkyboxAssetRef = skyboxAssetRef ?? ""; + ReflectionAssetRef = reflectionAssetRef ?? ""; + ReflectionIntensity = reflectionIntensity; } public string WorldRootId { get; } @@ -453,6 +462,12 @@ namespace GameCult.Eve.UnityScene public string LookModel { get; } + public string SkyboxAssetRef { get; } + + public string ReflectionAssetRef { get; } + + public float ReflectionIntensity { get; } + public string FocusCommand { get; } public string TargetCommand { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 60d6a5b..a790608 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -7,6 +7,7 @@ using GameCult.Eve.Surface; using GameCult.Mesh; using NUnit.Framework; using UnityEngine; +using UnityEngine.Rendering; #nullable enable @@ -410,7 +411,10 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(projection.PlayableWorld.AmbientLightR, Is.EqualTo(0.2f)); Assert.That(projection.PlayableWorld.AmbientLightG, Is.EqualTo(0.2f)); Assert.That(projection.PlayableWorld.AmbientLightB, Is.EqualTo(0.2f)); - Assert.That(projection.PlayableWorld.AmbientLightIntensity, Is.EqualTo(1f)); + Assert.That(projection.PlayableWorld.AmbientLightIntensity, Is.EqualTo(1.46f)); + Assert.That(projection.PlayableWorld.SkyboxAssetRef, Is.Empty); + Assert.That(projection.PlayableWorld.ReflectionAssetRef, Is.Empty); + Assert.That(projection.PlayableWorld.ReflectionIntensity, Is.EqualTo(1f)); Assert.That(projection.PlayableWorld.ExcludedRenderChannels, Is.EqualTo(new[] { "map" })); Assert.That(projection.PlayableWorld.PlayerEntityId, Is.EqualTo("player-vanguard")); Assert.That(projection.PlayableWorld.MovementCommand, Is.EqualTo("aetheria.daemon.move_intent")); @@ -1075,16 +1079,33 @@ namespace GameCult.Eve.UnityScene.Tests var rootObject = new GameObject("generic-eve-world-root"); var hostObject = new GameObject("generic-eve-client"); var cameraObject = new GameObject("generic-eve-camera"); + var competingCameraObject = new GameObject("competing-eve-camera"); hostObject.SetActive(false); var ambientMode = RenderSettings.ambientMode; var ambientLight = RenderSettings.ambientLight; + var ambientIntensity = RenderSettings.ambientIntensity; + var previousSkybox = RenderSettings.skybox; + var previousReflectionMode = RenderSettings.defaultReflectionMode; + var previousCustomReflection = RenderSettings.customReflectionTexture; + var previousReflectionIntensity = RenderSettings.reflectionIntensity; + Material? skyboxMaterial = null; + Cubemap? reflectionCubemap = null; try { var provider = hostObject.AddComponent(); + var nativeAssets = hostObject.AddComponent(); + var skyboxShader = Shader.Find("Skybox/Procedural"); + Assert.That(skyboxShader, Is.Not.Null); + skyboxMaterial = new Material(skyboxShader!); + reflectionCubemap = new Cubemap(4, TextureFormat.RGBA32, false); + nativeAssets.Set("material.environment.skybox", skyboxMaterial); + nativeAssets.Set("texture.environment.reflection", reflectionCubemap); provider.Set( new EveUnitySceneProviderSurfaceDocument( - PlayableArpgDocument(), + PlayableArpgDocument( + skyboxAssetRef: "material.environment.skybox", + reflectionAssetRef: "texture.environment.reflection"), Advertisement("aetheria.daemon.game"), "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", 1), @@ -1094,7 +1115,7 @@ namespace GameCult.Eve.UnityScene.Tests "aetheria")); var host = hostObject.AddComponent(); - host.Configure(rootObject.transform, provider, provider, provider, provider); + host.Configure(rootObject.transform, provider, provider, provider, provider, nativeAssets); host.Connect(); hostObject.SetActive(true); @@ -1126,8 +1147,19 @@ namespace GameCult.Eve.UnityScene.Tests var viewport = camera.WorldToViewportPoint(player.transform.position); Assert.That(viewport.x, Is.EqualTo(0.9f).Within(0.001f)); Assert.That(viewport.y, Is.EqualTo(0.55f).Within(0.001f)); - Assert.That(RenderSettings.ambientMode, Is.EqualTo(UnityEngine.Rendering.AmbientMode.Flat)); - Assert.That(RenderSettings.ambientLight.r, Is.EqualTo(0.2f).Within(0.001f)); + Assert.That(RenderSettings.ambientMode, Is.EqualTo(UnityEngine.Rendering.AmbientMode.Skybox)); + Assert.That(RenderSettings.ambientIntensity, Is.EqualTo(1.46f).Within(0.001f)); + Assert.That(RenderSettings.skybox, Is.SameAs(skyboxMaterial)); + Assert.That(RenderSettings.defaultReflectionMode, Is.EqualTo(DefaultReflectionMode.Custom)); + Assert.That(RenderSettings.customReflectionTexture, Is.SameAs(reflectionCubemap)); + Assert.That(RenderSettings.reflectionIntensity, Is.EqualTo(1f).Within(0.001f)); + Assert.That(camera.clearFlags, Is.EqualTo(CameraClearFlags.Skybox)); + + var competingRig = competingCameraObject.AddComponent(); + competingRig.Host = host; + competingRig.CameraTransform = competingCameraObject.transform; + Assert.That(competingRig.ApplyRig(0f), Is.False); + Assert.That(host.ActiveCameraTransform, Is.SameAs(cameraObject.transform)); var aim = hostObject.GetComponent(); Assert.That(aim, Is.Not.Null); @@ -1157,17 +1189,34 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That( cameraObject.transform.position.x - beforeFollow.x, Is.EqualTo(10f * (1f - Mathf.Exp(-0.5f))).Within(0.001f)); - rig.ReleaseRig(); + rig.CameraTransform = null; Assert.That(host.ActiveCameraTransform, Is.Null); aim.RefreshNow(); Assert.That(aim.ViewDotVisible, Is.False); Assert.That(RenderSettings.ambientMode, Is.EqualTo(ambientMode)); Assert.That(RenderSettings.ambientLight, Is.EqualTo(ambientLight)); + Assert.That(RenderSettings.ambientIntensity, Is.EqualTo(ambientIntensity)); + Assert.That(RenderSettings.skybox, Is.SameAs(previousSkybox)); + Assert.That(RenderSettings.defaultReflectionMode, Is.EqualTo(previousReflectionMode)); + Assert.That(RenderSettings.customReflectionTexture, Is.SameAs(previousCustomReflection)); + Assert.That(RenderSettings.reflectionIntensity, Is.EqualTo(previousReflectionIntensity)); + nativeAssets.Clear(); + nativeAssets.Set("some.other.asset", skyboxMaterial); + Assert.That(rig.ApplyRig(0f), Is.False); + Assert.That(host.ActiveCameraTransform, Is.Null); } finally { RenderSettings.ambientMode = ambientMode; RenderSettings.ambientLight = ambientLight; + RenderSettings.ambientIntensity = ambientIntensity; + RenderSettings.skybox = previousSkybox; + RenderSettings.defaultReflectionMode = previousReflectionMode; + RenderSettings.customReflectionTexture = previousCustomReflection; + RenderSettings.reflectionIntensity = previousReflectionIntensity; + if (skyboxMaterial != null) UnityEngine.Object.DestroyImmediate(skyboxMaterial); + if (reflectionCubemap != null) UnityEngine.Object.DestroyImmediate(reflectionCubemap); + UnityEngine.Object.DestroyImmediate(competingCameraObject); UnityEngine.Object.DestroyImmediate(cameraObject); UnityEngine.Object.DestroyImmediate(hostObject); UnityEngine.Object.DestroyImmediate(rootObject); @@ -1680,7 +1729,9 @@ namespace GameCult.Eve.UnityScene.Tests private static EveSurfaceDocument PlayableArpgDocument( bool includeRaider = true, - string playerPosition = "0,0,0") + string playerPosition = "0,0,0", + string skyboxAssetRef = "", + string reflectionAssetRef = "") { var playableChildren = new List { @@ -1812,7 +1863,10 @@ namespace GameCult.Eve.UnityScene.Tests ["cameraNearClipPlane"] = "0.3", ["cameraFarClipPlane"] = "4096", ["ambientLightColor"] = "0.2,0.2,0.2", - ["ambientLightIntensity"] = "1", + ["ambientLightIntensity"] = "1.46", + ["skyboxAssetRef"] = skyboxAssetRef, + ["reflectionAssetRef"] = reflectionAssetRef, + ["reflectionIntensity"] = "1", ["excludedRenderChannels"] = "map", ["playerEntityId"] = "player-vanguard", ["movementCommand"] = "aetheria.daemon.move_intent", @@ -2378,6 +2432,26 @@ namespace GameCult.Eve.UnityScene.Tests public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) => _prefab; } + private sealed class FixedNativeAssetProviderComponent : MonoBehaviour, IEveUnityNativeAssetProvider + { + private readonly Dictionary _assets = + new Dictionary(StringComparer.Ordinal); + + public void Set(string assetRef, UnityEngine.Object asset) + { + _assets[assetRef ?? ""] = asset; + } + + public void Clear() => _assets.Clear(); + + public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) => null; + + public UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType) => + _assets.TryGetValue(asset?.AssetRef ?? "", out var value) && assetType.IsInstanceOfType(value) + ? value + : null; + } + private sealed class ObservingGameObjectAssetProvider : IEveUnityGameObjectAssetProvider { private readonly Action _onResolve; diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 609948c..b70ee8e 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.25", + "version": "0.3.26", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 7e2c2fa720ca3437aa1b569ffba3a57e6b6f05c5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 06:38:47 +0100 Subject: [PATCH 056/292] Make native environment leases scene-global --- .../Runtime/EveUnityPlayableWorldCameraRig.cs | 28 +++++++++++++- .../EveUnitySceneSurfaceLowererTests.cs | 37 ++++++++++++++++++- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index e82a86a..4ca6b15 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -12,6 +12,8 @@ namespace GameCult.Eve.UnityScene public sealed class EveUnityPlayableWorldCameraRig : MonoBehaviour { + private static EveUnityPlayableWorldCameraRig? _environmentOwner; + [SerializeField] private EveUnityPlayableWorldClientHost? host; [SerializeField] private Transform? cameraTransform; [SerializeField] private bool driveInLateUpdate = true; @@ -113,6 +115,12 @@ namespace GameCult.Eve.UnityScene return false; } _cameraHost = resolvedHost; + if (_environmentOwner != null && !ReferenceEquals(_environmentOwner, this)) + { + ReleaseCamera(); + return false; + } + _environmentOwner = this; var cameraComponent = camera.GetComponent(); ApplyAmbientEnvironment(activeWorld, cameraComponent, skybox, reflection); @@ -205,13 +213,19 @@ namespace GameCult.Eve.UnityScene { skybox = null; reflection = null; + if (!IsFinite(world.AmbientLightR) || + !IsFinite(world.AmbientLightG) || + !IsFinite(world.AmbientLightB) || + !IsFinite(world.AmbientLightIntensity) || + !IsFinite(world.ReflectionIntensity)) + return false; var assets = host.NativeAssetProvider; if (!string.IsNullOrWhiteSpace(world.SkyboxAssetRef)) { skybox = assets?.ResolveAsset( new EveUnityPlayableWorldAssetBinding( world.SkyboxAssetRef, - "environment.skybox", + "", "provider-asset-ref"), typeof(Material)) as Material; if (skybox == null || skybox.shader == null || !skybox.shader.isSupported) @@ -222,7 +236,7 @@ namespace GameCult.Eve.UnityScene reflection = assets?.ResolveAsset( new EveUnityPlayableWorldAssetBinding( world.ReflectionAssetRef, - "environment.reflection", + "", "provider-asset-ref"), typeof(Cubemap)) as Cubemap; if (reflection == null) @@ -231,12 +245,15 @@ namespace GameCult.Eve.UnityScene return true; } + private static bool IsFinite(float value) => !float.IsNaN(value) && !float.IsInfinity(value); + private void ApplyAmbientEnvironment( EveUnityPlayableWorldProjection world, Camera? camera, Material? skybox, Cubemap? reflection) { + var environmentChanged = !ReferenceEquals(RenderSettings.skybox, skybox); if (!_ownsAmbientEnvironment) { _previousAmbientMode = RenderSettings.ambientMode; @@ -284,11 +301,14 @@ namespace GameCult.Eve.UnityScene RenderSettings.customReflectionTexture = _previousCustomReflection; RenderSettings.reflectionIntensity = _previousReflectionIntensity; } + if (environmentChanged) + DynamicGI.UpdateEnvironment(); } private void RestoreAmbientEnvironment() { if (!_ownsAmbientEnvironment) return; + var environmentChanged = !ReferenceEquals(RenderSettings.skybox, _previousSkybox); RenderSettings.ambientMode = _previousAmbientMode; RenderSettings.ambientLight = _previousAmbientLight; RenderSettings.ambientIntensity = _previousAmbientIntensity; @@ -302,6 +322,10 @@ namespace GameCult.Eve.UnityScene _previousSkybox = null; _previousCustomReflection = null; _ownsAmbientEnvironment = false; + if (ReferenceEquals(_environmentOwner, this)) + _environmentOwner = null; + if (environmentChanged) + DynamicGI.UpdateEnvironment(); } private void ReleaseCamera() diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index a790608..41ab39c 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -1080,7 +1080,11 @@ namespace GameCult.Eve.UnityScene.Tests var hostObject = new GameObject("generic-eve-client"); var cameraObject = new GameObject("generic-eve-camera"); var competingCameraObject = new GameObject("competing-eve-camera"); + var secondaryRootObject = new GameObject("secondary-eve-world-root"); + var secondaryHostObject = new GameObject("secondary-eve-client"); + var secondaryCameraObject = new GameObject("secondary-eve-camera"); hostObject.SetActive(false); + secondaryHostObject.SetActive(false); var ambientMode = RenderSettings.ambientMode; var ambientLight = RenderSettings.ambientLight; var ambientIntensity = RenderSettings.ambientIntensity; @@ -1124,6 +1128,7 @@ namespace GameCult.Eve.UnityScene.Tests rig.CameraTransform = cameraObject.transform; rig.RenderPolicySource = new FixedRenderChannelPolicy("map", 14); var camera = cameraObject.AddComponent(); + camera.clearFlags = CameraClearFlags.SolidColor; camera.aspect = 16f / 9f; camera.orthographic = true; camera.lensShift = new Vector2(0.25f, -0.2f); @@ -1154,6 +1159,20 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(RenderSettings.customReflectionTexture, Is.SameAs(reflectionCubemap)); Assert.That(RenderSettings.reflectionIntensity, Is.EqualTo(1f).Within(0.001f)); Assert.That(camera.clearFlags, Is.EqualTo(CameraClearFlags.Skybox)); + Assert.That(nativeAssets.LastBinding?.AssetRef, Is.EqualTo("texture.environment.reflection")); + Assert.That(nativeAssets.LastBinding?.EntityKind, Is.Empty); + + var secondaryHost = secondaryHostObject.AddComponent(); + secondaryHost.Configure(secondaryRootObject.transform, provider, provider, provider, provider, nativeAssets); + secondaryHost.Connect(); + secondaryHostObject.SetActive(true); + secondaryCameraObject.AddComponent(); + var secondaryRig = secondaryCameraObject.AddComponent(); + secondaryRig.Host = secondaryHost; + secondaryRig.CameraTransform = secondaryCameraObject.transform; + Assert.That(secondaryRig.ApplyRig(0f), Is.False); + Assert.That(secondaryHost.ActiveCameraTransform, Is.Null); + Assert.That(RenderSettings.skybox, Is.SameAs(skyboxMaterial)); var competingRig = competingCameraObject.AddComponent(); competingRig.Host = host; @@ -1200,10 +1219,16 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(RenderSettings.defaultReflectionMode, Is.EqualTo(previousReflectionMode)); Assert.That(RenderSettings.customReflectionTexture, Is.SameAs(previousCustomReflection)); Assert.That(RenderSettings.reflectionIntensity, Is.EqualTo(previousReflectionIntensity)); + Assert.That(camera.clearFlags, Is.EqualTo(CameraClearFlags.SolidColor)); + rig.CameraTransform = cameraObject.transform; nativeAssets.Clear(); nativeAssets.Set("some.other.asset", skyboxMaterial); Assert.That(rig.ApplyRig(0f), Is.False); Assert.That(host.ActiveCameraTransform, Is.Null); + nativeAssets.Set("material.environment.skybox", reflectionCubemap); + nativeAssets.Set("texture.environment.reflection", skyboxMaterial); + Assert.That(rig.ApplyRig(0f), Is.False); + Assert.That(host.ActiveCameraTransform, Is.Null); } finally { @@ -1216,6 +1241,9 @@ namespace GameCult.Eve.UnityScene.Tests RenderSettings.reflectionIntensity = previousReflectionIntensity; if (skyboxMaterial != null) UnityEngine.Object.DestroyImmediate(skyboxMaterial); if (reflectionCubemap != null) UnityEngine.Object.DestroyImmediate(reflectionCubemap); + UnityEngine.Object.DestroyImmediate(secondaryCameraObject); + UnityEngine.Object.DestroyImmediate(secondaryHostObject); + UnityEngine.Object.DestroyImmediate(secondaryRootObject); UnityEngine.Object.DestroyImmediate(competingCameraObject); UnityEngine.Object.DestroyImmediate(cameraObject); UnityEngine.Object.DestroyImmediate(hostObject); @@ -2437,6 +2465,8 @@ namespace GameCult.Eve.UnityScene.Tests private readonly Dictionary _assets = new Dictionary(StringComparer.Ordinal); + public EveUnityPlayableWorldAssetBinding? LastBinding { get; private set; } + public void Set(string assetRef, UnityEngine.Object asset) { _assets[assetRef ?? ""] = asset; @@ -2446,10 +2476,13 @@ namespace GameCult.Eve.UnityScene.Tests public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) => null; - public UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType) => - _assets.TryGetValue(asset?.AssetRef ?? "", out var value) && assetType.IsInstanceOfType(value) + public UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType) + { + LastBinding = asset; + return _assets.TryGetValue(asset?.AssetRef ?? "", out var value) && assetType.IsInstanceOfType(value) ? value : null; + } } private sealed class ObservingGameObjectAssetProvider : IEveUnityGameObjectAssetProvider From 42d9b4a948da0cf78c65bc149a6d77d0e7b13a3a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 06:44:09 +0100 Subject: [PATCH 057/292] Prove released provider-owned environment lowering --- .../PlayMode/GenericWorldCaptureTests.cs | 17 ++++++++++++++ ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 ++-- .../PlayMode/GenericWorldCaptureTests.cs | 17 ++++++++++++++ docs/aetheria-released-package-witness.md | 23 ++++++++++--------- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 6 files changed, 50 insertions(+), 15 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 4723fe8..38a93db 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -456,6 +456,19 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests cameraRig.CameraTransform = camera.transform; cameraRig.RenderPolicySource = provider; Assert.That(cameraRig.ApplyRig(0f), Is.True); + var skybox = provider.ResolveAsset(new EveUnityPlayableWorldAssetBinding( + runtime.ActiveWorld.SkyboxAssetRef, "", "provider-asset-ref"), typeof(Material)) as Material; + var reflection = provider.ResolveAsset(new EveUnityPlayableWorldAssetBinding( + runtime.ActiveWorld.ReflectionAssetRef, "", "provider-asset-ref"), typeof(Cubemap)) as Cubemap; + Assert.That(runtime.ActiveWorld.SkyboxAssetRef, Is.Not.Empty); + Assert.That(runtime.ActiveWorld.ReflectionAssetRef, Is.Not.Empty); + Assert.That(skybox, Is.Not.Null); + Assert.That(reflection, Is.Not.Null); + Assert.That(RenderSettings.skybox, Is.SameAs(skybox)); + Assert.That(RenderSettings.customReflectionTexture, Is.SameAs(reflection)); + Assert.That(camera.clearFlags, Is.EqualTo(CameraClearFlags.Skybox)); + var environmentPresentation = RenderSettings.skybox == skybox && + RenderSettings.customReflectionTexture == reflection; aimRenderer.RefreshNow(); Assert.That(aimRenderer.ViewDotVisible, Is.True); aimDotDistance = Vector3.Distance(playerMarker.transform.position, aimRenderer.ViewDotPosition); @@ -515,6 +528,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests mapChangedPixels, (camera.cullingMask & (1 << mapLayer)) == 0, (mapCamera.cullingMask & (1 << mapLayer)) != 0, + environmentPresentation, movementReceipt, lookReceipt, focusReceipt, @@ -554,6 +568,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests int mapChangedPixels, bool pilotCameraExcludesMapChannel, bool mapCameraIncludesMapChannel, + bool environmentPresentation, WitnessReceipt movement, WitnessReceipt look, WitnessReceipt targeting, @@ -572,6 +587,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests { providerAdvertisement = true, providerAssets = true, + environmentPresentation = environmentPresentation, movement = movement != null, aimPresentation = look != null && aimDotDistance >= 49.9f, targeting = targeting != null, @@ -608,6 +624,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests { public bool providerAdvertisement; public bool providerAssets; + public bool environmentPresentation; public bool movement; public bool aimPresentation; public bool targeting; diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 089ad45..036f757 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.25", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.26", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index c2e7ebf..984f6f4 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -142,7 +142,7 @@ "hash": "d0aa50d31f3b6da96bffc194fdc3aa358f37e332" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.25", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.26", "depth": 0, "source": "git", "dependencies": { @@ -151,7 +151,7 @@ "org.gamecult.eve.surface": "0.2.1", "com.unity.mathematics": "1.3.2" }, - "hash": "58d87c628a261751439584263c18e93a7d8d8708" + "hash": "7e2c2fa720ca3437aa1b569ffba3a57e6b6f05c5" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 4723fe8..38a93db 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -456,6 +456,19 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests cameraRig.CameraTransform = camera.transform; cameraRig.RenderPolicySource = provider; Assert.That(cameraRig.ApplyRig(0f), Is.True); + var skybox = provider.ResolveAsset(new EveUnityPlayableWorldAssetBinding( + runtime.ActiveWorld.SkyboxAssetRef, "", "provider-asset-ref"), typeof(Material)) as Material; + var reflection = provider.ResolveAsset(new EveUnityPlayableWorldAssetBinding( + runtime.ActiveWorld.ReflectionAssetRef, "", "provider-asset-ref"), typeof(Cubemap)) as Cubemap; + Assert.That(runtime.ActiveWorld.SkyboxAssetRef, Is.Not.Empty); + Assert.That(runtime.ActiveWorld.ReflectionAssetRef, Is.Not.Empty); + Assert.That(skybox, Is.Not.Null); + Assert.That(reflection, Is.Not.Null); + Assert.That(RenderSettings.skybox, Is.SameAs(skybox)); + Assert.That(RenderSettings.customReflectionTexture, Is.SameAs(reflection)); + Assert.That(camera.clearFlags, Is.EqualTo(CameraClearFlags.Skybox)); + var environmentPresentation = RenderSettings.skybox == skybox && + RenderSettings.customReflectionTexture == reflection; aimRenderer.RefreshNow(); Assert.That(aimRenderer.ViewDotVisible, Is.True); aimDotDistance = Vector3.Distance(playerMarker.transform.position, aimRenderer.ViewDotPosition); @@ -515,6 +528,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests mapChangedPixels, (camera.cullingMask & (1 << mapLayer)) == 0, (mapCamera.cullingMask & (1 << mapLayer)) != 0, + environmentPresentation, movementReceipt, lookReceipt, focusReceipt, @@ -554,6 +568,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests int mapChangedPixels, bool pilotCameraExcludesMapChannel, bool mapCameraIncludesMapChannel, + bool environmentPresentation, WitnessReceipt movement, WitnessReceipt look, WitnessReceipt targeting, @@ -572,6 +587,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests { providerAdvertisement = true, providerAssets = true, + environmentPresentation = environmentPresentation, movement = movement != null, aimPresentation = look != null && aimDotDistance >= 49.9f, targeting = targeting != null, @@ -608,6 +624,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests { public bool providerAdvertisement; public bool providerAssets; + public bool environmentPresentation; public bool movement; public bool aimPresentation; public bool targeting; diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 71ed37e..99ad2a7 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,32 +4,33 @@ The warm-cache released-package witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.25`, commit - `58d87c628a261751439584263c18e93a7d8d8708`; +- `org.gamecult.eve.unity-scene` `0.3.26`, commit + `7e2c2fa720ca3437aa1b569ffba3a57e6b6f05c5`; - `org.gamecult.cultlib` `1.0.13`, commit `feb5c71513e71d681699f462fe3682b3168c6f73`; - the generic `ReleaseConsumerProject` client connected directly to the Aetheria daemon. -Evidence is in `artifacts/aetheria-daemon-native-0325-warm`. The PlayMode test +Evidence is in `artifacts/aetheria-daemon-native-0326-warm`. The PlayMode test passed and recorded provider-owned reconciled movement, look, targeting, and action receipts. The daemon-owned look direction reached the SoA body rotation, and the generic aim presentation rendered at the advertised 50-unit minimum -convergence distance. +convergence distance. The advertised skybox material and reflection cubemap +resolved from the provider bundle as their required native types and became the +active camera rig's leased Unity environment. Camera-channel facts: - provider-authored player renderers: `12`; -- pilot changed pixels: `4,967`; -- map-channel renderers: `10`; -- map changed pixels: `4,766`; +- pilot changed pixels: `230,332`; +- map-channel renderers: `11`; +- map changed pixels: `5,496`; - the pilot camera excludes the advertised map layer; - the map camera renders exactly the advertised map layer. Visual inspection confirms that map glyphs are absent from the pilot frame and -present in the map-only frame. The current material fallback is visually dark; -this is authority, package, asset, command, and camera-channel evidence, not a -claim that the native art pass is complete. +present in the map-only frame. The pilot presentation remains visually dark; +the native art and lighting pass is not complete. Primary artifacts: @@ -41,7 +42,7 @@ Primary artifacts: ## Cold delivery is not currently proven -The current 13 MB Unity bundle times out from an empty cache. Its bytes are +The current 45.2 MB Unity bundle times out from an empty cache. Its bytes are still transported through batched snapshot records rather than the intended mapped/network body transport. Increasing the Unity timeout would hide the transport fault and is not an accepted fix. diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 94d4ce2..2b8b81f 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "58d87c628a261751439584263c18e93a7d8d8708" +$expectedEveUnityCommit = "7e2c2fa720ca3437aa1b569ffba3a57e6b6f05c5" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot $projectRoot = $ClientProject From 63a41712c5a68ce835bc1820980fc4786c34eb7b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 08:36:03 +0100 Subject: [PATCH 058/292] Lower provider-authored world key lights --- .../org.gamecult.eve.unity-scene/README.md | 6 ++ .../Runtime/EveUnityPlayableWorldCameraRig.cs | 60 ++++++++++++++++++- .../Runtime/EveUnitySceneSurfaceLowerer.cs | 31 +++++++++- .../EveUnitySceneSurfaceLowererTests.cs | 12 ++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 5 files changed, 107 insertions(+), 4 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index 27f94bd..5ad692f 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -29,6 +29,12 @@ own which semantic content belongs in a view, while EveUnity owns the native layer mapping and subtracts only those layers from the camera's configured culling mask. +World surfaces may also advertise a presentation-only directional key light +through `keyLightDirection`, `keyLightColor`, and `keyLightIntensity`. +EveUnity lowers those generic values into a transient Unity light and removes +it when the world camera lease ends. The provider owns the lighting design; +the light never becomes physics, visibility, or gameplay authority. + Retained provider feedback is lowered without becoming Unity authority. `EveUnityFeedbackPresenter` emits each new `feedback.event` identity once, and `EveUnityShotReceiptPresenter` does the same for `shot.receipt` trajectories. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index 4ca6b15..7b50fad 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -33,6 +33,7 @@ namespace GameCult.Eve.UnityScene private float _previousReflectionIntensity; private Camera? _environmentCamera; private CameraClearFlags _previousCameraClearFlags; + private GameObject? _keyLightObject; public EveUnityPlayableWorldClientHost? Host { @@ -124,6 +125,7 @@ namespace GameCult.Eve.UnityScene var cameraComponent = camera.GetComponent(); ApplyAmbientEnvironment(activeWorld, cameraComponent, skybox, reflection); + ApplyKeyLight(activeWorld); if (cameraComponent != null && _renderPolicySource != null) { var cullingMask = cameraComponent.cullingMask; @@ -217,7 +219,14 @@ namespace GameCult.Eve.UnityScene !IsFinite(world.AmbientLightG) || !IsFinite(world.AmbientLightB) || !IsFinite(world.AmbientLightIntensity) || - !IsFinite(world.ReflectionIntensity)) + !IsFinite(world.ReflectionIntensity) || + !IsFinite(world.KeyLightDirectionX) || + !IsFinite(world.KeyLightDirectionY) || + !IsFinite(world.KeyLightDirectionZ) || + !IsFinite(world.KeyLightColorR) || + !IsFinite(world.KeyLightColorG) || + !IsFinite(world.KeyLightColorB) || + !IsFinite(world.KeyLightIntensity)) return false; var assets = host.NativeAssetProvider; if (!string.IsNullOrWhiteSpace(world.SkyboxAssetRef)) @@ -245,6 +254,54 @@ namespace GameCult.Eve.UnityScene return true; } + private void ApplyKeyLight(EveUnityPlayableWorldProjection world) + { + var direction = new Vector3( + world.KeyLightDirectionX, + world.KeyLightDirectionY, + world.KeyLightDirectionZ); + if (world.KeyLightIntensity <= 0f || direction.sqrMagnitude <= 0.000001f) + { + ReleaseKeyLight(); + return; + } + + if (_keyLightObject == null) + { + _keyLightObject = new GameObject("Eve World Key Light"); + _keyLightObject.transform.SetParent(transform, worldPositionStays: false); + var created = _keyLightObject.AddComponent(); + created.type = LightType.Directional; + created.shadows = LightShadows.None; + } + + var light = _keyLightObject.GetComponent(); + light.color = new Color( + Mathf.Max(0f, world.KeyLightColorR), + Mathf.Max(0f, world.KeyLightColorG), + Mathf.Max(0f, world.KeyLightColorB), + 1f); + light.intensity = Mathf.Max(0f, world.KeyLightIntensity); + var normalized = direction.normalized; + var up = Mathf.Abs(Vector3.Dot(normalized, Vector3.up)) > 0.999f + ? Vector3.forward + : Vector3.up; + _keyLightObject.transform.rotation = Quaternion.LookRotation(normalized, up); + _keyLightObject.SetActive(true); + } + + private void ReleaseKeyLight() + { + if (_keyLightObject == null) + return; + _keyLightObject.SetActive(false); + if (Application.isPlaying) + Destroy(_keyLightObject); + else + DestroyImmediate(_keyLightObject); + _keyLightObject = null; + } + private static bool IsFinite(float value) => !float.IsNaN(value) && !float.IsInfinity(value); private void ApplyAmbientEnvironment( @@ -307,6 +364,7 @@ namespace GameCult.Eve.UnityScene private void RestoreAmbientEnvironment() { + ReleaseKeyLight(); if (!_ownsAmbientEnvironment) return; var environmentChanged = !ReferenceEquals(RenderSettings.skybox, _previousSkybox); RenderSettings.ambientMode = _previousAmbientMode; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index acb92b1..5607764 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -142,7 +142,10 @@ namespace GameCult.Eve.UnityScene worldRoot.GetProp("lookModel"), worldRoot.GetProp("skyboxAssetRef"), worldRoot.GetProp("reflectionAssetRef"), - ParseFloat(worldRoot.GetProp("reflectionIntensity"), 1f)); + ParseFloat(worldRoot.GetProp("reflectionIntensity"), 1f), + ParseVector3(worldRoot.GetProp("keyLightDirection")), + ParseVector3(worldRoot.GetProp("keyLightColor")), + ParseFloat(worldRoot.GetProp("keyLightIntensity"), 0f)); } private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component) @@ -369,7 +372,10 @@ namespace GameCult.Eve.UnityScene string lookModel = "", string skyboxAssetRef = "", string reflectionAssetRef = "", - float reflectionIntensity = 1f) + float reflectionIntensity = 1f, + (float x, float y, float z) keyLightDirection = default, + (float r, float g, float b) keyLightColor = default, + float keyLightIntensity = 0f) { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -406,6 +412,13 @@ namespace GameCult.Eve.UnityScene SkyboxAssetRef = skyboxAssetRef ?? ""; ReflectionAssetRef = reflectionAssetRef ?? ""; ReflectionIntensity = reflectionIntensity; + KeyLightDirectionX = keyLightDirection.x; + KeyLightDirectionY = keyLightDirection.y; + KeyLightDirectionZ = keyLightDirection.z; + KeyLightColorR = keyLightColor.r; + KeyLightColorG = keyLightColor.g; + KeyLightColorB = keyLightColor.b; + KeyLightIntensity = keyLightIntensity; } public string WorldRootId { get; } @@ -468,6 +481,20 @@ namespace GameCult.Eve.UnityScene public float ReflectionIntensity { get; } + public float KeyLightDirectionX { get; } + + public float KeyLightDirectionY { get; } + + public float KeyLightDirectionZ { get; } + + public float KeyLightColorR { get; } + + public float KeyLightColorG { get; } + + public float KeyLightColorB { get; } + + public float KeyLightIntensity { get; } + public string FocusCommand { get; } public string TargetCommand { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 41ab39c..0f7d926 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -1159,6 +1159,14 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(RenderSettings.customReflectionTexture, Is.SameAs(reflectionCubemap)); Assert.That(RenderSettings.reflectionIntensity, Is.EqualTo(1f).Within(0.001f)); Assert.That(camera.clearFlags, Is.EqualTo(CameraClearFlags.Skybox)); + var keyLightTransform = hostObject.transform.Find("Eve World Key Light"); + Assert.That(keyLightTransform, Is.Not.Null); + var keyLight = keyLightTransform.GetComponent(); + Assert.That(keyLight.type, Is.EqualTo(LightType.Directional)); + Assert.That(keyLight.intensity, Is.EqualTo(0.75f).Within(0.001f)); + Assert.That(keyLight.color, Is.EqualTo(new Color(1f, 0.95f, 0.9f, 1f))); + Assert.That(Vector3.Dot(keyLightTransform.forward, new Vector3(0.4f, -1f, 0.25f).normalized), + Is.GreaterThan(0.999f)); Assert.That(nativeAssets.LastBinding?.AssetRef, Is.EqualTo("texture.environment.reflection")); Assert.That(nativeAssets.LastBinding?.EntityKind, Is.Empty); @@ -1210,6 +1218,7 @@ namespace GameCult.Eve.UnityScene.Tests Is.EqualTo(10f * (1f - Mathf.Exp(-0.5f))).Within(0.001f)); rig.CameraTransform = null; Assert.That(host.ActiveCameraTransform, Is.Null); + Assert.That(hostObject.transform.Find("Eve World Key Light"), Is.Null); aim.RefreshNow(); Assert.That(aim.ViewDotVisible, Is.False); Assert.That(RenderSettings.ambientMode, Is.EqualTo(ambientMode)); @@ -1895,6 +1904,9 @@ namespace GameCult.Eve.UnityScene.Tests ["skyboxAssetRef"] = skyboxAssetRef, ["reflectionAssetRef"] = reflectionAssetRef, ["reflectionIntensity"] = "1", + ["keyLightDirection"] = "0.4,-1,0.25", + ["keyLightColor"] = "1,0.95,0.9", + ["keyLightIntensity"] = "0.75", ["excludedRenderChannels"] = "map", ["playerEntityId"] = "player-vanguard", ["movementCommand"] = "aetheria.daemon.move_intent", diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index b70ee8e..f19cb5a 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.26", + "version": "0.3.27", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From ebe132c7b1cb50fcd6f87d5aef8ca1b0b63d22aa Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 09:08:44 +0100 Subject: [PATCH 059/292] Prove provider-owned Aetheria presentation --- .../PlayMode/GenericWorldCaptureTests.cs | 232 +++++++++++++++++- ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 +- .../PlayMode/GenericWorldCaptureTests.cs | 232 +++++++++++++++++- docs/aetheria-released-package-witness.md | 20 +- scripts/run-aetheria-daemon-world-witness.ps1 | 7 +- 6 files changed, 470 insertions(+), 27 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 38a93db..6e802c3 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -181,7 +181,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var root = new GameObject("Generic Eve World Projection"); var cameraObject = new GameObject("Generic Eve Capture Camera"); var mapCameraObject = new GameObject("Generic Eve Map Camera"); - var lightObject = new GameObject("Generic Eve World Light"); RenderTexture target = null; Texture2D pixels = null; EveUnityCultMeshPlayableWorldProvider provider = null; @@ -415,10 +414,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests if (observedShot.Hit && (observedShot.AppliedDamage > 0 || observedShot.ShieldAbsorbedDamage > 0)) Assert.That(combatRenderer.HitMarkerVisible, Is.True); - var light = lightObject.AddComponent(); - light.type = LightType.Directional; - light.intensity = 1.4f; - lightObject.transform.rotation = Quaternion.Euler(45f, -35f, 0f); var camera = cameraObject.AddComponent(); camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(0.035f, 0.055f, 0.08f, 1f); @@ -478,6 +473,21 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var playerViewport = camera.WorldToViewportPoint(playerMarker.transform.position); Assert.That(playerViewport.x, Is.EqualTo(0.9f).Within(0.01f)); Assert.That(playerViewport.y, Is.EqualTo(0.55f).Within(0.01f)); + MeasureViewportCoverage(camera, playerRenderers, out var playerViewportWidth, out var playerViewportHeight); + var playerRendererFacts = playerRenderers + .Select(renderer => RendererFact.From(camera, renderer)) + .ToArray(); + var texturedPlayerMaterialCount = playerRenderers + .SelectMany(renderer => renderer.sharedMaterials) + .Where(material => material != null) + .Distinct() + .Count(HasAnyTexture); + var directionalLightIntensity = UnityEngine.Object.FindObjectsOfType() + .Where(light => light != null && light.enabled && light.type == LightType.Directional) + .Sum(light => light.intensity); + Assert.That(directionalLightIntensity, + Is.EqualTo(runtime.ActiveWorld.KeyLightIntensity).Within(0.001f), + "The live witness contains directional lighting not owned by the advertised provider surface."); var mapCamera = mapCameraObject.AddComponent(); mapCamera.cullingMask = 1 << mapLayer; @@ -489,10 +499,25 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); pixels.Apply(); var pilotChangedPixels = CountChangedPixels(pixels, camera.backgroundColor); + MeasureLuminance(pixels, out var pilotAverageLuminance, out var pilotBrightPixelCount); Assert.That(pilotChangedPixels, Is.GreaterThan(25), "The pilot camera rendered no provider-authored world pixels."); Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(capturePath))); File.WriteAllBytes(capturePath, pixels.EncodeToPNG()); Assert.That(new FileInfo(capturePath).Length, Is.GreaterThan(1024)); + MeasureRendererOutputs( + camera, + target, + pixels, + root.GetComponentsInChildren(includeInactive: true), + playerRenderers, + playerRendererFacts); + var playerMapRendererFacts = playerRendererFacts + .Where(fact => fact.layer == mapLayer) + .ToArray(); + Assert.That(playerMapRendererFacts, Is.Not.Empty, + "The provider-authored player prefab has no renderer on its advertised map channel."); + Assert.That(playerMapRendererFacts.Sum(fact => fact.renderedPixelCount), Is.Zero, + "A provider-prefab map-channel renderer leaked into the pilot camera."); mapRenderers = mapRenderers.Where(renderer => renderer != null).ToList(); Assert.That(mapRenderers, Is.Not.Empty, @@ -539,7 +564,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests hullRatioBefore, observedCombat?.HullRatio ?? hullRatioBefore, (float)observedShot.LockQuality, - aimDotDistance); + aimDotDistance, + playerViewportWidth, + playerViewportHeight, + texturedPlayerMaterialCount, + directionalLightIntensity, + pilotAverageLuminance, + pilotBrightPixelCount, + playerRendererFacts); } finally { @@ -551,7 +583,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests if (target != null) UnityEngine.Object.DestroyImmediate(target); if (pixels != null) UnityEngine.Object.DestroyImmediate(pixels); host?.Disconnect(); - UnityEngine.Object.DestroyImmediate(lightObject); UnityEngine.Object.DestroyImmediate(mapCameraObject); UnityEngine.Object.DestroyImmediate(cameraObject); UnityEngine.Object.DestroyImmediate(root); @@ -579,7 +610,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float hullRatioBefore, float hullRatioAfter, float lockProgress, - float aimDotDistance) + float aimDotDistance, + float playerViewportWidth, + float playerViewportHeight, + int texturedPlayerMaterialCount, + float directionalLightIntensity, + float pilotAverageLuminance, + int pilotBrightPixelCount, + RendererFact[] playerRendererFacts) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); if (string.IsNullOrWhiteSpace(path)) return; @@ -608,7 +646,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests movementDistance = movementDistance, aimDotDistance = aimDotDistance, playerRendererCount = playerRendererCount, + playerViewportWidth = playerViewportWidth, + playerViewportHeight = playerViewportHeight, + texturedPlayerMaterialCount = texturedPlayerMaterialCount, + directionalLightIntensity = directionalLightIntensity, pilotChangedPixels = pilotChangedPixels, + pilotAverageLuminance = pilotAverageLuminance, + pilotBrightPixelCount = pilotBrightPixelCount, + playerRenderers = playerRendererFacts, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, @@ -645,7 +690,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float movementDistance; public float aimDotDistance; public int playerRendererCount; + public float playerViewportWidth; + public float playerViewportHeight; + public int texturedPlayerMaterialCount; + public float directionalLightIntensity; public int pilotChangedPixels; + public float pilotAverageLuminance; + public int pilotBrightPixelCount; + public RendererFact[] playerRenderers; public int mapChannelRendererCount; public int mapChangedPixels; public bool pilotCameraExcludesMapChannel; @@ -685,6 +737,68 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } } + [Serializable] + private sealed class RendererFact + { + public string name; + public string rendererType; + public bool enabled; + public bool activeInHierarchy; + public int layer; + public string boundsSize; + public float viewportWidth; + public float viewportHeight; + public int renderedPixelCount; + public float renderedAverageLuminance; + public int renderedBrightPixelCount; + public MaterialFact[] materials; + + public static RendererFact From(Camera camera, Renderer renderer) + { + MeasureViewportCoverage(camera, new[] { renderer }, out var width, out var height); + return new RendererFact + { + name = renderer.name, + rendererType = renderer.GetType().Name, + enabled = renderer.enabled, + activeInHierarchy = renderer.gameObject.activeInHierarchy, + layer = renderer.gameObject.layer, + boundsSize = FormatVector(renderer.bounds.size), + viewportWidth = width, + viewportHeight = height, + materials = renderer.sharedMaterials + .Where(material => material != null) + .Select(MaterialFact.From) + .ToArray() + }; + } + } + + [Serializable] + private sealed class MaterialFact + { + public string name; + public string shader; + public bool shaderSupported; + public string baseColor; + public float surface; + public int textureCount; + public int renderQueue; + + public static MaterialFact From(Material material) => new MaterialFact + { + name = material.name, + shader = material.shader?.name ?? "", + shaderSupported = material.shader != null && material.shader.isSupported, + baseColor = material.HasProperty("_BaseColor") + ? FormatColor(material.GetColor("_BaseColor")) + : "", + surface = material.HasProperty("_Surface") ? material.GetFloat("_Surface") : -1f, + textureCount = material.GetTexturePropertyNames().Count(name => material.GetTexture(name) != null), + renderQueue = material.renderQueue + }; + } + private static void AssertReconciledProviderReceipt( EveUnitySceneCommandReceipt receipt, string commandId, @@ -714,6 +828,108 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests throw new InvalidOperationException($"Projected world has no entity marker '{entityId}'."); } + private static bool HasAnyTexture(Material material) + { + foreach (var propertyName in material.GetTexturePropertyNames()) + { + if (material.GetTexture(propertyName) != null) + return true; + } + return false; + } + + private static string FormatVector(Vector3 value) => + $"{value.x:R},{value.y:R},{value.z:R}"; + + private static string FormatColor(Color value) => + $"{value.r:R},{value.g:R},{value.b:R},{value.a:R}"; + + private static void MeasureViewportCoverage( + Camera camera, + IReadOnlyList renderers, + out float width, + out float height) + { + var min = new Vector2(float.PositiveInfinity, float.PositiveInfinity); + var max = new Vector2(float.NegativeInfinity, float.NegativeInfinity); + foreach (var renderer in renderers) + { + var bounds = renderer.bounds; + for (var x = -1; x <= 1; x += 2) + for (var y = -1; y <= 1; y += 2) + for (var z = -1; z <= 1; z += 2) + { + var point = bounds.center + Vector3.Scale(bounds.extents, new Vector3(x, y, z)); + var viewport = camera.WorldToViewportPoint(point); + if (viewport.z <= 0f) continue; + min = Vector2.Min(min, viewport); + max = Vector2.Max(max, viewport); + } + } + width = float.IsInfinity(min.x) ? 0f : Mathf.Max(0f, max.x - min.x); + height = float.IsInfinity(min.y) ? 0f : Mathf.Max(0f, max.y - min.y); + } + + private static void MeasureLuminance( + Texture2D texture, + out float average, + out int brightPixelCount) + { + double total = 0; + brightPixelCount = 0; + var pixels = texture.GetPixels32(); + foreach (var pixel in pixels) + { + var luminance = (0.2126 * pixel.r + 0.7152 * pixel.g + 0.0722 * pixel.b) / 255.0; + total += luminance; + if (luminance >= 0.2) + brightPixelCount++; + } + average = pixels.Length == 0 ? 0f : (float)(total / pixels.Length); + } + + private static void MeasureRendererOutputs( + Camera camera, + RenderTexture target, + Texture2D pixels, + IReadOnlyList allRenderers, + IReadOnlyList measuredRenderers, + IReadOnlyList facts) + { + var enabled = allRenderers.Select(renderer => renderer.enabled).ToArray(); + var clearFlags = camera.clearFlags; + var background = camera.backgroundColor; + try + { + foreach (var renderer in allRenderers) + renderer.enabled = false; + camera.clearFlags = CameraClearFlags.SolidColor; + camera.backgroundColor = Color.black; + for (var index = 0; index < measuredRenderers.Count; index++) + { + var fact = facts[index]; + measuredRenderers[index].enabled = true; + camera.Render(); + RenderTexture.active = target; + pixels.ReadPixels(new Rect(0, 0, target.width, target.height), 0, 0); + pixels.Apply(); + fact.renderedPixelCount = CountChangedPixels(pixels, Color.black); + MeasureLuminance( + pixels, + out fact.renderedAverageLuminance, + out fact.renderedBrightPixelCount); + measuredRenderers[index].enabled = false; + } + } + finally + { + for (var index = 0; index < allRenderers.Count; index++) + allRenderers[index].enabled = enabled[index]; + camera.clearFlags = clearFlags; + camera.backgroundColor = background; + } + } + private static int CountChangedPixels(Texture2D texture, Color background) { var changed = 0; diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 036f757..3fba6bd 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.26", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.27", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 984f6f4..65fec3a 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -142,7 +142,7 @@ "hash": "d0aa50d31f3b6da96bffc194fdc3aa358f37e332" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.26", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.27", "depth": 0, "source": "git", "dependencies": { @@ -151,7 +151,7 @@ "org.gamecult.eve.surface": "0.2.1", "com.unity.mathematics": "1.3.2" }, - "hash": "7e2c2fa720ca3437aa1b569ffba3a57e6b6f05c5" + "hash": "63a41712c5a68ce835bc1820980fc4786c34eb7b" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 38a93db..6e802c3 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -181,7 +181,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var root = new GameObject("Generic Eve World Projection"); var cameraObject = new GameObject("Generic Eve Capture Camera"); var mapCameraObject = new GameObject("Generic Eve Map Camera"); - var lightObject = new GameObject("Generic Eve World Light"); RenderTexture target = null; Texture2D pixels = null; EveUnityCultMeshPlayableWorldProvider provider = null; @@ -415,10 +414,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests if (observedShot.Hit && (observedShot.AppliedDamage > 0 || observedShot.ShieldAbsorbedDamage > 0)) Assert.That(combatRenderer.HitMarkerVisible, Is.True); - var light = lightObject.AddComponent(); - light.type = LightType.Directional; - light.intensity = 1.4f; - lightObject.transform.rotation = Quaternion.Euler(45f, -35f, 0f); var camera = cameraObject.AddComponent(); camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(0.035f, 0.055f, 0.08f, 1f); @@ -478,6 +473,21 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var playerViewport = camera.WorldToViewportPoint(playerMarker.transform.position); Assert.That(playerViewport.x, Is.EqualTo(0.9f).Within(0.01f)); Assert.That(playerViewport.y, Is.EqualTo(0.55f).Within(0.01f)); + MeasureViewportCoverage(camera, playerRenderers, out var playerViewportWidth, out var playerViewportHeight); + var playerRendererFacts = playerRenderers + .Select(renderer => RendererFact.From(camera, renderer)) + .ToArray(); + var texturedPlayerMaterialCount = playerRenderers + .SelectMany(renderer => renderer.sharedMaterials) + .Where(material => material != null) + .Distinct() + .Count(HasAnyTexture); + var directionalLightIntensity = UnityEngine.Object.FindObjectsOfType() + .Where(light => light != null && light.enabled && light.type == LightType.Directional) + .Sum(light => light.intensity); + Assert.That(directionalLightIntensity, + Is.EqualTo(runtime.ActiveWorld.KeyLightIntensity).Within(0.001f), + "The live witness contains directional lighting not owned by the advertised provider surface."); var mapCamera = mapCameraObject.AddComponent(); mapCamera.cullingMask = 1 << mapLayer; @@ -489,10 +499,25 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); pixels.Apply(); var pilotChangedPixels = CountChangedPixels(pixels, camera.backgroundColor); + MeasureLuminance(pixels, out var pilotAverageLuminance, out var pilotBrightPixelCount); Assert.That(pilotChangedPixels, Is.GreaterThan(25), "The pilot camera rendered no provider-authored world pixels."); Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(capturePath))); File.WriteAllBytes(capturePath, pixels.EncodeToPNG()); Assert.That(new FileInfo(capturePath).Length, Is.GreaterThan(1024)); + MeasureRendererOutputs( + camera, + target, + pixels, + root.GetComponentsInChildren(includeInactive: true), + playerRenderers, + playerRendererFacts); + var playerMapRendererFacts = playerRendererFacts + .Where(fact => fact.layer == mapLayer) + .ToArray(); + Assert.That(playerMapRendererFacts, Is.Not.Empty, + "The provider-authored player prefab has no renderer on its advertised map channel."); + Assert.That(playerMapRendererFacts.Sum(fact => fact.renderedPixelCount), Is.Zero, + "A provider-prefab map-channel renderer leaked into the pilot camera."); mapRenderers = mapRenderers.Where(renderer => renderer != null).ToList(); Assert.That(mapRenderers, Is.Not.Empty, @@ -539,7 +564,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests hullRatioBefore, observedCombat?.HullRatio ?? hullRatioBefore, (float)observedShot.LockQuality, - aimDotDistance); + aimDotDistance, + playerViewportWidth, + playerViewportHeight, + texturedPlayerMaterialCount, + directionalLightIntensity, + pilotAverageLuminance, + pilotBrightPixelCount, + playerRendererFacts); } finally { @@ -551,7 +583,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests if (target != null) UnityEngine.Object.DestroyImmediate(target); if (pixels != null) UnityEngine.Object.DestroyImmediate(pixels); host?.Disconnect(); - UnityEngine.Object.DestroyImmediate(lightObject); UnityEngine.Object.DestroyImmediate(mapCameraObject); UnityEngine.Object.DestroyImmediate(cameraObject); UnityEngine.Object.DestroyImmediate(root); @@ -579,7 +610,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float hullRatioBefore, float hullRatioAfter, float lockProgress, - float aimDotDistance) + float aimDotDistance, + float playerViewportWidth, + float playerViewportHeight, + int texturedPlayerMaterialCount, + float directionalLightIntensity, + float pilotAverageLuminance, + int pilotBrightPixelCount, + RendererFact[] playerRendererFacts) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); if (string.IsNullOrWhiteSpace(path)) return; @@ -608,7 +646,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests movementDistance = movementDistance, aimDotDistance = aimDotDistance, playerRendererCount = playerRendererCount, + playerViewportWidth = playerViewportWidth, + playerViewportHeight = playerViewportHeight, + texturedPlayerMaterialCount = texturedPlayerMaterialCount, + directionalLightIntensity = directionalLightIntensity, pilotChangedPixels = pilotChangedPixels, + pilotAverageLuminance = pilotAverageLuminance, + pilotBrightPixelCount = pilotBrightPixelCount, + playerRenderers = playerRendererFacts, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, @@ -645,7 +690,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float movementDistance; public float aimDotDistance; public int playerRendererCount; + public float playerViewportWidth; + public float playerViewportHeight; + public int texturedPlayerMaterialCount; + public float directionalLightIntensity; public int pilotChangedPixels; + public float pilotAverageLuminance; + public int pilotBrightPixelCount; + public RendererFact[] playerRenderers; public int mapChannelRendererCount; public int mapChangedPixels; public bool pilotCameraExcludesMapChannel; @@ -685,6 +737,68 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } } + [Serializable] + private sealed class RendererFact + { + public string name; + public string rendererType; + public bool enabled; + public bool activeInHierarchy; + public int layer; + public string boundsSize; + public float viewportWidth; + public float viewportHeight; + public int renderedPixelCount; + public float renderedAverageLuminance; + public int renderedBrightPixelCount; + public MaterialFact[] materials; + + public static RendererFact From(Camera camera, Renderer renderer) + { + MeasureViewportCoverage(camera, new[] { renderer }, out var width, out var height); + return new RendererFact + { + name = renderer.name, + rendererType = renderer.GetType().Name, + enabled = renderer.enabled, + activeInHierarchy = renderer.gameObject.activeInHierarchy, + layer = renderer.gameObject.layer, + boundsSize = FormatVector(renderer.bounds.size), + viewportWidth = width, + viewportHeight = height, + materials = renderer.sharedMaterials + .Where(material => material != null) + .Select(MaterialFact.From) + .ToArray() + }; + } + } + + [Serializable] + private sealed class MaterialFact + { + public string name; + public string shader; + public bool shaderSupported; + public string baseColor; + public float surface; + public int textureCount; + public int renderQueue; + + public static MaterialFact From(Material material) => new MaterialFact + { + name = material.name, + shader = material.shader?.name ?? "", + shaderSupported = material.shader != null && material.shader.isSupported, + baseColor = material.HasProperty("_BaseColor") + ? FormatColor(material.GetColor("_BaseColor")) + : "", + surface = material.HasProperty("_Surface") ? material.GetFloat("_Surface") : -1f, + textureCount = material.GetTexturePropertyNames().Count(name => material.GetTexture(name) != null), + renderQueue = material.renderQueue + }; + } + private static void AssertReconciledProviderReceipt( EveUnitySceneCommandReceipt receipt, string commandId, @@ -714,6 +828,108 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests throw new InvalidOperationException($"Projected world has no entity marker '{entityId}'."); } + private static bool HasAnyTexture(Material material) + { + foreach (var propertyName in material.GetTexturePropertyNames()) + { + if (material.GetTexture(propertyName) != null) + return true; + } + return false; + } + + private static string FormatVector(Vector3 value) => + $"{value.x:R},{value.y:R},{value.z:R}"; + + private static string FormatColor(Color value) => + $"{value.r:R},{value.g:R},{value.b:R},{value.a:R}"; + + private static void MeasureViewportCoverage( + Camera camera, + IReadOnlyList renderers, + out float width, + out float height) + { + var min = new Vector2(float.PositiveInfinity, float.PositiveInfinity); + var max = new Vector2(float.NegativeInfinity, float.NegativeInfinity); + foreach (var renderer in renderers) + { + var bounds = renderer.bounds; + for (var x = -1; x <= 1; x += 2) + for (var y = -1; y <= 1; y += 2) + for (var z = -1; z <= 1; z += 2) + { + var point = bounds.center + Vector3.Scale(bounds.extents, new Vector3(x, y, z)); + var viewport = camera.WorldToViewportPoint(point); + if (viewport.z <= 0f) continue; + min = Vector2.Min(min, viewport); + max = Vector2.Max(max, viewport); + } + } + width = float.IsInfinity(min.x) ? 0f : Mathf.Max(0f, max.x - min.x); + height = float.IsInfinity(min.y) ? 0f : Mathf.Max(0f, max.y - min.y); + } + + private static void MeasureLuminance( + Texture2D texture, + out float average, + out int brightPixelCount) + { + double total = 0; + brightPixelCount = 0; + var pixels = texture.GetPixels32(); + foreach (var pixel in pixels) + { + var luminance = (0.2126 * pixel.r + 0.7152 * pixel.g + 0.0722 * pixel.b) / 255.0; + total += luminance; + if (luminance >= 0.2) + brightPixelCount++; + } + average = pixels.Length == 0 ? 0f : (float)(total / pixels.Length); + } + + private static void MeasureRendererOutputs( + Camera camera, + RenderTexture target, + Texture2D pixels, + IReadOnlyList allRenderers, + IReadOnlyList measuredRenderers, + IReadOnlyList facts) + { + var enabled = allRenderers.Select(renderer => renderer.enabled).ToArray(); + var clearFlags = camera.clearFlags; + var background = camera.backgroundColor; + try + { + foreach (var renderer in allRenderers) + renderer.enabled = false; + camera.clearFlags = CameraClearFlags.SolidColor; + camera.backgroundColor = Color.black; + for (var index = 0; index < measuredRenderers.Count; index++) + { + var fact = facts[index]; + measuredRenderers[index].enabled = true; + camera.Render(); + RenderTexture.active = target; + pixels.ReadPixels(new Rect(0, 0, target.width, target.height), 0, 0); + pixels.Apply(); + fact.renderedPixelCount = CountChangedPixels(pixels, Color.black); + MeasureLuminance( + pixels, + out fact.renderedAverageLuminance, + out fact.renderedBrightPixelCount); + measuredRenderers[index].enabled = false; + } + } + finally + { + for (var index = 0; index < allRenderers.Count; index++) + allRenderers[index].enabled = enabled[index]; + camera.clearFlags = clearFlags; + camera.backgroundColor = background; + } + } + private static int CountChangedPixels(Texture2D texture, Color background) { var changed = 0; diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 99ad2a7..79fdb49 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,8 +4,8 @@ The warm-cache released-package witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.26`, commit - `7e2c2fa720ca3437aa1b569ffba3a57e6b6f05c5`; +- `org.gamecult.eve.unity-scene` `0.3.27`, commit + `63a41712c5a68ce835bc1820980fc4786c34eb7b`; - `org.gamecult.cultlib` `1.0.13`, commit `feb5c71513e71d681699f462fe3682b3168c6f73`; - the generic `ReleaseConsumerProject` client connected directly to the @@ -18,19 +18,27 @@ and the generic aim presentation rendered at the advertised 50-unit minimum convergence distance. The advertised skybox material and reflection cubemap resolved from the provider bundle as their required native types and became the active camera rig's leased Unity environment. +The witness supplies no client-authored light. Aetheria advertises the key-light +direction, color, and intensity; the generic camera rig lowers that contract to +the only live directional light (`0.75` intensity). Camera-channel facts: - provider-authored player renderers: `12`; -- pilot changed pixels: `230,332`; +- pilot changed pixels: `230,359`; - map-channel renderers: `11`; -- map changed pixels: `5,496`; +- map changed pixels: `5,153`; - the pilot camera excludes the advertised map layer; +- the player prefab's embedded layer-14 map icon contributes exactly `0` pilot + pixels; - the map camera renders exactly the advertised map layer. Visual inspection confirms that map glyphs are absent from the pilot frame and -present in the map-only frame. The pilot presentation remains visually dark; -the native art and lighting pass is not complete. +present in the map-only frame. The pilot presentation remains visually sparse +and dark. Renderer-isolation facts prove that the hull draws under the +provider-owned light, so the next native-art work is world composition and +available pre-generated provider textures, not a client-side lighting fallback. +Substance is not part of this path; later texture baking belongs in Blender. Primary artifacts: diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 2b8b81f..fac33e1 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "7e2c2fa720ca3437aa1b569ffba3a57e6b6f05c5" +$expectedEveUnityCommit = "63a41712c5a68ce835bc1820980fc4786c34eb7b" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot $projectRoot = $ClientProject @@ -66,7 +66,10 @@ foreach ($ephemeralPath in @( "$replicaPath.cultmesh", $statePath, "$statePath.records", - "$statePath.cultmesh" + "$statePath.cultmesh", + "$statePath.ymir.cc", + "$statePath.ymir.cc.records", + "$statePath.ymir.cc.cultmesh" )) { $resolved = [IO.Path]::GetFullPath($ephemeralPath) if (-not $resolved.StartsWith([IO.Path]::GetFullPath($outputRoot), [StringComparison]::OrdinalIgnoreCase)) { From 36b9642aa87fc85fec62ea7c35e9f29ce04f05d8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 09:20:04 +0100 Subject: [PATCH 060/292] Prove authored Aetheria camera composition --- .../PlayMode/GenericWorldCaptureTests.cs | 4 +++- .../PlayMode/GenericWorldCaptureTests.cs | 4 +++- docs/aetheria-released-package-witness.md | 20 +++++++++++++------ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 6e802c3..cef9d89 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -469,9 +469,11 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests aimDotDistance = Vector3.Distance(playerMarker.transform.position, aimRenderer.ViewDotPosition); Assert.That(aimDotDistance, Is.GreaterThanOrEqualTo(49.9f)); Assert.That(camera.cullingMask & (1 << mapLayer), Is.Zero); + Assert.That(runtime.ActiveWorld.CameraDistance, Is.EqualTo(70f).Within(0.001f)); Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); + Assert.That(runtime.ActiveWorld.CameraPositionDamping, Is.EqualTo(2f).Within(0.001f)); var playerViewport = camera.WorldToViewportPoint(playerMarker.transform.position); - Assert.That(playerViewport.x, Is.EqualTo(0.9f).Within(0.01f)); + Assert.That(playerViewport.x, Is.EqualTo(0.66f).Within(0.01f)); Assert.That(playerViewport.y, Is.EqualTo(0.55f).Within(0.01f)); MeasureViewportCoverage(camera, playerRenderers, out var playerViewportWidth, out var playerViewportHeight); var playerRendererFacts = playerRenderers diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 6e802c3..cef9d89 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -469,9 +469,11 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests aimDotDistance = Vector3.Distance(playerMarker.transform.position, aimRenderer.ViewDotPosition); Assert.That(aimDotDistance, Is.GreaterThanOrEqualTo(49.9f)); Assert.That(camera.cullingMask & (1 << mapLayer), Is.Zero); + Assert.That(runtime.ActiveWorld.CameraDistance, Is.EqualTo(70f).Within(0.001f)); Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); + Assert.That(runtime.ActiveWorld.CameraPositionDamping, Is.EqualTo(2f).Within(0.001f)); var playerViewport = camera.WorldToViewportPoint(playerMarker.transform.position); - Assert.That(playerViewport.x, Is.EqualTo(0.9f).Within(0.01f)); + Assert.That(playerViewport.x, Is.EqualTo(0.66f).Within(0.01f)); Assert.That(playerViewport.y, Is.EqualTo(0.55f).Within(0.01f)); MeasureViewportCoverage(camera, playerRenderers, out var playerViewportWidth, out var playerViewportHeight); var playerRendererFacts = playerRenderers diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 79fdb49..24714fa 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -21,23 +21,31 @@ active camera rig's leased Unity environment. The witness supplies no client-authored light. Aetheria advertises the key-light direction, color, and intensity; the generic camera rig lowers that contract to the only live directional light (`0.75` intensity). +Aetheria also advertises the original `ARPG.unity` follow-camera composition: +`70` unit distance, `60` degree vertical field of view, target screen position +`0.66,0.55`, and position damping `2`. The generic camera rig lowers those +values without Aetheria-specific camera code. Camera-channel facts: - provider-authored player renderers: `12`; -- pilot changed pixels: `230,359`; +- pilot changed pixels: `230,248`; +- pilot average luminance: `0.003696`; +- pilot bright pixels: `1,319`; - map-channel renderers: `11`; -- map changed pixels: `5,153`; +- map changed pixels: `4,975`; - the pilot camera excludes the advertised map layer; - the player prefab's embedded layer-14 map icon contributes exactly `0` pilot pixels; - the map camera renders exactly the advertised map layer. Visual inspection confirms that map glyphs are absent from the pilot frame and -present in the map-only frame. The pilot presentation remains visually sparse -and dark. Renderer-isolation facts prove that the hull draws under the -provider-owned light, so the next native-art work is world composition and -available pre-generated provider textures, not a client-side lighting fallback. +present in the map-only frame. Restoring the authored ARPG camera in place of +the copied TestScene tuning increased hull-renderer pixel coverage by roughly +four times and bright pilot pixels from `502` to `1,319`. The pilot presentation +remains dark. Renderer-isolation facts prove that the hull draws under the +provider-owned light, so the next native-art work is available pre-generated +provider textures, not a client-side lighting fallback. Substance is not part of this path; later texture baking belongs in Blender. Primary artifacts: From 84d781c4b1f7a1ec0cdde5f97f41494ab76ffa00 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 09:27:48 +0100 Subject: [PATCH 061/292] Prove provider material textures survive lowering --- .../Tests/PlayMode/GenericWorldCaptureTests.cs | 2 ++ .../Tests/PlayMode/GenericWorldCaptureTests.cs | 2 ++ docs/aetheria-released-package-witness.md | 15 ++++++++++----- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index cef9d89..5639750 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -484,6 +484,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests .Where(material => material != null) .Distinct() .Count(HasAnyTexture); + Assert.That(texturedPlayerMaterialCount, Is.GreaterThanOrEqualTo(3), + "The provider bundle must retain its advertised native material textures."); var directionalLightIntensity = UnityEngine.Object.FindObjectsOfType() .Where(light => light != null && light.enabled && light.type == LightType.Directional) .Sum(light => light.intensity); diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index cef9d89..5639750 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -484,6 +484,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests .Where(material => material != null) .Distinct() .Count(HasAnyTexture); + Assert.That(texturedPlayerMaterialCount, Is.GreaterThanOrEqualTo(3), + "The provider bundle must retain its advertised native material textures."); var directionalLightIntensity = UnityEngine.Object.FindObjectsOfType() .Where(light => light != null && light.enabled && light.type == LightType.Directional) .Sum(light => light.intensity); diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 24714fa..0c7e1a1 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -29,11 +29,11 @@ values without Aetheria-specific camera code. Camera-channel facts: - provider-authored player renderers: `12`; -- pilot changed pixels: `230,248`; -- pilot average luminance: `0.003696`; -- pilot bright pixels: `1,319`; +- pilot changed pixels: `230,169`; +- pilot average luminance: `0.003425`; +- pilot bright pixels: `1,242`; - map-channel renderers: `11`; -- map changed pixels: `4,975`; +- map changed pixels: `5,153`; - the pilot camera excludes the advertised map layer; - the player prefab's embedded layer-14 map icon contributes exactly `0` pilot pixels; @@ -47,6 +47,11 @@ remains dark. Renderer-isolation facts prove that the hull draws under the provider-owned light, so the next native-art work is available pre-generated provider textures, not a client-side lighting fallback. Substance is not part of this path; later texture baking belongs in Blender. +Aetheria currently bundles ambientCG's 1K `Metal012` color, normal, and +metalness maps for its brushed-aluminium material under CC0. The released +witness resolves four native textures on that generated URP material. This is +temporary provider art with recorded provenance, not a replacement texture +pipeline. Primary artifacts: @@ -58,7 +63,7 @@ Primary artifacts: ## Cold delivery is not currently proven -The current 45.2 MB Unity bundle times out from an empty cache. Its bytes are +The current 46.1 MB Unity bundle times out from an empty cache. Its bytes are still transported through batched snapshot records rather than the intended mapped/network body transport. Increasing the Unity timeout would hide the transport fault and is not an accepted fix. From 4d0cbe0185bdc4fc65eb63503a7c5cb578539669 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 09:39:11 +0100 Subject: [PATCH 062/292] Lower provider-authored cockpit overlays --- .../README.md | 7 ++++ .../Runtime/EveUiToolkitSurfaceLowerer.cs | 38 +++++++++++++++++-- .../Editor/EveUiToolkitSurfaceLowererTests.cs | 30 +++++++++++++++ .../package.json | 2 +- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/packages/org.gamecult.eve.unity-uitoolkit/README.md b/packages/org.gamecult.eve.unity-uitoolkit/README.md index a0b325c..cf788bd 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/README.md +++ b/packages/org.gamecult.eve.unity-uitoolkit/README.md @@ -27,6 +27,13 @@ Nested CultUI regions use component `EmbeddedDocuments`. Pass an mounts the resolved child surface under the slot while preserving the child document's command surface id. +`progress` components lower to native UI Toolkit `ProgressBar` elements. The +provider supplies a normalized `ratio` (falling back to `value`), label, and +layout; Unity clamps the native visual to `[0,1]` without becoming the value +authority. Generated layout supports relative or absolute positioning plus +`inset`, `top`, `right`, `bottom`, and `left`, so provider-authored overlays +can compose over scene lowerers without consumer-specific HUD code. + Discovery and test coverage for this feature live in the shared Eve parity matrix: `../../tools/parity/parity-manifest.json` names `embeddedDocuments` and `../../web/fixtures/cultui-embedded-surface.json` is the canonical fixture. diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs index f989ccf..6586d67 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs @@ -129,11 +129,14 @@ namespace GameCult.Eve.UnityUIToolkit } case "progress": { - var progress = new VisualElement(); + var progress = new ProgressBar + { + title = component.GetProp("label"), + lowValue = 0f, + highValue = 1f, + value = ParseRatio(component.GetProp("ratio", component.GetProp("value"))) + }; progress.AddToClassList("eve-progress"); - progress.style.flexDirection = FlexDirection.Column; - progress.Add(MutedLabel(component.GetProp("label"))); - progress.Add(ValueLabel(component.GetProp("value"))); return progress; } case "options": @@ -313,6 +316,26 @@ namespace GameCult.Eve.UnityUIToolkit element.style.display = string.Equals(display, "none", StringComparison.OrdinalIgnoreCase) ? DisplayStyle.None : DisplayStyle.Flex; + if (TryGet(layout, "position", out var position)) + element.style.position = string.Equals(position, "absolute", StringComparison.OrdinalIgnoreCase) + ? Position.Absolute + : Position.Relative; + if (TryGet(layout, "inset", out var inset)) + { + var value = ParseLength(inset); + element.style.top = value; + element.style.right = value; + element.style.bottom = value; + element.style.left = value; + } + if (TryGet(layout, "top", out var top)) + element.style.top = ParseLength(top); + if (TryGet(layout, "right", out var right)) + element.style.right = ParseLength(right); + if (TryGet(layout, "bottom", out var bottom)) + element.style.bottom = ParseLength(bottom); + if (TryGet(layout, "left", out var left)) + element.style.left = ParseLength(left); if (TryGet(layout, "direction", out var direction)) { if (string.Equals(direction, "horizontal", StringComparison.OrdinalIgnoreCase) || @@ -430,6 +453,13 @@ namespace GameCult.Eve.UnityUIToolkit : StyleKeyword.Null; } + private static float ParseRatio(string value) + { + return float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var ratio) + ? Mathf.Clamp01(ratio) + : 0f; + } + private static void ApplyBoxLength(string value, Action apply) { apply(ParseLength(value)); diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Tests/Editor/EveUiToolkitSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-uitoolkit/Tests/Editor/EveUiToolkitSurfaceLowererTests.cs index a9d88fb..0b9dc54 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/Tests/Editor/EveUiToolkitSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-uitoolkit/Tests/Editor/EveUiToolkitSurfaceLowererTests.cs @@ -112,6 +112,36 @@ namespace GameCult.Eve.UnityUIToolkit.Tests Assert.That(embedded.ClassListContains("eve-embedded-kind-inline"), Is.True); } + [Test] + public void ProgressAndAbsoluteLayoutLowerToNativeUiToolkitPrimitives() + { + var progress = new EveSurfaceComponent( + "hull", + "progress", + new Dictionary + { + ["label"] = "Hull", + ["ratio"] = "0.625" + }, + Array.Empty(), + Array.Empty(), + Array.Empty(), + new Dictionary + { + ["position"] = "absolute", + ["right"] = "24", + ["bottom"] = "32" + }); + + var root = new EveUiToolkitSurfaceLowerer().Lower(Document(progress)); + + Assert.That(root, Is.TypeOf()); + Assert.That(((ProgressBar)root).value, Is.EqualTo(0.625f)); + Assert.That(root.style.position.value, Is.EqualTo(Position.Absolute)); + Assert.That(root.style.right.value.value, Is.EqualTo(24f)); + Assert.That(root.style.bottom.value.value, Is.EqualTo(32f)); + } + private static EveSurfaceDocument Document(EveSurfaceComponent root, string surfaceId = "test-surface") { return new EveSurfaceDocument( diff --git a/packages/org.gamecult.eve.unity-uitoolkit/package.json b/packages/org.gamecult.eve.unity-uitoolkit/package.json index cf91087..ee02cd0 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/package.json +++ b/packages/org.gamecult.eve.unity-uitoolkit/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-uitoolkit", "displayName": "Eve UI Toolkit Lowering", - "version": "0.1.0", + "version": "0.1.1", "unity": "2021.3", "description": "Unity UI Toolkit lowering for gamecult.eve.surface.v1 documents.", "dependencies": { From 3b9e2a922f94b6fa932445c531a1aa4a0dc62466 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 09:49:31 +0100 Subject: [PATCH 063/292] Prove the released generic Aetheria cockpit --- .../Assets/Editor/GenericEveUnityBuild.cs | 20 ++++ .../Editor/GenericEveUnityBuild.cs.meta | 2 + .../Assets/GenericEveUnityLauncher.cs | 96 +++++++++++++++++++ .../Assets/GenericEveUnityLauncher.cs.meta | 2 + ...veUnity.GenericClient.PlayModeTests.asmdef | 1 + .../PlayMode/GenericWorldCaptureTests.cs | 16 ++++ ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 +- ...veUnity.GenericClient.PlayModeTests.asmdef | 1 + .../PlayMode/GenericWorldCaptureTests.cs | 16 ++++ docs/aetheria-released-package-witness.md | 23 ++++- scripts/run-aetheria-daemon-world-witness.ps1 | 15 ++- 12 files changed, 186 insertions(+), 12 deletions(-) create mode 100644 ReleaseConsumerProject/Assets/Editor/GenericEveUnityBuild.cs create mode 100644 ReleaseConsumerProject/Assets/Editor/GenericEveUnityBuild.cs.meta create mode 100644 ReleaseConsumerProject/Assets/GenericEveUnityLauncher.cs create mode 100644 ReleaseConsumerProject/Assets/GenericEveUnityLauncher.cs.meta diff --git a/ReleaseConsumerProject/Assets/Editor/GenericEveUnityBuild.cs b/ReleaseConsumerProject/Assets/Editor/GenericEveUnityBuild.cs new file mode 100644 index 0000000..3c6f3db --- /dev/null +++ b/ReleaseConsumerProject/Assets/Editor/GenericEveUnityBuild.cs @@ -0,0 +1,20 @@ +using System.IO; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; + +public static class GenericEveUnityBuild +{ + public static void BuildWindows() + { + const string scenePath = "Assets/GenericEveUnity.unity"; + var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); + new GameObject("Generic EveUnity Client").AddComponent(); + EditorSceneManager.SaveScene(scene, scenePath); + Directory.CreateDirectory("Build/Windows"); + var report = BuildPipeline.BuildPlayer(new[] { scenePath }, "Build/Windows/EveUnity.exe", BuildTarget.StandaloneWindows64, BuildOptions.Development); + if (report.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded) + throw new System.InvalidOperationException("Generic EveUnity Windows build failed: " + report.summary.result); + } +} diff --git a/ReleaseConsumerProject/Assets/Editor/GenericEveUnityBuild.cs.meta b/ReleaseConsumerProject/Assets/Editor/GenericEveUnityBuild.cs.meta new file mode 100644 index 0000000..9683f45 --- /dev/null +++ b/ReleaseConsumerProject/Assets/Editor/GenericEveUnityBuild.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8fcd5fd63f21489eb5a70306d22ed4a4 diff --git a/ReleaseConsumerProject/Assets/GenericEveUnityLauncher.cs b/ReleaseConsumerProject/Assets/GenericEveUnityLauncher.cs new file mode 100644 index 0000000..79978e2 --- /dev/null +++ b/ReleaseConsumerProject/Assets/GenericEveUnityLauncher.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections; +using System.IO; +using GameCult.Eve.UnityUIToolkit; +using GameCult.Eve.UnityScene; +using UnityEngine; +using UnityEngine.UIElements; + +public sealed class GenericEveUnityLauncher : MonoBehaviour +{ + private EveUnityCultMeshPlayableWorldProvider _provider; + private EveUnityPlayableWorldClientBootstrap _bootstrap; + private EveUiToolkitSurfaceLowerer _surfaceLowerer; + private UIDocument _surfaceDocument; + private PanelSettings _panelSettings; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] + private static void StartClient() + { + if (string.Equals(Environment.GetEnvironmentVariable("EVEUNITY_DISABLE_AUTO_LAUNCHER"), "1", StringComparison.Ordinal)) + return; + if (FindObjectOfType() != null) return; + new GameObject("Generic EveUnity Client").AddComponent(); + } + + private void Awake() + { + Application.runInBackground = true; + var endpoint = Environment.GetEnvironmentVariable("EVEUNITY_RENDEZVOUS_ENDPOINT") ?? "rudp://127.0.0.1:3076"; + var surface = Environment.GetEnvironmentVariable("EVEUNITY_SURFACE_ID") ?? "aetheria.pilot"; + var replica = Path.Combine(Application.persistentDataPath, "eve-unity-client.cc"); + + var cameraObject = new GameObject("Main Camera") { tag = "MainCamera" }; + var camera = cameraObject.AddComponent(); + camera.clearFlags = CameraClearFlags.SolidColor; + camera.backgroundColor = new Color(0.025f, 0.035f, 0.055f); + camera.nearClipPlane = 0.05f; + camera.farClipPlane = 10000f; + + _provider = gameObject.AddComponent(); + _provider.Configure(endpoint, replica, surfaceId: surface, requiredSurfaceKind: "interactive-world", clientRuntimeId: "eve-unity-interactive"); + _provider.DocumentAvailable += OnSurfaceAvailable; + _surfaceLowerer = new EveUiToolkitSurfaceLowerer(); + _panelSettings = ScriptableObject.CreateInstance(); + _panelSettings.scaleMode = PanelScaleMode.ScaleWithScreenSize; + _panelSettings.referenceResolution = new Vector2Int(1920, 1080); + _panelSettings.sortingOrder = 20; + _surfaceDocument = gameObject.AddComponent(); + _surfaceDocument.panelSettings = _panelSettings; + _bootstrap = gameObject.AddComponent(); + _bootstrap.ConfigureProvider(_provider); + StartCoroutine(Connect()); + } + + private IEnumerator Connect() + { + while (true) + { + var connected = false; + try + { + var presentation = _bootstrap.Mount(); + LowerSurface(_provider.CurrentDocument); + Debug.Log($"Connected generic Eve client: {_provider.Selection?.ProviderId} / {_provider.Selection?.SurfaceId} / {presentation.ActiveEntities} entities"); + connected = true; + } + catch (Exception ex) + { + Debug.Log("Waiting for Eve provider: " + ex.Message); + } + if (connected) yield break; + yield return new WaitForSecondsRealtime(0.5f); + } + } + + private void OnSurfaceAvailable(EveUnitySceneProviderSurfaceDocument document) + { + LowerSurface(document); + } + + private void LowerSurface(EveUnitySceneProviderSurfaceDocument document) + { + if (_surfaceDocument == null || _surfaceLowerer == null || document == null) return; + var root = _surfaceDocument.rootVisualElement; + root.Clear(); + var lowered = _surfaceLowerer.Lower(document.SurfaceDocument, _provider.Submit); + lowered.style.flexGrow = 1; + root.Add(lowered); + } + + private void OnDestroy() + { + if (_provider != null) _provider.DocumentAvailable -= OnSurfaceAvailable; + if (_panelSettings != null) Destroy(_panelSettings); + } +} diff --git a/ReleaseConsumerProject/Assets/GenericEveUnityLauncher.cs.meta b/ReleaseConsumerProject/Assets/GenericEveUnityLauncher.cs.meta new file mode 100644 index 0000000..dde265b --- /dev/null +++ b/ReleaseConsumerProject/Assets/GenericEveUnityLauncher.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5c2c8060cff846cda741810156b0558a diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef b/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef index 4d21cb3..4eadab4 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef @@ -4,6 +4,7 @@ "references": [ "GameCult.CultLib", "GameCult.Eve.Surface", + "GameCult.Eve.UnityUIToolkit", "GameCult.Eve.UnityScene", "GameCult.Eve.UnityScene.CultMesh" ], diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 5639750..57aadb6 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -6,10 +6,12 @@ using System.Linq; using GameCult.Caching; using GameCult.Caching.MessagePack; using GameCult.Eve.Surface; +using GameCult.Eve.UnityUIToolkit; using GameCult.Eve.UnityScene; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; +using UnityEngine.UIElements; namespace GameCult.EveUnity.GenericClient.PlayModeTests { @@ -197,6 +199,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests long initialVersion = 0; float movementDistance = 0f; float aimDotDistance = 0f; + int cockpitProgressCount = 0; try { @@ -236,6 +239,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests provider, provider); host.Connect(); + var loweredSurface = new EveUiToolkitSurfaceLowerer().Lower( + provider.CurrentDocument.SurfaceDocument, + provider.Submit); + var cockpit = loweredSurface.Q("aetheria.daemon.game.cockpit"); + Assert.That(cockpit, Is.Not.Null, "The provider did not publish a generic cockpit overlay."); + cockpitProgressCount = cockpit.Query().ToList().Count; + Assert.That(cockpitProgressCount, Is.GreaterThanOrEqualTo(6)); + Assert.That(loweredSurface.Q("aetheria.daemon.game.frame").style.display.value, + Is.EqualTo(DisplayStyle.None), "Daemon diagnostics leaked into the pilot UI."); host.ShotAvailable += shot => observedShot = shot; runtime = host.Runtime; Assert.That(runtime, Is.Not.Null); @@ -575,6 +587,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests directionalLightIntensity, pilotAverageLuminance, pilotBrightPixelCount, + cockpitProgressCount, playerRendererFacts); } finally @@ -621,6 +634,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float directionalLightIntensity, float pilotAverageLuminance, int pilotBrightPixelCount, + int cockpitProgressCount, RendererFact[] playerRendererFacts) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); @@ -657,6 +671,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pilotChangedPixels = pilotChangedPixels, pilotAverageLuminance = pilotAverageLuminance, pilotBrightPixelCount = pilotBrightPixelCount, + cockpitProgressCount = cockpitProgressCount, playerRenderers = playerRendererFacts, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, @@ -701,6 +716,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public int pilotChangedPixels; public float pilotAverageLuminance; public int pilotBrightPixelCount; + public int cockpitProgressCount; public RendererFact[] playerRenderers; public int mapChannelRendererCount; public int mapChangedPixels; diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 3fba6bd..cdbd57c 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -10,7 +10,7 @@ "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.27", - "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0" + "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ "org.gamecult.eve.unity-scene", diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 65fec3a..5979111 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -154,13 +154,13 @@ "hash": "63a41712c5a68ce835bc1820980fc4786c34eb7b" }, "org.gamecult.eve.unity-uitoolkit": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", "depth": 0, "source": "git", "dependencies": { "org.gamecult.eve.surface": "0.1.0" }, - "hash": "e10821f1cc81f619479c6507438f6a097d1f0ce2" + "hash": "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" }, "com.unity.modules.assetbundle": { "version": "1.0.0", diff --git a/TestProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef b/TestProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef index 4d21cb3..4eadab4 100644 --- a/TestProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef +++ b/TestProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef @@ -4,6 +4,7 @@ "references": [ "GameCult.CultLib", "GameCult.Eve.Surface", + "GameCult.Eve.UnityUIToolkit", "GameCult.Eve.UnityScene", "GameCult.Eve.UnityScene.CultMesh" ], diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 5639750..57aadb6 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -6,10 +6,12 @@ using System.Linq; using GameCult.Caching; using GameCult.Caching.MessagePack; using GameCult.Eve.Surface; +using GameCult.Eve.UnityUIToolkit; using GameCult.Eve.UnityScene; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; +using UnityEngine.UIElements; namespace GameCult.EveUnity.GenericClient.PlayModeTests { @@ -197,6 +199,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests long initialVersion = 0; float movementDistance = 0f; float aimDotDistance = 0f; + int cockpitProgressCount = 0; try { @@ -236,6 +239,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests provider, provider); host.Connect(); + var loweredSurface = new EveUiToolkitSurfaceLowerer().Lower( + provider.CurrentDocument.SurfaceDocument, + provider.Submit); + var cockpit = loweredSurface.Q("aetheria.daemon.game.cockpit"); + Assert.That(cockpit, Is.Not.Null, "The provider did not publish a generic cockpit overlay."); + cockpitProgressCount = cockpit.Query().ToList().Count; + Assert.That(cockpitProgressCount, Is.GreaterThanOrEqualTo(6)); + Assert.That(loweredSurface.Q("aetheria.daemon.game.frame").style.display.value, + Is.EqualTo(DisplayStyle.None), "Daemon diagnostics leaked into the pilot UI."); host.ShotAvailable += shot => observedShot = shot; runtime = host.Runtime; Assert.That(runtime, Is.Not.Null); @@ -575,6 +587,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests directionalLightIntensity, pilotAverageLuminance, pilotBrightPixelCount, + cockpitProgressCount, playerRendererFacts); } finally @@ -621,6 +634,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float directionalLightIntensity, float pilotAverageLuminance, int pilotBrightPixelCount, + int cockpitProgressCount, RendererFact[] playerRendererFacts) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); @@ -657,6 +671,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pilotChangedPixels = pilotChangedPixels, pilotAverageLuminance = pilotAverageLuminance, pilotBrightPixelCount = pilotBrightPixelCount, + cockpitProgressCount = cockpitProgressCount, playerRenderers = playerRendererFacts, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, @@ -701,6 +716,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public int pilotChangedPixels; public float pilotAverageLuminance; public int pilotBrightPixelCount; + public int cockpitProgressCount; public RendererFact[] playerRenderers; public int mapChannelRendererCount; public int mapChangedPixels; diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 0c7e1a1..9cb8016 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -6,6 +6,8 @@ The warm-cache released-package witness passes with: - `org.gamecult.eve.unity-scene` `0.3.27`, commit `63a41712c5a68ce835bc1820980fc4786c34eb7b`; +- `org.gamecult.eve.unity-uitoolkit` `0.1.1`, commit + `4d0cbe0185bdc4fc65eb63503a7c5cb578539669`; - `org.gamecult.cultlib` `1.0.13`, commit `feb5c71513e71d681699f462fe3682b3168c6f73`; - the generic `ReleaseConsumerProject` client connected directly to the @@ -25,15 +27,28 @@ Aetheria also advertises the original `ARPG.unity` follow-camera composition: `70` unit distance, `60` degree vertical field of view, target screen position `0.66,0.55`, and position damping `2`. The generic camera rig lowers those values without Aetheria-specific camera code. +Aetheria's pilot surface also publishes a transparent cockpit overlay. The +released generic UI Toolkit lowerer produced six native `ProgressBar` elements +for hull, shield, capacitor, weapon cooldown, and target state. Daemon frame, +player, and command diagnostics remain available in the document but lower +with `display: none` in the pilot presentation. The runnable generic-client +launcher mounts the same advertised document and forwards its typed commands; +it contains no Aetheria gameplay or client-authored world light. + +The two PNGs below are deliberate camera-channel captures, not a composited +screen capture, so they prove world/map isolation rather than HUD pixels. The +native cockpit proof is the asserted UI Toolkit visual tree recorded as +`cockpitProgressCount: 6` in the witness facts. Camera-channel facts: - provider-authored player renderers: `12`; -- pilot changed pixels: `230,169`; -- pilot average luminance: `0.003425`; -- pilot bright pixels: `1,242`; +- pilot changed pixels: `230,229`; +- pilot average luminance: `0.003307`; +- pilot bright pixels: `1,289`; - map-channel renderers: `11`; -- map changed pixels: `5,153`; +- map changed pixels: `5,396`; +- native cockpit progress bars: `6`; - the pilot camera excludes the advertised map layer; - the player prefab's embedded layer-14 map icon contributes exactly `0` pilot pixels; diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index fac33e1..4512bbe 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -14,6 +14,7 @@ param( $ErrorActionPreference = "Stop" $expectedEveUnityCommit = "63a41712c5a68ce835bc1820980fc4786c34eb7b" +$expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot $projectRoot = $ClientProject @@ -188,14 +189,17 @@ try { $releaseLock = Get-Content -LiteralPath (Join-Path $repoRoot "ReleaseConsumerProject\Packages\packages-lock.json") -Raw | ConvertFrom-Json $releasedPackageClient = $projectRoot -eq "ReleaseConsumerProject" $sceneLock = $releaseLock.dependencies.'org.gamecult.eve.unity-scene' + $uiToolkitLock = $releaseLock.dependencies.'org.gamecult.eve.unity-uitoolkit' $cultLibLock = $releaseLock.dependencies.'org.gamecult.cultlib' if (-not $releasedPackageClient) { throw "Released witness must run ReleaseConsumerProject, got '$projectRoot'." } - if ($sceneLock.source -ne "git" -or $cultLibLock.source -ne "git" -or - $sceneLock.version -like "file:*" -or $cultLibLock.version -like "file:*") { + if ($sceneLock.source -ne "git" -or $uiToolkitLock.source -ne "git" -or $cultLibLock.source -ne "git" -or + $sceneLock.version -like "file:*" -or $uiToolkitLock.version -like "file:*" -or $cultLibLock.version -like "file:*") { throw "Released witness resolved a non-git/local package dependency." } - if ($sceneLock.hash -ne $expectedEveUnityCommit -or $cultLibLock.hash -ne $expectedCultLibCommit) { - throw "Released package commits do not match the witnessed releases. scene=$($sceneLock.hash) cultlib=$($cultLibLock.hash)" + if ($sceneLock.hash -ne $expectedEveUnityCommit -or + $uiToolkitLock.hash -ne $expectedEveUnityUiToolkitCommit -or + $cultLibLock.hash -ne $expectedCultLibCommit) { + throw "Released package commits do not match the witnessed releases. scene=$($sceneLock.hash) uitoolkit=$($uiToolkitLock.hash) cultlib=$($cultLibLock.hash)" } $finalBodies = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.body -File -Recurse -ErrorAction SilentlyContinue) $finalPartials = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.partial -File -Recurse -ErrorAction SilentlyContinue) @@ -215,6 +219,7 @@ try { $facts | Add-Member -NotePropertyName releasedPackageClient -NotePropertyValue $releasedPackageClient $facts | Add-Member -NotePropertyName clientProject -NotePropertyValue $projectRoot $facts | Add-Member -NotePropertyName eveUnityPackageCommit -NotePropertyValue $sceneLock.hash + $facts | Add-Member -NotePropertyName eveUnityUiToolkitPackageCommit -NotePropertyValue $uiToolkitLock.hash $facts | Add-Member -NotePropertyName cultLibPackageCommit -NotePropertyValue $cultLibLock.hash $facts | Add-Member -NotePropertyName contentDelivery -NotePropertyValue ([ordered]@{ initialBodyCount = $initialBodyCount @@ -222,7 +227,7 @@ try { finalBodyCount = $finalBodies.Count finalPartialCount = $finalPartials.Count contentHash = $bodyHash - sizeBytes = $finalBodies[0].Length + sizeBytes = $promotedBundleBodies[0].Length }) $capture = Get-Item -LiteralPath $capturePath $mapCapture = Get-Item -LiteralPath $mapCapturePath From f314e213c314f8a151d56a5f91760aed49bad72f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 10:05:15 +0100 Subject: [PATCH 064/292] Require textured Aetheria hull materials --- .../Tests/PlayMode/GenericWorldCaptureTests.cs | 2 +- .../Tests/PlayMode/GenericWorldCaptureTests.cs | 2 +- docs/aetheria-released-package-witness.md | 18 ++++++++++-------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 57aadb6..addc5a0 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -496,7 +496,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests .Where(material => material != null) .Distinct() .Count(HasAnyTexture); - Assert.That(texturedPlayerMaterialCount, Is.GreaterThanOrEqualTo(3), + Assert.That(texturedPlayerMaterialCount, Is.GreaterThanOrEqualTo(9), "The provider bundle must retain its advertised native material textures."); var directionalLightIntensity = UnityEngine.Object.FindObjectsOfType() .Where(light => light != null && light.enabled && light.type == LightType.Directional) diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 57aadb6..addc5a0 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -496,7 +496,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests .Where(material => material != null) .Distinct() .Count(HasAnyTexture); - Assert.That(texturedPlayerMaterialCount, Is.GreaterThanOrEqualTo(3), + Assert.That(texturedPlayerMaterialCount, Is.GreaterThanOrEqualTo(9), "The provider bundle must retain its advertised native material textures."); var directionalLightIntensity = UnityEngine.Object.FindObjectsOfType() .Where(light => light != null && light.enabled && light.type == LightType.Directional) diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 9cb8016..d3ec677 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -43,11 +43,11 @@ native cockpit proof is the asserted UI Toolkit visual tree recorded as Camera-channel facts: - provider-authored player renderers: `12`; -- pilot changed pixels: `230,229`; -- pilot average luminance: `0.003307`; -- pilot bright pixels: `1,289`; +- pilot changed pixels: `230,203`; +- pilot average luminance: `0.004103`; +- pilot bright pixels: `1,420`; - map-channel renderers: `11`; -- map changed pixels: `5,396`; +- map changed pixels: `6,246`; - native cockpit progress bars: `6`; - the pilot camera excludes the advertised map layer; - the player prefab's embedded layer-14 map icon contributes exactly `0` pilot @@ -63,10 +63,12 @@ provider-owned light, so the next native-art work is available pre-generated provider textures, not a client-side lighting fallback. Substance is not part of this path; later texture baking belongs in Blender. Aetheria currently bundles ambientCG's 1K `Metal012` color, normal, and -metalness maps for its brushed-aluminium material under CC0. The released -witness resolves four native textures on that generated URP material. This is -temporary provider art with recorded provenance, not a replacement texture -pipeline. +metalness maps under CC0. The same pre-generated maps now replace dead +Substance archive sub-assets on the brushed aluminium, tinted car paint, +cockpit, steel, black-metal, and radiator materials. The released witness +resolves textured native URP materials in `9` distinct player-material slots, +up from `3`, without adding another texture payload. This is temporary provider +art with recorded provenance, not a replacement texture pipeline. Primary artifacts: From 44b441a2ffc8a4ca0393c9617cf609ed543e5593 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 10:16:55 +0100 Subject: [PATCH 065/292] Record released target lock cockpit proof --- docs/aetheria-released-package-witness.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index d3ec677..d84581a 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -28,8 +28,8 @@ Aetheria also advertises the original `ARPG.unity` follow-camera composition: `0.66,0.55`, and position damping `2`. The generic camera rig lowers those values without Aetheria-specific camera code. Aetheria's pilot surface also publishes a transparent cockpit overlay. The -released generic UI Toolkit lowerer produced six native `ProgressBar` elements -for hull, shield, capacitor, weapon cooldown, and target state. Daemon frame, +released generic UI Toolkit lowerer produced seven native `ProgressBar` elements +for hull, shield, capacitor, weapon cooldown, target lock, and target state. Daemon frame, player, and command diagnostics remain available in the document but lower with `display: none` in the pilot presentation. The runnable generic-client launcher mounts the same advertised document and forwards its typed commands; @@ -38,17 +38,18 @@ it contains no Aetheria gameplay or client-authored world light. The two PNGs below are deliberate camera-channel captures, not a composited screen capture, so they prove world/map isolation rather than HUD pixels. The native cockpit proof is the asserted UI Toolkit visual tree recorded as -`cockpitProgressCount: 6` in the witness facts. +`cockpitProgressCount: 7` in the witness facts. The same run records completed +daemon lock progress `1.0`; Unity does not manufacture or smooth that value. Camera-channel facts: - provider-authored player renderers: `12`; -- pilot changed pixels: `230,203`; -- pilot average luminance: `0.004103`; -- pilot bright pixels: `1,420`; +- pilot changed pixels: `230,189`; +- pilot average luminance: `0.003343`; +- pilot bright pixels: `1,227`; - map-channel renderers: `11`; - map changed pixels: `6,246`; -- native cockpit progress bars: `6`; +- native cockpit progress bars: `7`; - the pilot camera excludes the advertised map layer; - the player prefab's embedded layer-14 map icon contributes exactly `0` pilot pixels; From e4d83f342f1e6e00b0839d668919602ae732bf38 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 10:31:49 +0100 Subject: [PATCH 066/292] Lower provider-authored continuous beams --- TestProject/Packages/packages-lock.json | 13 +- .../org.gamecult.eve.unity-scene/README.md | 5 + .../Runtime/EveUnityBeamPresentation.cs | 67 ++++++++ .../Runtime/EveUnityBeamPresentation.cs.meta | 2 + .../EveUnityBeamPresentationRenderer.cs | 148 ++++++++++++++++++ .../EveUnityBeamPresentationRenderer.cs.meta | 2 + .../EveUnityPlayableWorldClientHost.cs | 3 + .../EveFieldsUnitySceneProjectionTests.cs | 2 +- .../Editor/EveUnityBeamPresentationTests.cs | 138 ++++++++++++++++ .../EveUnityBeamPresentationTests.cs.meta | 2 + .../org.gamecult.eve.unity-scene/package.json | 5 +- 11 files changed, 381 insertions(+), 6 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs.meta create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs.meta create mode 100644 packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs.meta diff --git a/TestProject/Packages/packages-lock.json b/TestProject/Packages/packages-lock.json index c270403..7a5b662 100644 --- a/TestProject/Packages/packages-lock.json +++ b/TestProject/Packages/packages-lock.json @@ -48,10 +48,11 @@ "depth": 0, "source": "local", "dependencies": { - "org.gamecult.cultlib": "1.0.9", + "org.gamecult.cultlib": "1.0.13", "org.gamecult.eve.plugin-fields": "0.1.1", - "org.gamecult.eve.surface": "0.2.0", - "com.unity.mathematics": "1.3.2" + "org.gamecult.eve.surface": "0.2.1", + "com.unity.mathematics": "1.3.2", + "com.unity.modules.particlesystem": "1.0.0" } }, "org.gamecult.eve.unity-uitoolkit": { @@ -92,6 +93,12 @@ "source": "builtin", "dependencies": {} }, + "com.unity.modules.particlesystem": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": {} + }, "com.unity.modules.physics": { "version": "1.0.0", "depth": 0, diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index 5ad692f..dd93898 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -49,6 +49,11 @@ new deduplicated shot receipt from the advertised controlled source to the advertised selection with authoritative applied or shield-absorbed damage. SoA remains the only transform authority; the presentation component never duplicates bodies or computes combat outcomes. +Continuous `beam.presentation` nodes bind provider-authored effect assets to +the current presented-entity generation. The generic beam renderer follows the +advertised source transform, applies provider-owned power to particle emission, +and reconciles asset lifecycles by semantic role. It never raycasts, applies +force, infers contact, collects cargo, or emits gameplay receipts. The reusable `LightningCompute` renderer and its shader/material bundle live in this runtime package with their original Unity GUIDs. Provider asset bundles may therefore reference the effect without importing Aetheria gameplay code. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs new file mode 100644 index 0000000..fc11c16 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +#nullable enable + +namespace GameCult.Eve.UnityScene +{ + public sealed class EveUnityBeamPresentation + { + public const string SourceForwardDirection = "source-forward.v1"; + + public EveUnityBeamPresentation(EveUnitySceneNode node) + { + if (node == null) throw new ArgumentNullException(nameof(node)); + Id = node.Id ?? ""; + SourceEntityId = Get(node, "sourceEntityId"); + AssetRole = Get(node, "assetRole"); + DirectionMode = Get(node, "directionMode"); + if (string.IsNullOrWhiteSpace(DirectionMode)) DirectionMode = SourceForwardDirection; + RenderChannel = Get(node, "renderChannel"); + ActivationActionId = Get(node, "activationActionId"); + Power = NonNegative(node, "power", 0f); + ActivationThreshold = NonNegative(node, "activationThreshold", 0.01f); + Radius = NonNegative(node, "radius", 0f); + MaximumDistance = NonNegative(node, "maximumDistance", 0f); + } + + public string Id { get; } + public string SourceEntityId { get; } + public string AssetRole { get; } + public string DirectionMode { get; } + public string RenderChannel { get; } + public string ActivationActionId { get; } + public float Power { get; } + public float ActivationThreshold { get; } + public float Radius { get; } + public float MaximumDistance { get; } + public bool UsesSourceForward => + string.Equals(DirectionMode, SourceForwardDirection, StringComparison.Ordinal); + + public static IReadOnlyList FindAll(EveUnitySceneProjection? projection) + { + var found = new List(); + if (projection != null) FindAll(projection.Root, found); + return found; + } + + private static void FindAll(EveUnitySceneNode node, ICollection found) + { + if (string.Equals(node.ComponentKind, "beam.presentation", StringComparison.Ordinal)) + found.Add(new EveUnityBeamPresentation(node)); + foreach (var child in node.Children) FindAll(child, found); + } + + private static string Get(EveUnitySceneNode node, string key) => + node.Props.TryGetValue(key, out var value) ? value ?? "" : ""; + + private static float NonNegative(EveUnitySceneNode node, string key, float fallback) + { + if (!float.TryParse(Get(node, key), NumberStyles.Float, CultureInfo.InvariantCulture, out var value) || + float.IsNaN(value) || float.IsInfinity(value)) + return fallback; + return Math.Max(0f, value); + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs.meta new file mode 100644 index 0000000..4acc86b --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3086cd42c94f2624197d19704909f3f5 \ No newline at end of file diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs new file mode 100644 index 0000000..c118c3b --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +#nullable enable + +namespace GameCult.Eve.UnityScene +{ + public sealed class EveUnityBeamPresentationRenderer : MonoBehaviour + { + private readonly Dictionary _active = new Dictionary(StringComparer.Ordinal); + private EveUnityPlayableWorldClientHost? _host; + private IEveUnityGameObjectAssetProvider? _assetProvider; + + public int ActiveBeamCount => _active.Count; + public int ActiveParticleSystemCount + { + get + { + var count = 0; + foreach (var beam in _active.Values) + if (beam.Root != null) + count += beam.Root.GetComponentsInChildren(true).Length; + return count; + } + } + + public void Bind( + EveUnityPlayableWorldClientHost host, + IEveUnityGameObjectAssetProvider? assetProvider = null) + { + _host = host != null ? host : throw new ArgumentNullException(nameof(host)); + _assetProvider = assetProvider; + } + + public void RefreshNow() + { + Apply( + EveUnityBeamPresentation.FindAll(_host?.ActiveProjection), + _host?.PresentedEntities, + _assetProvider); + } + + public void Apply( + IReadOnlyList presentations, + IEveUnityPresentedEntityRegistry? entities, + IEveUnityGameObjectAssetProvider? assetProvider) + { + var retained = new HashSet(StringComparer.Ordinal); + foreach (var presentation in presentations ?? Array.Empty()) + { + if (string.IsNullOrWhiteSpace(presentation.Id) || + string.IsNullOrWhiteSpace(presentation.SourceEntityId) || + string.IsNullOrWhiteSpace(presentation.AssetRole) || + !presentation.UsesSourceForward || entities == null || + !entities.TryGetByEntityId(presentation.SourceEntityId, out var source)) + continue; + + retained.Add(presentation.Id); + if (!_active.TryGetValue(presentation.Id, out var beam) || + !string.Equals(beam.SourceEntityId, presentation.SourceEntityId, StringComparison.Ordinal) || + !string.Equals(beam.AssetRole, presentation.AssetRole, StringComparison.Ordinal) || + beam.Root == null) + { + Remove(presentation.Id); + var prefab = assetProvider?.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( + "", presentation.AssetRole, "provider-asset-ref")); + if (prefab == null) continue; + var root = Instantiate(prefab, source.Transform, false); + root.name = "Eve beam " + presentation.Id; + beam = new ActiveBeam(root, presentation.SourceEntityId, presentation.AssetRole); + _active[presentation.Id] = beam; + } + else if (beam.Root.transform.parent != source.Transform) + { + beam.Root.transform.SetParent(source.Transform, false); + } + + var visiblePower = presentation.Power > presentation.ActivationThreshold + ? presentation.Power + : 0f; + foreach (var particles in beam.Root.GetComponentsInChildren(true)) + { + var emission = particles.emission; + emission.rateOverTimeMultiplier = visiblePower; + } + beam.Power = visiblePower; + } + + var stale = new List(); + foreach (var id in _active.Keys) + if (!retained.Contains(id)) stale.Add(id); + foreach (var id in stale) Remove(id); + } + + public bool TryGetPower(string presentationId, out float power) + { + if (_active.TryGetValue(presentationId ?? "", out var beam) && beam.Root != null) + { + power = beam.Power; + return true; + } + power = 0f; + return false; + } + + private void LateUpdate() => RefreshNow(); + + private void Remove(string id) + { + if (!_active.TryGetValue(id, out var beam)) return; + _active.Remove(id); + if (beam.Root != null) + { + if (Application.isPlaying) Destroy(beam.Root); + else DestroyImmediate(beam.Root); + } + } + + private void OnDestroy() + { + foreach (var beam in _active.Values) + if (beam.Root != null) + { + if (Application.isPlaying) Destroy(beam.Root); + else DestroyImmediate(beam.Root); + } + _active.Clear(); + _host = null; + _assetProvider = null; + } + + private sealed class ActiveBeam + { + public ActiveBeam(GameObject root, string sourceEntityId, string assetRole) + { + Root = root; + SourceEntityId = sourceEntityId; + AssetRole = assetRole; + } + + public GameObject Root { get; } + public string SourceEntityId { get; } + public string AssetRole { get; } + public float Power { get; set; } + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs.meta new file mode 100644 index 0000000..ecae7e9 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f91c99c24bc0e20488fa03daef2eec06 \ No newline at end of file diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs index 17741f4..22848d1 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs @@ -103,6 +103,9 @@ namespace GameCult.Eve.UnityScene var aimPresentation = GetComponent(); if (aimPresentation == null) aimPresentation = gameObject.AddComponent(); aimPresentation.Bind(this); + var beamPresentation = GetComponent(); + if (beamPresentation == null) beamPresentation = gameObject.AddComponent(); + beamPresentation.Bind(this, Runtime.GameObjectAssetProvider); var thermal = GetComponent(); var thermalHud = GetComponent(); if (thermalHud == null) thermalHud = gameObject.AddComponent(); diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index f3e130f..959b571 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -49,7 +49,7 @@ namespace GameCult.Eve.UnityScene.Tests var previous = RenderTexture.active; RenderTexture.active = output; - var readback = new Texture2D(64, 64, TextureFormat.RGBAFloat, false, true); + var readback = new Texture2D(64, 64, TextureFormat.RGBA32, false, true); readback.ReadPixels(new Rect(0, 0, 64, 64), 0, 0); readback.Apply(); RenderTexture.active = previous; diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs new file mode 100644 index 0000000..99e965f --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using UnityEngine; + +#nullable enable + +namespace GameCult.Eve.UnityScene.Tests +{ + public sealed class EveUnityBeamPresentationTests + { + [Test] + public void ProjectionPreservesProviderBeamSourceAssetAndFossilShape() + { + var beam = EveUnityBeamPresentation.FindAll(Projection(Node( + "tractor", "beam.presentation", new Dictionary(StringComparer.Ordinal) + { + ["sourceEntityId"] = "run.zone.0.entity.4", + ["assetRole"] = "effect.beam.tractor", + ["directionMode"] = "source-forward.v1", + ["renderChannel"] = "world.effects", + ["activationActionId"] = "pilot.scoop", + ["power"] = "0.625", + ["activationThreshold"] = "0.01", + ["radius"] = "25", + ["maximumDistance"] = "75" + }))).Single(); + + Assert.That(beam.SourceEntityId, Is.EqualTo("run.zone.0.entity.4")); + Assert.That(beam.AssetRole, Is.EqualTo("effect.beam.tractor")); + Assert.That(beam.ActivationActionId, Is.EqualTo("pilot.scoop")); + Assert.That(beam.UsesSourceForward, Is.True); + Assert.That(beam.Power, Is.EqualTo(0.625f).Within(0.0001f)); + Assert.That(beam.Radius, Is.EqualTo(25f)); + Assert.That(beam.MaximumDistance, Is.EqualTo(75f)); + } + + [Test] + public void RendererReconcilesProviderPrefabAgainstPresentedSourceWithoutPhysics() + { + var host = new GameObject("beam-test-host"); + var source = new GameObject("beam-test-source"); + var prefab = new GameObject("beam-test-prefab"); + prefab.AddComponent(); + var renderer = host.AddComponent(); + var registry = new Registry("source", source.transform); + var provider = new Provider(prefab); + try + { + renderer.Apply(new[] { Presentation(0.5f) }, registry, provider); + + Assert.That(provider.LastRole, Is.EqualTo("effect.beam.tractor")); + Assert.That(renderer.ActiveBeamCount, Is.EqualTo(1)); + Assert.That(renderer.TryGetPower("tractor", out var power), Is.True); + Assert.That(power, Is.EqualTo(0.5f).Within(0.0001f)); + var instance = source.transform.GetChild(0).gameObject; + Assert.That(instance.GetComponent().emission.rateOverTimeMultiplier, + Is.EqualTo(0.5f).Within(0.0001f)); + Assert.That(instance.GetComponentsInChildren(true), Is.Empty); + Assert.That(instance.GetComponentsInChildren(true), Is.Empty); + + renderer.Apply(new[] { Presentation(0.005f) }, registry, provider); + Assert.That(renderer.TryGetPower("tractor", out power), Is.True); + Assert.That(power, Is.Zero); + + renderer.Apply(Array.Empty(), registry, provider); + Assert.That(renderer.ActiveBeamCount, Is.Zero); + } + finally + { + UnityEngine.Object.DestroyImmediate(host); + UnityEngine.Object.DestroyImmediate(source); + UnityEngine.Object.DestroyImmediate(prefab); + } + } + + private static EveUnityBeamPresentation Presentation(float power) => new EveUnityBeamPresentation(Node( + "tractor", "beam.presentation", new Dictionary(StringComparer.Ordinal) + { + ["sourceEntityId"] = "source", + ["assetRole"] = "effect.beam.tractor", + ["directionMode"] = "source-forward.v1", + ["power"] = power.ToString(System.Globalization.CultureInfo.InvariantCulture), + ["activationThreshold"] = "0.01" + })); + + private static EveUnitySceneProjection Projection(params EveUnitySceneNode[] children) => + new EveUnitySceneProjection("provider", "surface", "world", "commands", "receipt", "provider", null, + Node("root", "surface", Empty(), children)); + + private static EveUnitySceneNode Node( + string id, + string kind, + IReadOnlyDictionary props, + params EveUnitySceneNode[] children) => + new EveUnitySceneNode(id, kind, "scene-node", props, Empty(), Empty(), 0, 0, + Array.Empty(), null, children); + + private static IReadOnlyDictionary Empty() => + new Dictionary(StringComparer.Ordinal); + + private sealed class Registry : IEveUnityPresentedEntityRegistry + { + private readonly EveUnityPresentedEntityHandle _handle; + public Registry(string entityId, Transform transform) + { + _handle = new EveUnityPresentedEntityHandle( + new EveUnityPresentedEntity(0, entityId, "ship", "", "", Vector3.zero, 0, Vector3.zero, + 1, 1, 0, 0, true, true, ""), + transform); + } + public EveUnityPresentedEntityGeneration? CurrentGeneration => null; + public bool TryGetByEntityId(string entityId, out EveUnityPresentedEntityHandle handle) + { + handle = _handle; + return string.Equals(entityId, _handle.Entity.EntityId, StringComparison.Ordinal); + } + public bool TryGetBySourceIndex(int sourceIndex, out EveUnityPresentedEntityHandle handle) + { + handle = _handle; + return sourceIndex == _handle.Entity.SourceIndex; + } + } + + private sealed class Provider : IEveUnityGameObjectAssetProvider + { + private readonly GameObject _prefab; + public Provider(GameObject prefab) { _prefab = prefab; } + public string LastRole { get; private set; } = ""; + public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) + { + LastRole = asset.EntityKind; + return _prefab; + } + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs.meta b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs.meta new file mode 100644 index 0000000..84e1c67 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7cd6b9cdacbecf14593d6ca2e0044849 \ No newline at end of file diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index f19cb5a..ac92254 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,14 +1,15 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.27", + "version": "0.3.28", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { "org.gamecult.cultlib": "1.0.13", "org.gamecult.eve.plugin-fields": "0.1.1", "org.gamecult.eve.surface": "0.2.1", - "com.unity.mathematics": "1.3.2" + "com.unity.mathematics": "1.3.2", + "com.unity.modules.particlesystem": "1.0.0" }, "keywords": [ "Eve", From 7272667e2cb9777dc40f3c8b3bed5f7ffd325560 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 10:53:19 +0100 Subject: [PATCH 067/292] Resolve provider beam roles in live clients --- .../Runtime/EveUnityBeamPresentationRenderer.cs | 2 +- .../Tests/Editor/EveUnityBeamPresentationTests.cs | 3 +++ packages/org.gamecult.eve.unity-scene/package.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs index c118c3b..4265b4c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs @@ -64,7 +64,7 @@ namespace GameCult.Eve.UnityScene { Remove(presentation.Id); var prefab = assetProvider?.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( - "", presentation.AssetRole, "provider-asset-ref")); + presentation.AssetRole, presentation.AssetRole, "provider-asset-ref")); if (prefab == null) continue; var root = Instantiate(prefab, source.Transform, false); root.name = "Eve beam " + presentation.Id; diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs index 99e965f..0af9e69 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs @@ -50,6 +50,7 @@ namespace GameCult.Eve.UnityScene.Tests { renderer.Apply(new[] { Presentation(0.5f) }, registry, provider); + Assert.That(provider.LastAssetRef, Is.EqualTo("effect.beam.tractor")); Assert.That(provider.LastRole, Is.EqualTo("effect.beam.tractor")); Assert.That(renderer.ActiveBeamCount, Is.EqualTo(1)); Assert.That(renderer.TryGetPower("tractor", out var power), Is.True); @@ -127,9 +128,11 @@ namespace GameCult.Eve.UnityScene.Tests { private readonly GameObject _prefab; public Provider(GameObject prefab) { _prefab = prefab; } + public string LastAssetRef { get; private set; } = ""; public string LastRole { get; private set; } = ""; public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) { + LastAssetRef = asset.AssetRef; LastRole = asset.EntityKind; return _prefab; } diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index ac92254..01fc2fa 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.28", + "version": "0.3.29", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From f979cd425a9f8f951736c56051e8368b6d6ac2f3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 10:58:17 +0100 Subject: [PATCH 068/292] Route input actions through advertised operations --- .../EveUnityPlayableWorldClientHost.cs | 22 +++++++++++++++++++ .../EveUnityPlayableWorldInputDriver.cs | 2 +- .../EveUnityPlayableWorldLiveClient.cs | 8 +++++++ .../Runtime/EveUnityPlayableWorldRuntime.cs | 9 ++++++++ .../EveUnitySceneProviderConnection.cs | 9 ++++++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 6 files changed, 50 insertions(+), 2 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs index 22848d1..f29bf73 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs @@ -198,6 +198,28 @@ namespace GameCult.Eve.UnityScene return RequireRuntime().SubmitActionIntent(entityId, actionId, issuedAt); } + public EveSurfaceCommandRequest SubmitAdvertisedActionIntent( + string entityId, + string actionId, + DateTimeOffset? issuedAt = null) + { + var capability = InputCapability ?? + throw new InvalidOperationException("The provider did not advertise an input capability."); + var action = Array.Find(capability.Actions ?? Array.Empty(), candidate => + string.Equals(candidate?.ActionId, actionId, StringComparison.Ordinal)); + if (action == null || string.IsNullOrWhiteSpace(action.Operation)) + throw new InvalidOperationException($"Input action '{actionId}' is not advertised with an operation."); + if (!string.Equals(action.Availability, "available", StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException($"Input action '{actionId}' is not available."); + + var payload = action.Payload == null + ? new Dictionary(StringComparer.Ordinal) + : new Dictionary(action.Payload, StringComparer.Ordinal); + payload["entityId"] = entityId ?? ""; + payload["actionId"] = action.ActionId; + return RequireRuntime().SubmitCommandIntent(action.Operation, payload, issuedAt); + } + public void Disconnect() { if (_cameraOwner is EveUnityPlayableWorldCameraRig rig) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs index 9abd10a..62f1d24 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs @@ -100,7 +100,7 @@ namespace GameCult.Eve.UnityScene if (string.IsNullOrWhiteSpace(resolvedActionId)) return null; - return resolvedHost.SubmitActionIntent( + return resolvedHost.SubmitAdvertisedActionIntent( resolvedHost.ActiveWorld.PlayerEntityId, resolvedActionId); } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs index adbb76e..9840feb 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs @@ -121,6 +121,14 @@ namespace GameCult.Eve.UnityScene return _connection.SubmitActionIntent(entityId, actionId, issuedAt); } + public EveSurfaceCommandRequest SubmitCommandIntent( + string commandId, + IReadOnlyDictionary? payload = null, + DateTimeOffset? issuedAt = null) + { + return _connection.SubmitCommandIntent(commandId, payload, issuedAt); + } + public void Disconnect() { if (_connected) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs index 28100fb..7af275f 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using GameCult.Eve.Surface; using UnityEngine; @@ -178,6 +179,14 @@ namespace GameCult.Eve.UnityScene return _client.SubmitActionIntent(entityId, actionId, issuedAt); } + public EveSurfaceCommandRequest SubmitCommandIntent( + string commandId, + IReadOnlyDictionary? payload = null, + DateTimeOffset? issuedAt = null) + { + return _client.SubmitCommandIntent(commandId, payload, issuedAt); + } + public void Disconnect() { if (_entityViewsConnected && _entityViews != null) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneProviderConnection.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneProviderConnection.cs index 3d4f18c..f38c3d5 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneProviderConnection.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneProviderConnection.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using GameCult.Eve.Surface; #nullable enable @@ -254,6 +255,14 @@ namespace GameCult.Eve.UnityScene return Submit(_session.CreateActionIntent(entityId, actionId, issuedAt)); } + public EveSurfaceCommandRequest SubmitCommandIntent( + string commandId, + IReadOnlyDictionary? payload = null, + DateTimeOffset? issuedAt = null) + { + return Submit(_session.CreatePlayableWorldIntent(commandId, payload, issuedAt)); + } + public void Disconnect() { if (!_connected) diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 01fc2fa..6997c1c 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.29", + "version": "0.3.30", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From f6fb8a5b1788bbc9456a0127c6c350bceaed7375 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 11:04:11 +0100 Subject: [PATCH 069/292] Record released tractor beam witness --- .../PlayMode/GenericWorldCaptureTests.cs | 57 ++++++++++++++++++- ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 13 ++++- .../PlayMode/GenericWorldCaptureTests.cs | 57 ++++++++++++++++++- docs/aetheria-released-package-witness.md | 32 +++++++---- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 6 files changed, 144 insertions(+), 19 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index addc5a0..050ab02 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -191,6 +191,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests WitnessReceipt movementReceipt = null; WitnessReceipt lookReceipt = null; WitnessReceipt focusReceipt = null; + WitnessReceipt tractorReceipt = null; WitnessReceipt actionReceipt = null; EveUnityShotReceipt observedShot = null; EveUnityCombatPresentation observedCombat = null; @@ -200,6 +201,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float movementDistance = 0f; float aimDotDistance = 0f; int cockpitProgressCount = 0; + float tractorBeamPower = 0f; + int tractorBeamParticleSystemCount = 0; try { @@ -245,7 +248,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var cockpit = loweredSurface.Q("aetheria.daemon.game.cockpit"); Assert.That(cockpit, Is.Not.Null, "The provider did not publish a generic cockpit overlay."); cockpitProgressCount = cockpit.Query().ToList().Count; - Assert.That(cockpitProgressCount, Is.GreaterThanOrEqualTo(6)); + Assert.That(cockpitProgressCount, Is.GreaterThanOrEqualTo(7)); Assert.That(loweredSurface.Q("aetheria.daemon.game.frame").style.display.value, Is.EqualTo(DisplayStyle.None), "Daemon diagnostics leaked into the pilot UI."); host.ShotAvailable += shot => observedShot = shot; @@ -376,6 +379,44 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var aimRenderer = root.GetComponent(); Assert.That(aimRenderer, Is.Not.Null); + var beamPresentation = EveUnityBeamPresentation.FindAll(runtime.ActiveProjection).SingleOrDefault(); + Assert.That(beamPresentation, Is.Not.Null, + "The provider did not advertise its continuous beam presentation."); + Assert.That(beamPresentation.ActivationActionId, Is.Not.Empty, + "The beam presentation does not name an advertised generic action."); + var tractorVersion = runtime.ActiveVersion; + var tractor = host.SubmitAdvertisedActionIntent(playerId, beamPresentation.ActivationActionId); + var tractorDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < tractorDeadline && + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != tractor.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || + runtime.ActiveVersion < runtime.LastReceipt.SourceVersion || + EveUnityBeamPresentation.FindAll(runtime.ActiveProjection).Single().Power <= + beamPresentation.ActivationThreshold)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + AssertReconciledProviderReceipt(runtime.LastReceipt, tractor.CommandId, + provider.Selection.ProviderId, provider.Selection.SurfaceId, tractorVersion); + tractorReceipt = WitnessReceipt.From("beam", runtime.LastReceipt); + Assert.That(beamPresentation.SourceEntityId, Is.EqualTo(playerId), + "The daemon beam source does not identify the presented player entity."); + Assert.That(host.PresentedEntities.TryGetByEntityId(beamPresentation.SourceEntityId, out _), Is.True, + "The generic beam lowerer cannot resolve the advertised source entity."); + Assert.That(provider.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( + beamPresentation.AssetRole, beamPresentation.AssetRole, "provider-asset-ref")), Is.Not.Null, + $"The provider asset catalog did not resolve beam role '{beamPresentation.AssetRole}'."); + var beamRenderer = root.GetComponent(); + Assert.That(beamRenderer, Is.Not.Null); + beamRenderer.RefreshNow(); + Assert.That(beamRenderer.ActiveBeamCount, Is.EqualTo(1)); + Assert.That(beamRenderer.TryGetPower(beamPresentation.Id, out tractorBeamPower), Is.True); + Assert.That(tractorBeamPower, Is.GreaterThan(beamPresentation.ActivationThreshold)); + tractorBeamParticleSystemCount = beamRenderer.ActiveParticleSystemCount; + Assert.That(tractorBeamParticleSystemCount, Is.GreaterThan(0), + "The generic beam lowerer did not instantiate the provider-owned effect prefab."); + var focusVersion = runtime.ActiveVersion; var inputDriver = root.GetComponent() ?? root.AddComponent(); @@ -573,6 +614,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests movementReceipt, lookReceipt, focusReceipt, + tractorReceipt, actionReceipt, observedShot, shieldRatioBefore, @@ -588,6 +630,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pilotAverageLuminance, pilotBrightPixelCount, cockpitProgressCount, + tractorBeamPower, + tractorBeamParticleSystemCount, playerRendererFacts); } finally @@ -620,6 +664,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests WitnessReceipt movement, WitnessReceipt look, WitnessReceipt targeting, + WitnessReceipt tractor, WitnessReceipt action, EveUnityShotReceipt shot, float shieldRatioBefore, @@ -635,6 +680,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float pilotAverageLuminance, int pilotBrightPixelCount, int cockpitProgressCount, + float tractorBeamPower, + int tractorBeamParticleSystemCount, RendererFact[] playerRendererFacts) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); @@ -647,6 +694,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests movement = movement != null, aimPresentation = look != null && aimDotDistance >= 49.9f, targeting = targeting != null, + beamPresentation = tractor != null && tractorBeamPower > 0.01f && tractorBeamParticleSystemCount > 0, action = action != null, combatPresentation = shot != null, shotId = shot?.ShotId ?? "", @@ -672,12 +720,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pilotAverageLuminance = pilotAverageLuminance, pilotBrightPixelCount = pilotBrightPixelCount, cockpitProgressCount = cockpitProgressCount, + tractorBeamPower = tractorBeamPower, + tractorBeamParticleSystemCount = tractorBeamParticleSystemCount, playerRenderers = playerRendererFacts, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, mapCameraIncludesMapChannel = mapCameraIncludesMapChannel, - receipts = new[] { movement, look, targeting, action } + receipts = new[] { movement, look, targeting, tractor, action } }; Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); File.WriteAllText(path, JsonUtility.ToJson(document, true)); @@ -692,6 +742,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public bool movement; public bool aimPresentation; public bool targeting; + public bool beamPresentation; public bool action; public bool combatPresentation; public string shotId; @@ -717,6 +768,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float pilotAverageLuminance; public int pilotBrightPixelCount; public int cockpitProgressCount; + public float tractorBeamPower; + public int tractorBeamParticleSystemCount; public RendererFact[] playerRenderers; public int mapChannelRendererCount; public int mapChangedPixels; diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index cdbd57c..1dfc17c 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.27", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.30", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 5979111..269434a 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -142,16 +142,17 @@ "hash": "d0aa50d31f3b6da96bffc194fdc3aa358f37e332" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.27", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.30", "depth": 0, "source": "git", "dependencies": { "org.gamecult.cultlib": "1.0.13", "org.gamecult.eve.plugin-fields": "0.1.1", "org.gamecult.eve.surface": "0.2.1", - "com.unity.mathematics": "1.3.2" + "com.unity.mathematics": "1.3.2", + "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "63a41712c5a68ce835bc1820980fc4786c34eb7b" + "hash": "f979cd425a9f8f951736c56051e8368b6d6ac2f3" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", @@ -192,6 +193,12 @@ "source": "builtin", "dependencies": {} }, + "com.unity.modules.particlesystem": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": {} + }, "com.unity.modules.physics": { "version": "1.0.0", "depth": 0, diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index addc5a0..050ab02 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -191,6 +191,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests WitnessReceipt movementReceipt = null; WitnessReceipt lookReceipt = null; WitnessReceipt focusReceipt = null; + WitnessReceipt tractorReceipt = null; WitnessReceipt actionReceipt = null; EveUnityShotReceipt observedShot = null; EveUnityCombatPresentation observedCombat = null; @@ -200,6 +201,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float movementDistance = 0f; float aimDotDistance = 0f; int cockpitProgressCount = 0; + float tractorBeamPower = 0f; + int tractorBeamParticleSystemCount = 0; try { @@ -245,7 +248,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var cockpit = loweredSurface.Q("aetheria.daemon.game.cockpit"); Assert.That(cockpit, Is.Not.Null, "The provider did not publish a generic cockpit overlay."); cockpitProgressCount = cockpit.Query().ToList().Count; - Assert.That(cockpitProgressCount, Is.GreaterThanOrEqualTo(6)); + Assert.That(cockpitProgressCount, Is.GreaterThanOrEqualTo(7)); Assert.That(loweredSurface.Q("aetheria.daemon.game.frame").style.display.value, Is.EqualTo(DisplayStyle.None), "Daemon diagnostics leaked into the pilot UI."); host.ShotAvailable += shot => observedShot = shot; @@ -376,6 +379,44 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var aimRenderer = root.GetComponent(); Assert.That(aimRenderer, Is.Not.Null); + var beamPresentation = EveUnityBeamPresentation.FindAll(runtime.ActiveProjection).SingleOrDefault(); + Assert.That(beamPresentation, Is.Not.Null, + "The provider did not advertise its continuous beam presentation."); + Assert.That(beamPresentation.ActivationActionId, Is.Not.Empty, + "The beam presentation does not name an advertised generic action."); + var tractorVersion = runtime.ActiveVersion; + var tractor = host.SubmitAdvertisedActionIntent(playerId, beamPresentation.ActivationActionId); + var tractorDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < tractorDeadline && + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != tractor.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || + runtime.ActiveVersion < runtime.LastReceipt.SourceVersion || + EveUnityBeamPresentation.FindAll(runtime.ActiveProjection).Single().Power <= + beamPresentation.ActivationThreshold)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + AssertReconciledProviderReceipt(runtime.LastReceipt, tractor.CommandId, + provider.Selection.ProviderId, provider.Selection.SurfaceId, tractorVersion); + tractorReceipt = WitnessReceipt.From("beam", runtime.LastReceipt); + Assert.That(beamPresentation.SourceEntityId, Is.EqualTo(playerId), + "The daemon beam source does not identify the presented player entity."); + Assert.That(host.PresentedEntities.TryGetByEntityId(beamPresentation.SourceEntityId, out _), Is.True, + "The generic beam lowerer cannot resolve the advertised source entity."); + Assert.That(provider.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( + beamPresentation.AssetRole, beamPresentation.AssetRole, "provider-asset-ref")), Is.Not.Null, + $"The provider asset catalog did not resolve beam role '{beamPresentation.AssetRole}'."); + var beamRenderer = root.GetComponent(); + Assert.That(beamRenderer, Is.Not.Null); + beamRenderer.RefreshNow(); + Assert.That(beamRenderer.ActiveBeamCount, Is.EqualTo(1)); + Assert.That(beamRenderer.TryGetPower(beamPresentation.Id, out tractorBeamPower), Is.True); + Assert.That(tractorBeamPower, Is.GreaterThan(beamPresentation.ActivationThreshold)); + tractorBeamParticleSystemCount = beamRenderer.ActiveParticleSystemCount; + Assert.That(tractorBeamParticleSystemCount, Is.GreaterThan(0), + "The generic beam lowerer did not instantiate the provider-owned effect prefab."); + var focusVersion = runtime.ActiveVersion; var inputDriver = root.GetComponent() ?? root.AddComponent(); @@ -573,6 +614,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests movementReceipt, lookReceipt, focusReceipt, + tractorReceipt, actionReceipt, observedShot, shieldRatioBefore, @@ -588,6 +630,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pilotAverageLuminance, pilotBrightPixelCount, cockpitProgressCount, + tractorBeamPower, + tractorBeamParticleSystemCount, playerRendererFacts); } finally @@ -620,6 +664,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests WitnessReceipt movement, WitnessReceipt look, WitnessReceipt targeting, + WitnessReceipt tractor, WitnessReceipt action, EveUnityShotReceipt shot, float shieldRatioBefore, @@ -635,6 +680,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float pilotAverageLuminance, int pilotBrightPixelCount, int cockpitProgressCount, + float tractorBeamPower, + int tractorBeamParticleSystemCount, RendererFact[] playerRendererFacts) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); @@ -647,6 +694,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests movement = movement != null, aimPresentation = look != null && aimDotDistance >= 49.9f, targeting = targeting != null, + beamPresentation = tractor != null && tractorBeamPower > 0.01f && tractorBeamParticleSystemCount > 0, action = action != null, combatPresentation = shot != null, shotId = shot?.ShotId ?? "", @@ -672,12 +720,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pilotAverageLuminance = pilotAverageLuminance, pilotBrightPixelCount = pilotBrightPixelCount, cockpitProgressCount = cockpitProgressCount, + tractorBeamPower = tractorBeamPower, + tractorBeamParticleSystemCount = tractorBeamParticleSystemCount, playerRenderers = playerRendererFacts, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, mapCameraIncludesMapChannel = mapCameraIncludesMapChannel, - receipts = new[] { movement, look, targeting, action } + receipts = new[] { movement, look, targeting, tractor, action } }; Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); File.WriteAllText(path, JsonUtility.ToJson(document, true)); @@ -692,6 +742,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public bool movement; public bool aimPresentation; public bool targeting; + public bool beamPresentation; public bool action; public bool combatPresentation; public string shotId; @@ -717,6 +768,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float pilotAverageLuminance; public int pilotBrightPixelCount; public int cockpitProgressCount; + public float tractorBeamPower; + public int tractorBeamParticleSystemCount; public RendererFact[] playerRenderers; public int mapChannelRendererCount; public int mapChangedPixels; diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index d84581a..0bb254b 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,8 +4,8 @@ The warm-cache released-package witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.27`, commit - `63a41712c5a68ce835bc1820980fc4786c34eb7b`; +- `org.gamecult.eve.unity-scene` `0.3.30`, commit + `f979cd425a9f8f951736c56051e8368b6d6ac2f3`; - `org.gamecult.eve.unity-uitoolkit` `0.1.1`, commit `4d0cbe0185bdc4fc65eb63503a7c5cb578539669`; - `org.gamecult.cultlib` `1.0.13`, commit @@ -14,8 +14,8 @@ The warm-cache released-package witness passes with: Aetheria daemon. Evidence is in `artifacts/aetheria-daemon-native-0326-warm`. The PlayMode test -passed and recorded provider-owned reconciled movement, look, targeting, and -action receipts. The daemon-owned look direction reached the SoA body rotation, +passed and recorded provider-owned reconciled movement, look, targeting, +tractor, and action receipts. The daemon-owned look direction reached the SoA body rotation, and the generic aim presentation rendered at the advertised 50-unit minimum convergence distance. The advertised skybox material and reflection cubemap resolved from the provider bundle as their required native types and became the @@ -43,13 +43,15 @@ daemon lock progress `1.0`; Unity does not manufacture or smooth that value. Camera-channel facts: -- provider-authored player renderers: `12`; -- pilot changed pixels: `230,189`; -- pilot average luminance: `0.003343`; -- pilot bright pixels: `1,227`; +- provider-authored player renderers: `14`, including the lowered tractor effect; +- pilot changed pixels: `230,255`; +- pilot average luminance: `0.004634`; +- pilot bright pixels: `1,542`; - map-channel renderers: `11`; -- map changed pixels: `6,246`; +- map changed pixels: `8,510`; - native cockpit progress bars: `7`; +- daemon tractor power at capture: `0.24`; +- provider tractor particle systems: `1`; - the pilot camera excludes the advertised map layer; - the player prefab's embedded layer-14 map icon contributes exactly `0` pilot pixels; @@ -71,6 +73,14 @@ resolves textured native URP materials in `9` distinct player-material slots, up from `3`, without adding another texture payload. This is temporary provider art with recorded provenance, not a replacement texture pipeline. +The tractor proof is structural, not yet a native-look victory lap. The daemon +publishes `beam.presentation`, the generic client routes `pilot.scoop` through +the advertised `SetTractorPower` operation and payload, and the provider-owned +prefab is attached to the SoA-presented ship with nonzero daemon power. Its +particle renderer contributes only one measured pilot pixel in this capture. +Effect tuning remains provider art work; the runtime must not compensate with +an Aetheria-specific beam. + Primary artifacts: - `results.xml`: one passing PlayMode witness; @@ -81,7 +91,9 @@ Primary artifacts: ## Cold delivery is not currently proven -The current 46.1 MB Unity bundle times out from an empty cache. Its bytes are +The current `46,133,487` byte Unity bundle (SHA-256 +`ff07aa7f71aa2da0e8e6fe2f4ed68900931d6e67c153b586777467316d4fb80f`) +times out from an empty cache. Its bytes are still transported through batched snapshot records rather than the intended mapped/network body transport. Increasing the Unity timeout would hide the transport fault and is not an accepted fix. diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 4512bbe..3069618 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "63a41712c5a68ce835bc1820980fc4786c34eb7b" +$expectedEveUnityCommit = "f979cd425a9f8f951736c56051e8368b6d6ac2f3" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot From 140e1bd963a0033e66777a3b2c5fe6e9c97dfe32 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 11:14:22 +0100 Subject: [PATCH 070/292] Lower advertised held input values --- .../PlayMode/GenericWorldCaptureTests.cs | 37 +++++- TestProject/Packages/packages-lock.json | 2 +- .../Runtime/EveInputCapabilityDocument.cs | 8 ++ .../org.gamecult.eve.surface/package.json | 2 +- .../Runtime/EveUnityAdvertisedInputAction.cs | 63 +++++++++ .../EveUnityAdvertisedInputAction.cs.meta | 2 + .../EveUnityPlayableWorldClientHost.cs | 28 ++-- .../EveUnityPlayableWorldInputDriver.cs | 120 ++++++++++++++++++ .../EveUnityAdvertisedInputActionTests.cs | 68 ++++++++++ ...EveUnityAdvertisedInputActionTests.cs.meta | 2 + .../org.gamecult.eve.unity-scene/package.json | 4 +- 11 files changed, 316 insertions(+), 20 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdvertisedInputAction.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdvertisedInputAction.cs.meta create mode 100644 packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAdvertisedInputActionTests.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAdvertisedInputActionTests.cs.meta diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 050ab02..ce53c38 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -192,6 +192,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests WitnessReceipt lookReceipt = null; WitnessReceipt focusReceipt = null; WitnessReceipt tractorReceipt = null; + WitnessReceipt tractorReleaseReceipt = null; WitnessReceipt actionReceipt = null; EveUnityShotReceipt observedShot = null; EveUnityCombatPresentation observedCombat = null; @@ -202,6 +203,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float aimDotDistance = 0f; int cockpitProgressCount = 0; float tractorBeamPower = 0f; + float tractorReleasedPower = 1f; int tractorBeamParticleSystemCount = 0; try @@ -385,7 +387,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(beamPresentation.ActivationActionId, Is.Not.Empty, "The beam presentation does not name an advertised generic action."); var tractorVersion = runtime.ActiveVersion; - var tractor = host.SubmitAdvertisedActionIntent(playerId, beamPresentation.ActivationActionId); + var tractor = host.SubmitAdvertisedActionValueIntent(playerId, beamPresentation.ActivationActionId, 1f); var tractorDeadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < tractorDeadline && (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != tractor.CommandId || @@ -600,6 +602,29 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(mapCapturePath))); File.WriteAllBytes(mapCapturePath, pixels.EncodeToPNG()); Assert.That(new FileInfo(mapCapturePath).Length, Is.GreaterThan(1024)); + var tractorReleaseVersion = runtime.ActiveVersion; + var tractorRelease = host.SubmitAdvertisedActionValueIntent( + playerId, + beamPresentation.ActivationActionId, + 0f); + var tractorReleaseDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < tractorReleaseDeadline && + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != tractorRelease.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || + runtime.ActiveVersion < runtime.LastReceipt.SourceVersion || + EveUnityBeamPresentation.FindAll(runtime.ActiveProjection).Single().Power > + beamPresentation.ActivationThreshold)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + AssertReconciledProviderReceipt(runtime.LastReceipt, tractorRelease.CommandId, + provider.Selection.ProviderId, provider.Selection.SurfaceId, tractorReleaseVersion); + tractorReleaseReceipt = WitnessReceipt.From("beam-release", runtime.LastReceipt); + beamRenderer.RefreshNow(); + Assert.That(beamRenderer.TryGetPower(beamPresentation.Id, out tractorReleasedPower), Is.True); + Assert.That(tractorReleasedPower, Is.Zero, + "Released held input did not return the daemon-authored tractor presentation to zero."); WriteWitnessFacts( initialVersion, runtime.ActiveVersion, @@ -615,6 +640,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests lookReceipt, focusReceipt, tractorReceipt, + tractorReleaseReceipt, actionReceipt, observedShot, shieldRatioBefore, @@ -631,6 +657,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pilotBrightPixelCount, cockpitProgressCount, tractorBeamPower, + tractorReleasedPower, tractorBeamParticleSystemCount, playerRendererFacts); } @@ -665,6 +692,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests WitnessReceipt look, WitnessReceipt targeting, WitnessReceipt tractor, + WitnessReceipt tractorRelease, WitnessReceipt action, EveUnityShotReceipt shot, float shieldRatioBefore, @@ -681,6 +709,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests int pilotBrightPixelCount, int cockpitProgressCount, float tractorBeamPower, + float tractorReleasedPower, int tractorBeamParticleSystemCount, RendererFact[] playerRendererFacts) { @@ -695,6 +724,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests aimPresentation = look != null && aimDotDistance >= 49.9f, targeting = targeting != null, beamPresentation = tractor != null && tractorBeamPower > 0.01f && tractorBeamParticleSystemCount > 0, + beamRelease = tractorRelease != null && tractorReleasedPower == 0f, action = action != null, combatPresentation = shot != null, shotId = shot?.ShotId ?? "", @@ -721,13 +751,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pilotBrightPixelCount = pilotBrightPixelCount, cockpitProgressCount = cockpitProgressCount, tractorBeamPower = tractorBeamPower, + tractorReleasedPower = tractorReleasedPower, tractorBeamParticleSystemCount = tractorBeamParticleSystemCount, playerRenderers = playerRendererFacts, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, mapCameraIncludesMapChannel = mapCameraIncludesMapChannel, - receipts = new[] { movement, look, targeting, tractor, action } + receipts = new[] { movement, look, targeting, tractor, tractorRelease, action } }; Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); File.WriteAllText(path, JsonUtility.ToJson(document, true)); @@ -743,6 +774,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public bool aimPresentation; public bool targeting; public bool beamPresentation; + public bool beamRelease; public bool action; public bool combatPresentation; public string shotId; @@ -769,6 +801,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public int pilotBrightPixelCount; public int cockpitProgressCount; public float tractorBeamPower; + public float tractorReleasedPower; public int tractorBeamParticleSystemCount; public RendererFact[] playerRenderers; public int mapChannelRendererCount; diff --git a/TestProject/Packages/packages-lock.json b/TestProject/Packages/packages-lock.json index 7a5b662..1f1ebaf 100644 --- a/TestProject/Packages/packages-lock.json +++ b/TestProject/Packages/packages-lock.json @@ -50,7 +50,7 @@ "dependencies": { "org.gamecult.cultlib": "1.0.13", "org.gamecult.eve.plugin-fields": "0.1.1", - "org.gamecult.eve.surface": "0.2.1", + "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" } diff --git a/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs b/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs index 356edee..d3fdb61 100644 --- a/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs +++ b/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs @@ -29,6 +29,14 @@ namespace GameCult.Eve.Surface [Key(5)] public string Availability { get; set; } = "available"; [Key(6)] public string SourceRef { get; set; } = ""; [Key(7)] public Dictionary Payload { get; set; } = new Dictionary(StringComparer.Ordinal); + [Key(8)] public EveInputValueDocument? InputValue { get; set; } + } + + [MessagePackObject] + public sealed class EveInputValueDocument + { + [Key(0)] public string Model { get; set; } = ""; + [Key(1)] public string PayloadKey { get; set; } = ""; } [MessagePackObject] diff --git a/packages/org.gamecult.eve.surface/package.json b/packages/org.gamecult.eve.surface/package.json index 0da4785..87896d6 100644 --- a/packages/org.gamecult.eve.surface/package.json +++ b/packages/org.gamecult.eve.surface/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.surface", "displayName": "Eve Surface Contract", - "version": "0.2.1", + "version": "0.2.2", "unity": "2021.3", "description": "Unity-compatible DTOs for gamecult.eve.surface.v1 documents.", "dependencies": { diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdvertisedInputAction.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdvertisedInputAction.cs new file mode 100644 index 0000000..33d5748 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdvertisedInputAction.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using GameCult.Eve.Surface; + +#nullable enable + +namespace GameCult.Eve.UnityScene +{ + public sealed class EveUnityAdvertisedInputAction + { + public const string ButtonHoldValueModel = "button-hold.v1"; + public const string AxisValueModel = "axis.v1"; + + private readonly EveInputActionDocument _action; + + private EveUnityAdvertisedInputAction(EveInputActionDocument action) + { + _action = action; + } + + public string ActionId => _action.ActionId ?? ""; + public string Operation => _action.Operation ?? ""; + public string Availability => _action.Availability ?? ""; + public string InputValueModel => _action.InputValue?.Model ?? ""; + public string InputValuePayloadKey => _action.InputValue?.PayloadKey ?? ""; + public bool IsButtonHold => string.Equals(InputValueModel, ButtonHoldValueModel, StringComparison.Ordinal); + + public static EveUnityAdvertisedInputAction Resolve(EveInputCapabilityDocument? capability, string actionId) + { + if (capability == null) + throw new InvalidOperationException("The provider did not advertise an input capability."); + var action = (capability.Actions ?? Array.Empty()).FirstOrDefault(candidate => + string.Equals(candidate?.ActionId, actionId, StringComparison.Ordinal)); + if (action == null || string.IsNullOrWhiteSpace(action.Operation)) + throw new InvalidOperationException($"Input action '{actionId}' is not advertised with an operation."); + if (!string.Equals(action.Availability, "available", StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException($"Input action '{actionId}' is not available."); + return new EveUnityAdvertisedInputAction(action); + } + + public IReadOnlyDictionary BuildPayload(string entityId, float? inputValue = null) + { + var payload = _action.Payload == null + ? new Dictionary(StringComparer.Ordinal) + : new Dictionary(_action.Payload, StringComparer.Ordinal); + payload["entityId"] = entityId ?? ""; + payload["actionId"] = ActionId; + + if (inputValue.HasValue) + { + if (float.IsNaN(inputValue.Value) || float.IsInfinity(inputValue.Value)) + throw new ArgumentOutOfRangeException(nameof(inputValue), "Input action value must be finite."); + if (string.IsNullOrWhiteSpace(InputValueModel) || string.IsNullOrWhiteSpace(InputValuePayloadKey)) + throw new InvalidOperationException($"Input action '{ActionId}' does not advertise a value payload."); + payload[InputValuePayloadKey] = inputValue.Value.ToString("R", CultureInfo.InvariantCulture); + } + + return payload; + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdvertisedInputAction.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdvertisedInputAction.cs.meta new file mode 100644 index 0000000..a2236f2 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdvertisedInputAction.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c7555b03e7f9451aa891e92958b845c5 diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs index f29bf73..4dfdf43 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs @@ -203,21 +203,21 @@ namespace GameCult.Eve.UnityScene string actionId, DateTimeOffset? issuedAt = null) { - var capability = InputCapability ?? - throw new InvalidOperationException("The provider did not advertise an input capability."); - var action = Array.Find(capability.Actions ?? Array.Empty(), candidate => - string.Equals(candidate?.ActionId, actionId, StringComparison.Ordinal)); - if (action == null || string.IsNullOrWhiteSpace(action.Operation)) - throw new InvalidOperationException($"Input action '{actionId}' is not advertised with an operation."); - if (!string.Equals(action.Availability, "available", StringComparison.OrdinalIgnoreCase)) - throw new InvalidOperationException($"Input action '{actionId}' is not available."); + var action = EveUnityAdvertisedInputAction.Resolve(InputCapability, actionId); + return RequireRuntime().SubmitCommandIntent(action.Operation, action.BuildPayload(entityId), issuedAt); + } - var payload = action.Payload == null - ? new Dictionary(StringComparer.Ordinal) - : new Dictionary(action.Payload, StringComparer.Ordinal); - payload["entityId"] = entityId ?? ""; - payload["actionId"] = action.ActionId; - return RequireRuntime().SubmitCommandIntent(action.Operation, payload, issuedAt); + public EveSurfaceCommandRequest SubmitAdvertisedActionValueIntent( + string entityId, + string actionId, + float inputValue, + DateTimeOffset? issuedAt = null) + { + var action = EveUnityAdvertisedInputAction.Resolve(InputCapability, actionId); + return RequireRuntime().SubmitCommandIntent( + action.Operation, + action.BuildPayload(entityId, inputValue), + issuedAt); } public void Disconnect() diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs index 62f1d24..1dbca9c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using GameCult.Eve.Surface; using UnityEngine; @@ -32,6 +33,10 @@ namespace GameCult.Eve.UnityScene private bool _hasLookYaw; private float _pendingLookHorizontal; private string _lookOwnerKey = ""; + private readonly Dictionary _heldBindings = + new Dictionary(StringComparer.Ordinal); + + public Func? DigitalControlReader { get; set; } public EveUnityPlayableWorldClientHost? Host { @@ -39,6 +44,7 @@ namespace GameCult.Eve.UnityScene set { if (ReferenceEquals(host, value)) return; + ReleaseHeldActions(); host = value; ResetLookState(); } @@ -105,6 +111,21 @@ namespace GameCult.Eve.UnityScene resolvedActionId); } + public EveSurfaceCommandRequest? SubmitActionValue( + string actionId, + float inputValue, + DateTimeOffset? issuedAt = null) + { + var resolvedHost = ResolveHost(); + if (resolvedHost?.ActiveWorld == null || string.IsNullOrWhiteSpace(actionId)) + return null; + return resolvedHost.SubmitAdvertisedActionValueIntent( + resolvedHost.ActiveWorld.PlayerEntityId, + actionId, + inputValue, + issuedAt); + } + public EveSurfaceCommandRequest? SubmitLookInput( float horizontalDelta, DateTimeOffset? issuedAt = null) @@ -178,11 +199,98 @@ namespace GameCult.Eve.UnityScene _nextCommandAt = Time.unscaledTime + Math.Max(0.01f, commandIntervalSeconds); SubmitMoveVectorInput(Input.GetAxisRaw(horizontalAxis), Input.GetAxisRaw(verticalAxis)); SubmitPendingLookInput(); + SubmitChangedHeldActions(); if (!string.IsNullOrWhiteSpace(actionButton) && Input.GetButtonDown(actionButton)) SubmitPrimaryAction(); } + private void SubmitChangedHeldActions() + { + var resolvedHost = ResolveHost(); + var capability = resolvedHost?.InputCapability; + if (resolvedHost?.ActiveWorld == null || capability == null) + return; + + var profile = (capability.DefaultProfiles ?? Array.Empty()) + .FirstOrDefault(value => string.Equals(value.DeviceClass, "keyboard-mouse", StringComparison.Ordinal)); + var retained = new HashSet(StringComparer.Ordinal); + foreach (var binding in profile?.Bindings ?? Array.Empty()) + { + var action = (capability.Actions ?? Array.Empty()) + .FirstOrDefault(candidate => string.Equals(candidate.ActionId, binding.ActionId, StringComparison.Ordinal)); + if (action?.InputValue == null || + !string.Equals(action.InputValue.Model, EveUnityAdvertisedInputAction.ButtonHoldValueModel, StringComparison.Ordinal) || + !TryReadDigitalGesture(binding.Gesture, out var pressed)) + continue; + + var stateKey = string.IsNullOrWhiteSpace(binding.BindingId) ? binding.ActionId : binding.BindingId; + retained.Add(stateKey); + if (!_heldBindings.TryGetValue(stateKey, out var previous)) + { + _heldBindings[stateKey] = new HeldBindingState(binding.ActionId, pressed); + if (pressed) SubmitActionValue(binding.ActionId, 1f); + continue; + } + if (previous.Pressed == pressed) continue; + _heldBindings[stateKey] = new HeldBindingState(binding.ActionId, pressed); + SubmitActionValue(binding.ActionId, pressed ? 1f : 0f); + } + + foreach (var stale in _heldBindings.Keys.Where(key => !retained.Contains(key)).ToArray()) + { + var previous = _heldBindings[stale]; + if (previous.Pressed) TrySubmitRelease(previous.ActionId); + _heldBindings.Remove(stale); + } + } + + private bool TryReadDigitalGesture(EveInputGestureDocument? gesture, out bool pressed) + { + pressed = false; + if (gesture == null || + !(string.Equals(gesture.Kind, "direct", StringComparison.Ordinal) || + string.Equals(gesture.Kind, "chord", StringComparison.Ordinal))) + return false; + var controls = gesture.Controls ?? Array.Empty(); + if (controls.Length == 0) return false; + foreach (var control in controls) + { + var value = DigitalControlReader?.Invoke(control) ?? ReadStandardDigitalControl(control); + if (!value.HasValue) return false; + if (!value.Value) return true; + } + pressed = true; + return true; + } + + private static bool? ReadStandardDigitalControl(string control) + { + if (string.Equals(control, "mouse.primary", StringComparison.Ordinal)) return Input.GetMouseButton(0); + if (string.Equals(control, "mouse.secondary", StringComparison.Ordinal)) return Input.GetMouseButton(1); + if (string.Equals(control, "mouse.middle", StringComparison.Ordinal)) return Input.GetMouseButton(2); + const string keyboardPrefix = "keyboard."; + if (control != null && control.StartsWith(keyboardPrefix, StringComparison.Ordinal) && + Enum.TryParse(control.Substring(keyboardPrefix.Length), true, out KeyCode key)) + return Input.GetKey(key); + return null; + } + + private void OnDisable() => ReleaseHeldActions(); + + private void ReleaseHeldActions() + { + foreach (var state in _heldBindings.Values) + if (state.Pressed) TrySubmitRelease(state.ActionId); + _heldBindings.Clear(); + } + + private void TrySubmitRelease(string actionId) + { + try { SubmitActionValue(actionId, 0f); } + catch (InvalidOperationException) { } + } + private EveUnityPlayableWorldClientHost? ResolveHost() { if (host != null) @@ -207,6 +315,18 @@ namespace GameCult.Eve.UnityScene _pendingLookHorizontal = 0f; _lookOwnerKey = ""; } + + private readonly struct HeldBindingState + { + public HeldBindingState(string actionId, bool pressed) + { + ActionId = actionId ?? ""; + Pressed = pressed; + } + + public string ActionId { get; } + public bool Pressed { get; } + } } public readonly struct EveUnityPlayableWorldLookDirection diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAdvertisedInputActionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAdvertisedInputActionTests.cs new file mode 100644 index 0000000..8174a80 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAdvertisedInputActionTests.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using GameCult.Eve.Surface; +using NUnit.Framework; + +#nullable enable + +namespace GameCult.Eve.UnityScene.Tests +{ + public sealed class EveUnityAdvertisedInputActionTests + { + [Test] + public void HeldValueOverridesAdvertisedPayloadWithoutMutatingCapability() + { + var sourcePayload = new Dictionary(StringComparer.Ordinal) + { + ["scalarValue"] = "0" + }; + var action = EveUnityAdvertisedInputAction.Resolve(new EveInputCapabilityDocument + { + Actions = new[] + { + new EveInputActionDocument + { + ActionId = "pilot.scoop", + Operation = "aetheria.daemon.commands.SetTractorPower", + Availability = "available", + Payload = sourcePayload, + InputValue = new EveInputValueDocument + { + Model = EveUnityAdvertisedInputAction.ButtonHoldValueModel, + PayloadKey = "scalarValue" + } + } + } + }, "pilot.scoop"); + + var pressed = action.BuildPayload("entity.4", 1f); + var released = action.BuildPayload("entity.4", 0f); + + Assert.That(action.IsButtonHold, Is.True); + Assert.That(pressed["scalarValue"], Is.EqualTo("1")); + Assert.That(released["scalarValue"], Is.EqualTo("0")); + Assert.That(pressed["entityId"], Is.EqualTo("entity.4")); + Assert.That(pressed["actionId"], Is.EqualTo("pilot.scoop")); + Assert.That(sourcePayload["scalarValue"], Is.EqualTo("0")); + } + + [Test] + public void ValueFailsClosedWhenActionDoesNotAdvertiseValueContract() + { + var action = EveUnityAdvertisedInputAction.Resolve(new EveInputCapabilityDocument + { + Actions = new[] + { + new EveInputActionDocument + { + ActionId = "pilot.dock", + Operation = "aetheria.daemon.commands.DockNearest", + Availability = "available" + } + } + }, "pilot.dock"); + + Assert.Throws(() => action.BuildPayload("entity.4", 1f)); + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAdvertisedInputActionTests.cs.meta b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAdvertisedInputActionTests.cs.meta new file mode 100644 index 0000000..5f325ab --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAdvertisedInputActionTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a60138c42bf44c2ba38309b21a886c93 diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 6997c1c..4882abb 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,13 +1,13 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.30", + "version": "0.3.31", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { "org.gamecult.cultlib": "1.0.13", "org.gamecult.eve.plugin-fields": "0.1.1", - "org.gamecult.eve.surface": "0.2.1", + "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" }, From 72c9ed709d32d08a81c15b6e0355867acb4cdf28 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 11:27:20 +0100 Subject: [PATCH 071/292] Record released held-input witness --- .../PlayMode/GenericWorldCaptureTests.cs | 37 ++++++++++++++++++- ReleaseConsumerProject/Packages/manifest.json | 4 +- .../Packages/packages-lock.json | 10 ++--- docs/aetheria-released-package-witness.md | 33 ++++++++++------- scripts/run-aetheria-daemon-world-witness.ps1 | 6 +-- 5 files changed, 64 insertions(+), 26 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 050ab02..ce53c38 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -192,6 +192,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests WitnessReceipt lookReceipt = null; WitnessReceipt focusReceipt = null; WitnessReceipt tractorReceipt = null; + WitnessReceipt tractorReleaseReceipt = null; WitnessReceipt actionReceipt = null; EveUnityShotReceipt observedShot = null; EveUnityCombatPresentation observedCombat = null; @@ -202,6 +203,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float aimDotDistance = 0f; int cockpitProgressCount = 0; float tractorBeamPower = 0f; + float tractorReleasedPower = 1f; int tractorBeamParticleSystemCount = 0; try @@ -385,7 +387,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(beamPresentation.ActivationActionId, Is.Not.Empty, "The beam presentation does not name an advertised generic action."); var tractorVersion = runtime.ActiveVersion; - var tractor = host.SubmitAdvertisedActionIntent(playerId, beamPresentation.ActivationActionId); + var tractor = host.SubmitAdvertisedActionValueIntent(playerId, beamPresentation.ActivationActionId, 1f); var tractorDeadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < tractorDeadline && (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != tractor.CommandId || @@ -600,6 +602,29 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(mapCapturePath))); File.WriteAllBytes(mapCapturePath, pixels.EncodeToPNG()); Assert.That(new FileInfo(mapCapturePath).Length, Is.GreaterThan(1024)); + var tractorReleaseVersion = runtime.ActiveVersion; + var tractorRelease = host.SubmitAdvertisedActionValueIntent( + playerId, + beamPresentation.ActivationActionId, + 0f); + var tractorReleaseDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < tractorReleaseDeadline && + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != tractorRelease.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || + runtime.ActiveVersion < runtime.LastReceipt.SourceVersion || + EveUnityBeamPresentation.FindAll(runtime.ActiveProjection).Single().Power > + beamPresentation.ActivationThreshold)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + AssertReconciledProviderReceipt(runtime.LastReceipt, tractorRelease.CommandId, + provider.Selection.ProviderId, provider.Selection.SurfaceId, tractorReleaseVersion); + tractorReleaseReceipt = WitnessReceipt.From("beam-release", runtime.LastReceipt); + beamRenderer.RefreshNow(); + Assert.That(beamRenderer.TryGetPower(beamPresentation.Id, out tractorReleasedPower), Is.True); + Assert.That(tractorReleasedPower, Is.Zero, + "Released held input did not return the daemon-authored tractor presentation to zero."); WriteWitnessFacts( initialVersion, runtime.ActiveVersion, @@ -615,6 +640,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests lookReceipt, focusReceipt, tractorReceipt, + tractorReleaseReceipt, actionReceipt, observedShot, shieldRatioBefore, @@ -631,6 +657,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pilotBrightPixelCount, cockpitProgressCount, tractorBeamPower, + tractorReleasedPower, tractorBeamParticleSystemCount, playerRendererFacts); } @@ -665,6 +692,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests WitnessReceipt look, WitnessReceipt targeting, WitnessReceipt tractor, + WitnessReceipt tractorRelease, WitnessReceipt action, EveUnityShotReceipt shot, float shieldRatioBefore, @@ -681,6 +709,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests int pilotBrightPixelCount, int cockpitProgressCount, float tractorBeamPower, + float tractorReleasedPower, int tractorBeamParticleSystemCount, RendererFact[] playerRendererFacts) { @@ -695,6 +724,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests aimPresentation = look != null && aimDotDistance >= 49.9f, targeting = targeting != null, beamPresentation = tractor != null && tractorBeamPower > 0.01f && tractorBeamParticleSystemCount > 0, + beamRelease = tractorRelease != null && tractorReleasedPower == 0f, action = action != null, combatPresentation = shot != null, shotId = shot?.ShotId ?? "", @@ -721,13 +751,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pilotBrightPixelCount = pilotBrightPixelCount, cockpitProgressCount = cockpitProgressCount, tractorBeamPower = tractorBeamPower, + tractorReleasedPower = tractorReleasedPower, tractorBeamParticleSystemCount = tractorBeamParticleSystemCount, playerRenderers = playerRendererFacts, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, mapCameraIncludesMapChannel = mapCameraIncludesMapChannel, - receipts = new[] { movement, look, targeting, tractor, action } + receipts = new[] { movement, look, targeting, tractor, tractorRelease, action } }; Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); File.WriteAllText(path, JsonUtility.ToJson(document, true)); @@ -743,6 +774,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public bool aimPresentation; public bool targeting; public bool beamPresentation; + public bool beamRelease; public bool action; public bool combatPresentation; public string shotId; @@ -769,6 +801,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public int pilotBrightPixelCount; public int cockpitProgressCount; public float tractorBeamPower; + public float tractorReleasedPower; public int tractorBeamParticleSystemCount; public RendererFact[] playerRenderers; public int mapChannelRendererCount; diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 1dfc17c..df32b8b 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -8,8 +8,8 @@ "com.unity.render-pipelines.universal": "17.4.0", "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", - "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.30", + "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.31", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 269434a..a4b2b92 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -133,26 +133,26 @@ "hash": "0b099274a4e2b4c301eced4322166837792f585a" }, "org.gamecult.eve.surface": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.1", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", "depth": 0, "source": "git", "dependencies": { "org.gamecult.cultlib": "1.0.8" }, - "hash": "d0aa50d31f3b6da96bffc194fdc3aa358f37e332" + "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.30", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.31", "depth": 0, "source": "git", "dependencies": { "org.gamecult.cultlib": "1.0.13", "org.gamecult.eve.plugin-fields": "0.1.1", - "org.gamecult.eve.surface": "0.2.1", + "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "f979cd425a9f8f951736c56051e8368b6d6ac2f3" + "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 0bb254b..6c8be3a 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,8 +4,9 @@ The warm-cache released-package witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.30`, commit - `f979cd425a9f8f951736c56051e8368b6d6ac2f3`; +- `org.gamecult.eve.unity-scene` `0.3.31`, commit + `140e1bd963a0033e66777a3b2c5fe6e9c97dfe32`; +- `org.gamecult.eve.surface` `0.2.2`, from the same commit; - `org.gamecult.eve.unity-uitoolkit` `0.1.1`, commit `4d0cbe0185bdc4fc65eb63503a7c5cb578539669`; - `org.gamecult.cultlib` `1.0.13`, commit @@ -15,7 +16,7 @@ The warm-cache released-package witness passes with: Evidence is in `artifacts/aetheria-daemon-native-0326-warm`. The PlayMode test passed and recorded provider-owned reconciled movement, look, targeting, -tractor, and action receipts. The daemon-owned look direction reached the SoA body rotation, +tractor press, tractor release, and action receipts. The daemon-owned look direction reached the SoA body rotation, and the generic aim presentation rendered at the advertised 50-unit minimum convergence distance. The advertised skybox material and reflection cubemap resolved from the provider bundle as their required native types and became the @@ -44,13 +45,14 @@ daemon lock progress `1.0`; Unity does not manufacture or smooth that value. Camera-channel facts: - provider-authored player renderers: `14`, including the lowered tractor effect; -- pilot changed pixels: `230,255`; -- pilot average luminance: `0.004634`; -- pilot bright pixels: `1,542`; -- map-channel renderers: `11`; -- map changed pixels: `8,510`; +- pilot changed pixels: `230,173`; +- pilot average luminance: `0.002677`; +- pilot bright pixels: `1,024`; +- map-channel renderers: `10`; +- map changed pixels: `4,626`; - native cockpit progress bars: `7`; -- daemon tractor power at capture: `0.24`; +- daemon tractor power at capture: `0.16`; +- daemon tractor power after the advertised release: `0`; - provider tractor particle systems: `1`; - the pilot camera excludes the advertised map layer; - the player prefab's embedded layer-14 map icon contributes exactly `0` pilot @@ -69,15 +71,18 @@ Aetheria currently bundles ambientCG's 1K `Metal012` color, normal, and metalness maps under CC0. The same pre-generated maps now replace dead Substance archive sub-assets on the brushed aluminium, tinted car paint, cockpit, steel, black-metal, and radiator materials. The released witness -resolves textured native URP materials in `9` distinct player-material slots, +resolves textured native URP materials in `10` distinct player-material slots, up from `3`, without adding another texture payload. This is temporary provider art with recorded provenance, not a replacement texture pipeline. The tractor proof is structural, not yet a native-look victory lap. The daemon -publishes `beam.presentation`, the generic client routes `pilot.scoop` through -the advertised `SetTractorPower` operation and payload, and the provider-owned -prefab is attached to the SoA-presented ship with nonzero daemon power. Its -particle renderer contributes only one measured pilot pixel in this capture. +publishes `beam.presentation` and advertises `pilot.scoop` as a +`button-hold.v1` value action. The generic client routes scalar `1` on press and +scalar `0` on release through the advertised `SetTractorPower` operation. Both +commands receive provider-owned reconciled receipts, and the daemon ramps the +published power from `0.16` at capture to exactly `0` after release. The +provider-owned prefab is attached to the SoA-presented ship, but its particle +renderer contributes zero measured pilot pixels in this capture. Effect tuning remains provider art work; the runtime must not compensate with an Aetheria-specific beam. diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 3069618..b4890ec 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -1,9 +1,9 @@ param( [string] $UnityExe = "C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Unity.exe", [string] $AetheriaRoot = "E:\Projects\Aetheria", - [string] $CultLibRoot = "E:\Projects\CultLib", + [string] $CultLibRoot = "E:\Projects\CultLib-codex-cultmesh-reliability", [string] $EveUnityRoot = "E:\Projects\EveUnity", - [string] $YmirRoot = "E:\Projects\Ymir", + [string] $YmirRoot = "E:\Projects\Ymir-aetheria-integration", [string] $ClientProject = "ReleaseConsumerProject", [int] $Port = 3076, [string] $OutputDirectory = "artifacts\aetheria-daemon", @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "f979cd425a9f8f951736c56051e8368b6d6ac2f3" +$expectedEveUnityCommit = "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot From 9a650242517ff39d4de7886adab8aa608d0b1fe2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 11:38:52 +0100 Subject: [PATCH 072/292] Capture full-power provider tractor effect --- .../PlayMode/GenericWorldCaptureTests.cs | 6 ++--- .../PlayMode/GenericWorldCaptureTests.cs | 6 ++--- docs/aetheria-released-package-witness.md | 27 ++++++++++--------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index ce53c38..ab1981f 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -393,8 +393,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != tractor.CommandId || !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || runtime.ActiveVersion < runtime.LastReceipt.SourceVersion || - EveUnityBeamPresentation.FindAll(runtime.ActiveProjection).Single().Power <= - beamPresentation.ActivationThreshold)) + EveUnityBeamPresentation.FindAll(runtime.ActiveProjection).Single().Power < 0.99f)) { yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); @@ -414,7 +413,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests beamRenderer.RefreshNow(); Assert.That(beamRenderer.ActiveBeamCount, Is.EqualTo(1)); Assert.That(beamRenderer.TryGetPower(beamPresentation.Id, out tractorBeamPower), Is.True); - Assert.That(tractorBeamPower, Is.GreaterThan(beamPresentation.ActivationThreshold)); + Assert.That(tractorBeamPower, Is.GreaterThanOrEqualTo(0.99f), + "Held input did not reach full daemon-authored tractor power before capture."); tractorBeamParticleSystemCount = beamRenderer.ActiveParticleSystemCount; Assert.That(tractorBeamParticleSystemCount, Is.GreaterThan(0), "The generic beam lowerer did not instantiate the provider-owned effect prefab."); diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index ce53c38..ab1981f 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -393,8 +393,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != tractor.CommandId || !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || runtime.ActiveVersion < runtime.LastReceipt.SourceVersion || - EveUnityBeamPresentation.FindAll(runtime.ActiveProjection).Single().Power <= - beamPresentation.ActivationThreshold)) + EveUnityBeamPresentation.FindAll(runtime.ActiveProjection).Single().Power < 0.99f)) { yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); @@ -414,7 +413,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests beamRenderer.RefreshNow(); Assert.That(beamRenderer.ActiveBeamCount, Is.EqualTo(1)); Assert.That(beamRenderer.TryGetPower(beamPresentation.Id, out tractorBeamPower), Is.True); - Assert.That(tractorBeamPower, Is.GreaterThan(beamPresentation.ActivationThreshold)); + Assert.That(tractorBeamPower, Is.GreaterThanOrEqualTo(0.99f), + "Held input did not reach full daemon-authored tractor power before capture."); tractorBeamParticleSystemCount = beamRenderer.ActiveParticleSystemCount; Assert.That(tractorBeamParticleSystemCount, Is.GreaterThan(0), "The generic beam lowerer did not instantiate the provider-owned effect prefab."); diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 6c8be3a..ec3f4f0 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -44,14 +44,15 @@ daemon lock progress `1.0`; Unity does not manufacture or smooth that value. Camera-channel facts: -- provider-authored player renderers: `14`, including the lowered tractor effect; -- pilot changed pixels: `230,173`; -- pilot average luminance: `0.002677`; -- pilot bright pixels: `1,024`; -- map-channel renderers: `10`; -- map changed pixels: `4,626`; +- provider-authored player renderers: `13`, including the lowered tractor effect; +- pilot changed pixels: `230,105`; +- pilot average luminance: `0.004590`; +- pilot bright pixels: `1,620`; +- map-channel renderers: `11`; +- map changed pixels: `5,187`; - native cockpit progress bars: `7`; -- daemon tractor power at capture: `0.16`; +- daemon tractor power at capture: `1.0` after the held input completed its + authored ramp; - daemon tractor power after the advertised release: `0`; - provider tractor particle systems: `1`; - the pilot camera excludes the advertised map layer; @@ -80,9 +81,11 @@ publishes `beam.presentation` and advertises `pilot.scoop` as a `button-hold.v1` value action. The generic client routes scalar `1` on press and scalar `0` on release through the advertised `SetTractorPower` operation. Both commands receive provider-owned reconciled receipts, and the daemon ramps the -published power from `0.16` at capture to exactly `0` after release. The -provider-owned prefab is attached to the SoA-presented ship, but its particle -renderer contributes zero measured pilot pixels in this capture. +published power from `1.0` at capture to exactly `0` after release. The provider +build removes the fossil's embedded tractor object from player/ship prefabs, so +the standalone `beam.presentation` prefab is the only tractor renderer. Its +cyan/yellow dotted band is visible in the pilot capture and absent from the map +capture. Effect tuning remains provider art work; the runtime must not compensate with an Aetheria-specific beam. @@ -96,8 +99,8 @@ Primary artifacts: ## Cold delivery is not currently proven -The current `46,133,487` byte Unity bundle (SHA-256 -`ff07aa7f71aa2da0e8e6fe2f4ed68900931d6e67c153b586777467316d4fb80f`) +The current `46,131,569` byte Unity bundle (SHA-256 +`a4b26fb234ae7d82eda26fb342d7f77a236e3bc1107420175eb1c626785db212`) times out from an empty cache. Its bytes are still transported through batched snapshot records rather than the intended mapped/network body transport. Increasing the Unity timeout would hide the From 817912a041ac1911e1e27a4b88617754d90ad4b7 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 11:45:50 +0100 Subject: [PATCH 073/292] Expose provider cargo deltas in feedback --- .../PlayMode/GenericWorldCaptureTests.cs | 60 +++++++++++++++++++ .../Runtime/EveUnityFeedbackPresenter.cs | 9 ++- .../Editor/EveUnityFeedbackPresenterTests.cs | 41 ++++++++++++- .../org.gamecult.eve.unity-scene/package.json | 2 +- 4 files changed, 108 insertions(+), 4 deletions(-) diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index ab1981f..e9b71e2 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -205,6 +205,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float tractorBeamPower = 0f; float tractorReleasedPower = 1f; int tractorBeamParticleSystemCount = 0; + EveUnityFeedbackEvent pickupCollection = null; + int pickupCollectionEventCount = 0; + int initialPickupEntityCount = 0; + int finalPickupEntityCount = -1; try { @@ -267,6 +271,16 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } Assert.That(host.PresentedEntities?.CurrentGeneration, Is.Not.Null); Assert.That(host.PresentedEntities.CurrentGeneration.Entities.Count, Is.GreaterThan(0)); + initialPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity => + string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); + Assert.That(initialPickupEntityCount, Is.GreaterThan(0), + "The daemon fixture did not publish its physical starter pickup through the entity SoA."); + runtime.FeedbackAvailable += feedback => + { + if (!string.Equals(feedback.Kind, "pickup.collected", StringComparison.Ordinal)) return; + pickupCollectionEventCount++; + pickupCollection = feedback; + }; var playerId = runtime.ActiveWorld.PlayerEntityId; var playerFact = host.PresentedEntities.CurrentGeneration.Entities @@ -418,6 +432,27 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamParticleSystemCount = beamRenderer.ActiveParticleSystemCount; Assert.That(tractorBeamParticleSystemCount, Is.GreaterThan(0), "The generic beam lowerer did not instantiate the provider-owned effect prefab."); + var pickupDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < pickupDeadline && + (pickupCollection == null || + host.PresentedEntities.CurrentGeneration.Entities.Any(entity => + string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)))) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + finalPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity => + string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); + Assert.That(pickupCollection, Is.Not.Null, + "The released client did not observe provider-owned feedback for Ymir contact collection."); + Assert.That(pickupCollectionEventCount, Is.EqualTo(1), + "One Ymir contact transaction must emit one client-visible collection event."); + Assert.That(pickupCollection.ItemKey, Is.EqualTo("scrap-metal")); + Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1)); + Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero); + Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1)); + Assert.That(finalPickupEntityCount, Is.EqualTo(initialPickupEntityCount - 1), + "Collected pickup remained in the provider-authored entity generation."); var focusVersion = runtime.ActiveVersion; var inputDriver = root.GetComponent() ?? @@ -659,6 +694,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamPower, tractorReleasedPower, tractorBeamParticleSystemCount, + initialPickupEntityCount, + finalPickupEntityCount, + pickupCollectionEventCount, + pickupCollection, playerRendererFacts); } finally @@ -711,6 +750,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float tractorBeamPower, float tractorReleasedPower, int tractorBeamParticleSystemCount, + int initialPickupEntityCount, + int finalPickupEntityCount, + int pickupCollectionEventCount, + EveUnityFeedbackEvent pickupCollection, RendererFact[] playerRendererFacts) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); @@ -753,6 +796,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamPower = tractorBeamPower, tractorReleasedPower = tractorReleasedPower, tractorBeamParticleSystemCount = tractorBeamParticleSystemCount, + pickupCollection = pickupCollection != null && pickupCollectionEventCount == 1 && + pickupCollection.CargoQuantityAfter - pickupCollection.CargoQuantityBefore == pickupCollection.ScalarValue, + initialPickupEntityCount = initialPickupEntityCount, + finalPickupEntityCount = finalPickupEntityCount, + pickupCollectionEventCount = pickupCollectionEventCount, + pickupItemKey = pickupCollection?.ItemKey ?? "", + pickupQuantity = (float)(pickupCollection?.ScalarValue ?? 0), + cargoQuantityBeforePickup = (float)(pickupCollection?.CargoQuantityBefore ?? 0), + cargoQuantityAfterPickup = (float)(pickupCollection?.CargoQuantityAfter ?? 0), playerRenderers = playerRendererFacts, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, @@ -803,6 +855,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float tractorBeamPower; public float tractorReleasedPower; public int tractorBeamParticleSystemCount; + public bool pickupCollection; + public int initialPickupEntityCount; + public int finalPickupEntityCount; + public int pickupCollectionEventCount; + public string pickupItemKey; + public float pickupQuantity; + public float cargoQuantityBeforePickup; + public float cargoQuantityAfterPickup; public RendererFact[] playerRenderers; public int mapChannelRendererCount; public int mapChangedPixels; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityFeedbackPresenter.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityFeedbackPresenter.cs index 1f189fe..723671f 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityFeedbackPresenter.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityFeedbackPresenter.cs @@ -11,10 +11,13 @@ namespace GameCult.Eve.UnityScene public EveUnityFeedbackEvent(EveUnitySceneNode node) { EventId = Get(node, "eventId"); Kind = Get(node, "eventKind"); SubjectKey = Get(node, "subjectKey"); - ItemKey = Get(node, "itemKey"); Position = Get(node, "position"); + ItemKey = Get(node, "itemKey"); Position = Get(node, "position"); Reason = Get(node, "reason"); FrameId = Long(node, "frameId", -1); ZoneIndex = Int(node, "zoneIndex", -1); SourceEntityIndex = Int(node, "sourceEntityIndex", -1); TargetEntityIndex = Int(node, "targetEntityIndex", -1); PickupIndex = Int(node, "pickupIndex", -1); ScalarValue = Double(node, "scalarValue", 0); + AuxiliaryValue = Double(node, "auxiliaryValue", 0); + CargoQuantityBefore = Double(node, "cargoQuantityBefore", 0); + CargoQuantityAfter = Double(node, "cargoQuantityAfter", 0); } public string EventId { get; } public string Kind { get; } @@ -26,7 +29,11 @@ namespace GameCult.Eve.UnityScene public string SubjectKey { get; } public string ItemKey { get; } public string Position { get; } + public string Reason { get; } public double ScalarValue { get; } + public double AuxiliaryValue { get; } + public double CargoQuantityBefore { get; } + public double CargoQuantityAfter { get; } private static string Get(EveUnitySceneNode node, string key) => node.Props.TryGetValue(key, out var value) ? value ?? "" : ""; private static int Int(EveUnitySceneNode node, string key, int fallback) => int.TryParse(Get(node, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : fallback; private static long Long(EveUnitySceneNode node, string key, long fallback) => long.TryParse(Get(node, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : fallback; diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityFeedbackPresenterTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityFeedbackPresenterTests.cs index e4bd612..8dc0d35 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityFeedbackPresenterTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityFeedbackPresenterTests.cs @@ -29,14 +29,51 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(emitted.Count, Is.EqualTo(1)); } + [Test] + public void FeedbackEventPreservesProviderOwnedCargoDeltaAndReason() + { + var node = Event("pickup", "pickup.collected", 9, new Dictionary + { + ["itemKey"] = "scrap-metal", + ["scalarValue"] = "1", + ["auxiliaryValue"] = "0", + ["cargoQuantityBefore"] = "0", + ["cargoQuantityAfter"] = "1", + ["reason"] = "" + }); + + var value = new EveUnityFeedbackEvent(node); + + Assert.That(value.ItemKey, Is.EqualTo("scrap-metal")); + Assert.That(value.ScalarValue, Is.EqualTo(1)); + Assert.That(value.AuxiliaryValue, Is.Zero); + Assert.That(value.CargoQuantityBefore, Is.Zero); + Assert.That(value.CargoQuantityAfter, Is.EqualTo(1)); + Assert.That(value.Reason, Is.Empty); + } + private static EveUnitySceneProjection Projection(params EveUnitySceneNode[] events) => new EveUnitySceneProjection("provider", "surface", "world", "commands", "receipt", "provider", null, Node("root", "surface", Node("feedback", "feedback.stream", events))); - private static EveUnitySceneNode Event(string id, string kind, long frame) => + private static EveUnitySceneNode Event( + string id, + string kind, + long frame, + IReadOnlyDictionary? extra = null) + { + var props = new Dictionary(StringComparer.Ordinal) + { + ["eventId"] = id, + ["eventKind"] = kind, + ["frameId"] = frame.ToString() + }; + foreach (var pair in extra ?? Empty()) props[pair.Key] = pair.Value; + return new EveUnitySceneNode(id, "feedback.event", "scene-node", - new Dictionary(StringComparer.Ordinal) { ["eventId"] = id, ["eventKind"] = kind, ["frameId"] = frame.ToString() }, + props, Empty(), Empty(), 0, 0, Array.Empty(), null, Array.Empty()); + } private static EveUnitySceneNode Node(string id, string kind, params EveUnitySceneNode[] children) => new EveUnitySceneNode(id, kind, "scene-node", Empty(), Empty(), Empty(), 0, 0, diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 4882abb..4bfe911 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.31", + "version": "0.3.32", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From fbe326f21194536bbaa597644c17c841c7b13a5c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 16:36:32 +0100 Subject: [PATCH 074/292] Prove released-client cargo collection --- .../PlayMode/GenericWorldCaptureTests.cs | 68 ++++++++++++++++++- ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 +- .../PlayMode/GenericWorldCaptureTests.cs | 8 ++- docs/aetheria-released-package-witness.md | 27 ++++++-- scripts/run-aetheria-daemon-world-witness.ps1 | 56 +++++++++------ 6 files changed, 133 insertions(+), 32 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index ab1981f..27875c4 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -205,9 +205,16 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float tractorBeamPower = 0f; float tractorReleasedPower = 1f; int tractorBeamParticleSystemCount = 0; + EveUnityFeedbackEvent pickupCollection = null; + int pickupCollectionEventCount = 0; + int initialPickupEntityCount = 0; + int finalPickupEntityCount = -1; try { + var providerReadyPath = Environment.GetEnvironmentVariable("EVEUNITY_PROVIDER_READY_PATH"); + if (!string.IsNullOrWhiteSpace(providerReadyPath)) + File.WriteAllText(providerReadyPath, DateTimeOffset.UtcNow.ToString("O")); provider = root.AddComponent(); provider.Configure( rendezvousEndpoint, @@ -216,7 +223,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests surfaceId, requiredSurfaceKind: "interactive-world", clientRuntimeId: $"eve-unity-test-{Guid.NewGuid():N}"); - var publicationDeadline = Time.realtimeSinceStartup + 10f; + var publicationDeadline = Time.realtimeSinceStartup + 30f; while (true) { var published = false; @@ -228,6 +235,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests catch (InvalidOperationException) when (Time.realtimeSinceStartup < publicationDeadline) { } + catch (TimeoutException) when (Time.realtimeSinceStartup < publicationDeadline) + { + } if (published) break; yield return new WaitForSecondsRealtime(0.1f); } @@ -267,6 +277,16 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } Assert.That(host.PresentedEntities?.CurrentGeneration, Is.Not.Null); Assert.That(host.PresentedEntities.CurrentGeneration.Entities.Count, Is.GreaterThan(0)); + initialPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity => + string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); + Assert.That(initialPickupEntityCount, Is.GreaterThan(0), + "The daemon fixture did not publish its physical starter pickup through the entity SoA."); + runtime.FeedbackAvailable += feedback => + { + if (!string.Equals(feedback.Kind, "pickup.collected", StringComparison.Ordinal)) return; + pickupCollectionEventCount++; + pickupCollection = feedback; + }; var playerId = runtime.ActiveWorld.PlayerEntityId; var playerFact = host.PresentedEntities.CurrentGeneration.Entities @@ -418,6 +438,27 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamParticleSystemCount = beamRenderer.ActiveParticleSystemCount; Assert.That(tractorBeamParticleSystemCount, Is.GreaterThan(0), "The generic beam lowerer did not instantiate the provider-owned effect prefab."); + var pickupDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < pickupDeadline && + (pickupCollection == null || + host.PresentedEntities.CurrentGeneration.Entities.Any(entity => + string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)))) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + finalPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity => + string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); + Assert.That(pickupCollection, Is.Not.Null, + "The released client did not observe provider-owned feedback for Ymir contact collection."); + Assert.That(pickupCollectionEventCount, Is.EqualTo(1), + "One Ymir contact transaction must emit one client-visible collection event."); + Assert.That(pickupCollection.ItemKey, Is.EqualTo("scrap-metal")); + Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1)); + Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero); + Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1)); + Assert.That(finalPickupEntityCount, Is.EqualTo(initialPickupEntityCount - 1), + "Collected pickup remained in the provider-authored entity generation."); var focusVersion = runtime.ActiveVersion; var inputDriver = root.GetComponent() ?? @@ -659,6 +700,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamPower, tractorReleasedPower, tractorBeamParticleSystemCount, + initialPickupEntityCount, + finalPickupEntityCount, + pickupCollectionEventCount, + pickupCollection, playerRendererFacts); } finally @@ -711,6 +756,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float tractorBeamPower, float tractorReleasedPower, int tractorBeamParticleSystemCount, + int initialPickupEntityCount, + int finalPickupEntityCount, + int pickupCollectionEventCount, + EveUnityFeedbackEvent pickupCollection, RendererFact[] playerRendererFacts) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); @@ -753,6 +802,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamPower = tractorBeamPower, tractorReleasedPower = tractorReleasedPower, tractorBeamParticleSystemCount = tractorBeamParticleSystemCount, + pickupCollection = pickupCollection != null && pickupCollectionEventCount == 1 && + pickupCollection.CargoQuantityAfter - pickupCollection.CargoQuantityBefore == pickupCollection.ScalarValue, + initialPickupEntityCount = initialPickupEntityCount, + finalPickupEntityCount = finalPickupEntityCount, + pickupCollectionEventCount = pickupCollectionEventCount, + pickupItemKey = pickupCollection?.ItemKey ?? "", + pickupQuantity = (float)(pickupCollection?.ScalarValue ?? 0), + cargoQuantityBeforePickup = (float)(pickupCollection?.CargoQuantityBefore ?? 0), + cargoQuantityAfterPickup = (float)(pickupCollection?.CargoQuantityAfter ?? 0), playerRenderers = playerRendererFacts, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, @@ -803,6 +861,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float tractorBeamPower; public float tractorReleasedPower; public int tractorBeamParticleSystemCount; + public bool pickupCollection; + public int initialPickupEntityCount; + public int finalPickupEntityCount; + public int pickupCollectionEventCount; + public string pickupItemKey; + public float pickupQuantity; + public float cargoQuantityBeforePickup; + public float cargoQuantityAfterPickup; public RendererFact[] playerRenderers; public int mapChannelRendererCount; public int mapChangedPixels; diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index df32b8b..0eca353 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.31", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.32", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index a4b2b92..fbe0822 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -142,7 +142,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.31", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.32", "depth": 0, "source": "git", "dependencies": { @@ -152,7 +152,7 @@ "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" + "hash": "817912a041ac1911e1e27a4b88617754d90ad4b7" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index e9b71e2..27875c4 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -212,6 +212,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests try { + var providerReadyPath = Environment.GetEnvironmentVariable("EVEUNITY_PROVIDER_READY_PATH"); + if (!string.IsNullOrWhiteSpace(providerReadyPath)) + File.WriteAllText(providerReadyPath, DateTimeOffset.UtcNow.ToString("O")); provider = root.AddComponent(); provider.Configure( rendezvousEndpoint, @@ -220,7 +223,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests surfaceId, requiredSurfaceKind: "interactive-world", clientRuntimeId: $"eve-unity-test-{Guid.NewGuid():N}"); - var publicationDeadline = Time.realtimeSinceStartup + 10f; + var publicationDeadline = Time.realtimeSinceStartup + 30f; while (true) { var published = false; @@ -232,6 +235,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests catch (InvalidOperationException) when (Time.realtimeSinceStartup < publicationDeadline) { } + catch (TimeoutException) when (Time.realtimeSinceStartup < publicationDeadline) + { + } if (published) break; yield return new WaitForSecondsRealtime(0.1f); } diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index ec3f4f0..475d987 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,8 +4,8 @@ The warm-cache released-package witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.31`, commit - `140e1bd963a0033e66777a3b2c5fe6e9c97dfe32`; +- `org.gamecult.eve.unity-scene` `0.3.32`, commit + `817912a041ac1911e1e27a4b88617754d90ad4b7`; - `org.gamecult.eve.surface` `0.2.2`, from the same commit; - `org.gamecult.eve.unity-uitoolkit` `0.1.1`, commit `4d0cbe0185bdc4fc65eb63503a7c5cb578539669`; @@ -16,7 +16,8 @@ The warm-cache released-package witness passes with: Evidence is in `artifacts/aetheria-daemon-native-0326-warm`. The PlayMode test passed and recorded provider-owned reconciled movement, look, targeting, -tractor press, tractor release, and action receipts. The daemon-owned look direction reached the SoA body rotation, +tractor press, tractor release, contact-gated cargo collection, and action +receipts. The daemon-owned look direction reached the SoA body rotation, and the generic aim presentation rendered at the advertised 50-unit minimum convergence distance. The advertised skybox material and reflection cubemap resolved from the provider bundle as their required native types and became the @@ -45,16 +46,20 @@ daemon lock progress `1.0`; Unity does not manufacture or smooth that value. Camera-channel facts: - provider-authored player renderers: `13`, including the lowered tractor effect; -- pilot changed pixels: `230,105`; -- pilot average luminance: `0.004590`; -- pilot bright pixels: `1,620`; +- pilot changed pixels: `230,302`; +- pilot average luminance: `0.002843`; +- pilot bright pixels: `1,040`; - map-channel renderers: `11`; -- map changed pixels: `5,187`; +- map changed pixels: `3,921`; - native cockpit progress bars: `7`; - daemon tractor power at capture: `1.0` after the held input completed its authored ramp; - daemon tractor power after the advertised release: `0`; - provider tractor particle systems: `1`; +- pickup entities before/after collection: `1` / `0`; +- provider `pickup.collected` events: exactly `1`; +- collected item and quantity: `scrap-metal`, `1`; +- authoritative cargo quantity before/after: `0` / `1`; - the pilot camera excludes the advertised map layer; - the player prefab's embedded layer-14 map icon contributes exactly `0` pilot pixels; @@ -86,6 +91,14 @@ build removes the fossil's embedded tractor object from player/ship prefabs, so the standalone `beam.presentation` prefab is the only tractor renderer. Its cyan/yellow dotted band is visible in the pilot capture and absent from the map capture. +The same released run begins with one provider-owned pickup in the entity SoA. +Held tractor input produces a Ymir Begin contact fact; the daemon consumes that +fact once, removes the pickup once, changes cargo from `0` to `1`, and emits one +`pickup.collected` feedback event carrying the exact delta. Aetheria's Ymir body +mapping excludes stations and non-ship world bodies from pickup collision facts, +so ambient station contact cannot become a cargo writer. Capacity rejection and +its `cargo-capacity` reason are covered by the daemon smoke, not claimed as a +released-client live scenario here. Effect tuning remains provider art work; the runtime must not compensate with an Aetheria-specific beam. diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index b4890ec..b412222 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" +$expectedEveUnityCommit = "817912a041ac1911e1e27a4b88617754d90ad4b7" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot @@ -26,6 +26,7 @@ $daemonLogPath = Join-Path $outputRoot "aetheria-daemon.log" $capturePath = Join-Path $outputRoot "aetheria-daemon-world.png" $mapCapturePath = Join-Path $outputRoot "aetheria-daemon-map.png" $factsPath = Join-Path $outputRoot "witness-facts.json" +$providerReadyPath = Join-Path $outputRoot "provider-ready.txt" $witnessPath = Join-Path $outputRoot "runtime-witness.json" $replicaPath = Join-Path $outputRoot "eve-unity-replica.cc" $assetCachePath = Join-Path $outputRoot "asset-cache" @@ -44,7 +45,7 @@ if ((Get-FileHash -LiteralPath $canonicalWitnessTest -Algorithm SHA256).Hash -ne } New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null -foreach ($priorArtifact in @($resultsPath, $capturePath, $mapCapturePath, $factsPath, $witnessPath)) { +foreach ($priorArtifact in @($resultsPath, $capturePath, $mapCapturePath, $factsPath, $providerReadyPath, $witnessPath)) { if (Test-Path -LiteralPath $priorArtifact) { Remove-Item -LiteralPath $priorArtifact -Force } } if ($CacheState -eq "cold" -and (Test-Path -LiteralPath $assetCachePath)) { @@ -132,6 +133,37 @@ $daemonArguments = @( ) $env:AETHERIA_TRACE_EVE_SNAPSHOTS = "1" $env:AETHERIA_TRACE_CLIENT_RUDP = "1" +$env:EVEUNITY_RENDEZVOUS_ENDPOINT = "rudp://127.0.0.1:$Port" +$env:EVEUNITY_PROVIDER_ID = "aetheria" +Remove-Item Env:EVEUNITY_SURFACE_ID -ErrorAction SilentlyContinue +$env:EVEUNITY_REPLICA_PATH = $replicaPath +$env:EVEUNITY_AETHERIA_CAPTURE_PATH = $capturePath +$env:EVEUNITY_AETHERIA_MAP_CAPTURE_PATH = $mapCapturePath +$env:EVEUNITY_DISABLE_AUTO_LAUNCHER = "1" +$env:EVEUNITY_ASSET_CACHE_PATH = $assetCachePath +$env:EVEUNITY_WITNESS_FACTS_PATH = $factsPath +$env:EVEUNITY_PROVIDER_READY_PATH = $providerReadyPath +$arguments = @( + "-batchmode", "-projectPath", $projectRoot, + "-runTests", "-testPlatform", "PlayMode", + "-assemblyNames", "GameCult.EveUnity.GenericClient.PlayModeTests", + "-testFilter", "GenericCultMeshClientLowersAndMovesAdvertisedWorld", + "-testResults", $resultsPath, "-logFile", $unityLogPath +) +$unity = Start-Process -FilePath $UnityExe -ArgumentList $arguments -WorkingDirectory $repoRoot -PassThru -WindowStyle Hidden +Write-Host "Unity witness PID: $($unity.Id)" +Write-Host "Unity log: $unityLogPath" +$clientReady = $false +for ($attempt = 0; $attempt -lt 240; $attempt++) { + if ($unity.HasExited) { throw "Unity exited before reaching provider connection. See $unityLogPath" } + if (Test-Path -LiteralPath $providerReadyPath) { $clientReady = $true; break } + Start-Sleep -Milliseconds 500 +} +if (-not $clientReady) { + Stop-Process -Id $unity.Id -Force -ErrorAction SilentlyContinue + throw "Unity did not reach provider connection within 120 seconds. See $unityLogPath" +} + $daemon = Start-Process -FilePath "dotnet" -ArgumentList $daemonArguments -PassThru -WindowStyle Hidden ` -RedirectStandardOutput $daemonLogPath -RedirectStandardError (Join-Path $outputRoot "aetheria-daemon.error.log") Write-Host "Aetheria daemon PID: $($daemon.Id)" @@ -148,23 +180,6 @@ try { } if (-not $ready) { throw "Aetheria daemon did not open CultMesh port $Port within 120 seconds. See $daemonLogPath" } - $env:EVEUNITY_RENDEZVOUS_ENDPOINT = "rudp://127.0.0.1:$Port" - $env:EVEUNITY_PROVIDER_ID = "aetheria" - Remove-Item Env:EVEUNITY_SURFACE_ID -ErrorAction SilentlyContinue - $env:EVEUNITY_REPLICA_PATH = $replicaPath - $env:EVEUNITY_AETHERIA_CAPTURE_PATH = $capturePath - $env:EVEUNITY_AETHERIA_MAP_CAPTURE_PATH = $mapCapturePath - $env:EVEUNITY_DISABLE_AUTO_LAUNCHER = "1" - $env:EVEUNITY_ASSET_CACHE_PATH = $assetCachePath - $env:EVEUNITY_WITNESS_FACTS_PATH = $factsPath - $arguments = @( - "-batchmode", "-projectPath", $projectRoot, - "-runTests", "-testPlatform", "PlayMode", - "-assemblyNames", "GameCult.EveUnity.GenericClient.PlayModeTests", - "-testFilter", "GenericCultMeshClientLowersAndMovesAdvertisedWorld", - "-testResults", $resultsPath, "-logFile", $unityLogPath - ) - $unity = Start-Process -FilePath $UnityExe -ArgumentList $arguments -WorkingDirectory $repoRoot -PassThru -WindowStyle Hidden if (-not $unity.WaitForExit(300000)) { Stop-Process -Id $unity.Id -Force -ErrorAction SilentlyContinue throw "Unity witness exceeded 300 seconds. See $unityLogPath" @@ -296,7 +311,8 @@ try { Write-Host "Cache-state witness: $stateWitnessPath" } finally { + if ($null -ne $unity -and -not $unity.HasExited) { Stop-Process -Id $unity.Id -Force -ErrorAction SilentlyContinue } if ($null -ne $daemon -and -not $daemon.HasExited) { Stop-Process -Id $daemon.Id -Force } - Remove-Item Env:EVEUNITY_RENDEZVOUS_ENDPOINT, Env:EVEUNITY_PROVIDER_ENDPOINT, Env:EVEUNITY_PROVIDER_ID, Env:EVEUNITY_SURFACE_ID, Env:EVEUNITY_REPLICA_PATH, Env:EVEUNITY_AETHERIA_CAPTURE_PATH, Env:EVEUNITY_AETHERIA_MAP_CAPTURE_PATH, Env:EVEUNITY_DISABLE_AUTO_LAUNCHER, Env:EVEUNITY_ASSET_CACHE_PATH, Env:EVEUNITY_WITNESS_FACTS_PATH -ErrorAction SilentlyContinue + Remove-Item Env:EVEUNITY_RENDEZVOUS_ENDPOINT, Env:EVEUNITY_PROVIDER_ENDPOINT, Env:EVEUNITY_PROVIDER_ID, Env:EVEUNITY_SURFACE_ID, Env:EVEUNITY_REPLICA_PATH, Env:EVEUNITY_AETHERIA_CAPTURE_PATH, Env:EVEUNITY_AETHERIA_MAP_CAPTURE_PATH, Env:EVEUNITY_DISABLE_AUTO_LAUNCHER, Env:EVEUNITY_ASSET_CACHE_PATH, Env:EVEUNITY_WITNESS_FACTS_PATH, Env:EVEUNITY_PROVIDER_READY_PATH -ErrorAction SilentlyContinue Remove-Item Env:AETHERIA_TRACE_EVE_SNAPSHOTS -ErrorAction SilentlyContinue } From 852458dade1ae535d2ff3cfc9f8fe430e341ce2a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 17:12:11 +0100 Subject: [PATCH 075/292] Lower provider-owned volumetric fields --- .../EveUnityCultMeshLiveProviderTransport.cs | 39 +++ .../EveUnityCultMeshPlayableWorldProvider.cs | 13 + .../GameCult.Eve.UnityScene.CultMesh.asmdef | 1 + .../EveUnityPlayableWorldClientHost.cs | 4 + .../Runtime/EveUnitySceneSurfaceLowerer.cs | 56 +++- .../Fields/EveUnityFieldsDocumentSource.cs | 10 + .../EveUnityFieldsDocumentSource.cs.meta | 11 + .../Fields/EveUnityFieldsVolumeRenderer.cs | 291 ++++++++++++++++++ .../EveUnityFieldsVolumeRenderer.cs.meta | 11 + .../EveFieldsUnitySceneProjectionTests.cs | 18 ++ .../EveUnitySceneSurfaceLowererTests.cs | 20 ++ .../org.gamecult.eve.unity-scene/package.json | 4 +- 12 files changed, 474 insertions(+), 4 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs.meta create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs.meta diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 3c43f78..f90d0ed 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -5,7 +5,9 @@ using System.Linq; using System.Collections.Concurrent; using GameCult.Caching; using GameCult.Caching.MessagePack; +using GameCult.Eve.PluginFields; using GameCult.Eve.Surface; +using GameCult.Eve.UnityScene.Fields; using GameCult.Mesh; using GameCult.Networking; using UnityEngine; @@ -30,6 +32,7 @@ namespace GameCult.Eve.UnityScene typeof(EveCommandReceiptDocument), typeof(EveAssetCatalogDocument), typeof(EveEntitySoaViewDocument), + typeof(EveFieldsSplatsDocument), typeof(CultMeshBodyPublicationDocument), typeof(CultMeshNetworkBodyDocument), typeof(CultMeshCdnArtifactManifest), @@ -60,8 +63,10 @@ namespace GameCult.Eve.UnityScene private CultMeshBodyPublicationResolver? _bodyResolver; private DateTime _nextReceiptPollUtc; private DateTime _nextEntityViewPollUtc; + private DateTime _nextFieldsPollUtc; private long _lastQueuedEntityViewEpoch = -1; private long _lastQueuedEntityViewSequence = -1; + private readonly Dictionary _lastFieldsFrameByRecord = new Dictionary(StringComparer.Ordinal); public EveUnityCultMeshLiveProviderTransport( string replicaPath, @@ -110,6 +115,8 @@ namespace GameCult.Eve.UnityScene public event Action? EntityViewAvailable; + public event Action? FieldsSplatsAvailable; + private static CultMeshBodyPublicationHandle BodyPublicationHandle(EveEntitySoaViewDocument document) { if (document == null) throw new ArgumentNullException(nameof(document)); @@ -174,10 +181,13 @@ namespace GameCult.Eve.UnityScene public void PumpLiveEvents() { PollCurrentEntityView(); + PollCurrentFields(); while (_liveDocuments.TryDequeue(out var document)) { if (document is EveEntitySoaViewDocument entityView) PublishEntityView(entityView); + else if (document is EveFieldsSplatsDocument fields) + FieldsSplatsAvailable?.Invoke(fields); else if (document is EveSurfaceDocument surface) PublishSurface(surface); else if (document is EveCommandReceiptDocument receipt) @@ -186,6 +196,34 @@ namespace GameCult.Eve.UnityScene PollPendingReceipts(); } + private void PollCurrentFields() + { + if (DateTime.UtcNow < _nextFieldsPollUtc) + return; + _nextFieldsPollUtc = DateTime.UtcNow.AddMilliseconds(250); + var world = new EveUnitySceneSurfaceLowerer() + .Lower(CurrentSurfaceDocument.SurfaceDocument, CurrentSurfaceDocument.AdvertisedSurface) + .PlayableWorld; + if (world == null) + return; + foreach (var recordRef in world.FieldVolumes + .Select(field => field.DocumentRef) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Distinct(StringComparer.Ordinal)) + { + var current = _snapshot! + .FetchDocumentsAsync( + recordKeys: new[] { recordRef }, + schemaIds: new[] { EveFieldsSchemas.Splats }) + .GetAwaiter().GetResult().FirstOrDefault(); + if (current == null || + (_lastFieldsFrameByRecord.TryGetValue(recordRef, out var frame) && current.FrameId <= frame)) + continue; + _lastFieldsFrameByRecord[recordRef] = current.FrameId; + _liveDocuments.Enqueue(current); + } + } + private void PollCurrentEntityView() { if (DateTime.UtcNow < _nextEntityViewPollUtc) @@ -289,6 +327,7 @@ namespace GameCult.Eve.UnityScene _replicaShard = null; _pendingCommandIds.Clear(); _publishedReceiptIds.Clear(); + _lastFieldsFrameByRecord.Clear(); } public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs index a2f6ea3..fe07bb7 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs @@ -2,6 +2,8 @@ using System; using System.IO; using System.Collections.Concurrent; using GameCult.Eve.Surface; +using GameCult.Eve.PluginFields; +using GameCult.Eve.UnityScene.Fields; using GameCult.Mesh; using UnityEngine; @@ -17,6 +19,7 @@ namespace GameCult.Eve.UnityScene IEveUnitySceneCommandReceiptSource, IEveUnityProviderRefreshSource, IEveUnityEntitySoaViewDocumentSource, + IEveUnityFieldsSplatsDocumentSource, IEveUnityCameraRenderPolicySource, IEveUnityNativeAssetProvider, IEveUnityInputCapabilitySource @@ -32,9 +35,12 @@ namespace GameCult.Eve.UnityScene private EveUnityCultMeshLiveProviderTransport? _transport; private EveUnitySceneLiveProviderBridge? _bridge; private readonly ConcurrentQueue _pendingEntityViews = new ConcurrentQueue(); + private readonly ConcurrentQueue _pendingFields = new ConcurrentQueue(); public event Action? EntityViewAvailable; + public event Action? FieldsSplatsAvailable; + public EveUnityCultMeshProviderSelection? Selection { get; private set; } public string SinkKind => Bridge.SinkKind; @@ -129,6 +135,7 @@ namespace GameCult.Eve.UnityScene _bridge?.Dispose(); _transport?.Dispose(); while (_pendingEntityViews.TryDequeue(out var pending)) pending.Lease.Dispose(); + while (_pendingFields.TryDequeue(out _)) { } _bridge = null; _transport = null; Selection = null; @@ -137,6 +144,11 @@ namespace GameCult.Eve.UnityScene private void Update() { _transport?.PumpLiveEvents(); + EveFieldsSplatsDocument? latestFields = null; + while (_pendingFields.TryDequeue(out var fields)) + latestFields = fields; + if (latestFields != null) + FieldsSplatsAvailable?.Invoke(latestFields); EntityViewLease? latest = null; while (_pendingEntityViews.TryDequeue(out var next)) { @@ -181,6 +193,7 @@ namespace GameCult.Eve.UnityScene Selection.SurfaceId, runtimeId); _transport.EntityViewAvailable += (view, lease) => _pendingEntityViews.Enqueue(new EntityViewLease(view, lease)); + _transport.FieldsSplatsAvailable += fields => _pendingFields.Enqueue(fields); _bridge = new EveUnitySceneLiveProviderBridge(_transport); } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/GameCult.Eve.UnityScene.CultMesh.asmdef b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/GameCult.Eve.UnityScene.CultMesh.asmdef index 405df3f..72530cb 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/GameCult.Eve.UnityScene.CultMesh.asmdef +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/GameCult.Eve.UnityScene.CultMesh.asmdef @@ -4,6 +4,7 @@ "references": [ "GameCult.Eve.Surface", "GameCult.Eve.UnityScene", + "GameCult.Eve.PluginFields", "UnityEngine.AssetBundleModule" ], "includePlatforms": [], diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs index 4dfdf43..f1e0ee2 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using GameCult.Eve.Surface; +using GameCult.Eve.UnityScene.Fields; using UnityEngine; #nullable enable @@ -111,6 +112,9 @@ namespace GameCult.Eve.UnityScene if (thermalHud == null) thermalHud = gameObject.AddComponent(); if (thermal == null) thermal = gameObject.AddComponent(); thermal.Bind(this, Runtime.GameObjectAssetProvider as IEveUnityNativeAssetProvider); + var fieldVolume = GetComponent(); + if (fieldVolume == null) fieldVolume = gameObject.AddComponent(); + fieldVolume.Bind(this, providerSurfaceDocuments as IEveUnityFieldsSplatsDocumentSource); var presentation = Runtime.Connect(); ConnectionEpoch++; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index 5607764..3482d66 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using GameCult.Eve.PluginFields; using GameCult.Eve.Surface; using GameCult.Mesh; @@ -103,10 +104,13 @@ namespace GameCult.Eve.UnityScene return null; var entities = new List(); + var fieldVolumes = new List(); foreach (var component in Flatten(worldRoot)) { if (string.Equals(component.Kind, "world.entity3d", StringComparison.Ordinal)) entities.Add(BuildPlayableEntity(component)); + else if (string.Equals(component.Kind, "field.volume3d", StringComparison.Ordinal)) + fieldVolumes.Add(BuildFieldVolume(component)); } return new EveUnityPlayableWorldProjection( @@ -145,9 +149,21 @@ namespace GameCult.Eve.UnityScene ParseFloat(worldRoot.GetProp("reflectionIntensity"), 1f), ParseVector3(worldRoot.GetProp("keyLightDirection")), ParseVector3(worldRoot.GetProp("keyLightColor")), - ParseFloat(worldRoot.GetProp("keyLightIntensity"), 0f)); + ParseFloat(worldRoot.GetProp("keyLightIntensity"), 0f), + fieldVolumes); } + private static EveUnityFieldVolumeProjection BuildFieldVolume(EveSurfaceComponent component) => + new EveUnityFieldVolumeProjection( + component.Id, + component.GetProp("documentRef"), + component.GetProp("documentSchema", EveFieldsSchemas.Splats), + component.GetProp("materialAssetRef"), + component.GetProp("renderChannel", "world.transparent"), + component.GetProp("compositeMode", "premultiplied-alpha"), + component.GetProp("quality", "normal"), + component.Props); + private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component) { var position = ParseVector3(component.GetProp("position")); @@ -375,7 +391,8 @@ namespace GameCult.Eve.UnityScene float reflectionIntensity = 1f, (float x, float y, float z) keyLightDirection = default, (float r, float g, float b) keyLightColor = default, - float keyLightIntensity = 0f) + float keyLightIntensity = 0f, + IReadOnlyList? fieldVolumes = null) { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -419,6 +436,7 @@ namespace GameCult.Eve.UnityScene KeyLightColorG = keyLightColor.g; KeyLightColorB = keyLightColor.b; KeyLightIntensity = keyLightIntensity; + FieldVolumes = fieldVolumes ?? Array.Empty(); } public string WorldRootId { get; } @@ -506,6 +524,40 @@ namespace GameCult.Eve.UnityScene public int EntityCount => Entities.Count; public IReadOnlyList Entities { get; } + + public IReadOnlyList FieldVolumes { get; } + } + + public sealed class EveUnityFieldVolumeProjection + { + public EveUnityFieldVolumeProjection( + string nodeId, + string documentRef, + string documentSchema, + string materialAssetRef, + string renderChannel, + string compositeMode, + string quality, + IReadOnlyDictionary? props = null) + { + NodeId = nodeId ?? ""; + DocumentRef = documentRef ?? ""; + DocumentSchema = documentSchema ?? ""; + MaterialAssetRef = materialAssetRef ?? ""; + RenderChannel = renderChannel ?? ""; + CompositeMode = compositeMode ?? ""; + Quality = quality ?? ""; + Props = props ?? new Dictionary(StringComparer.Ordinal); + } + + public string NodeId { get; } + public string DocumentRef { get; } + public string DocumentSchema { get; } + public string MaterialAssetRef { get; } + public string RenderChannel { get; } + public string CompositeMode { get; } + public string Quality { get; } + public IReadOnlyDictionary Props { get; } } public sealed class EveUnityPlayableWorldEntity diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs new file mode 100644 index 0000000..e40a392 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs @@ -0,0 +1,10 @@ +using System; +using GameCult.Eve.PluginFields; + +namespace GameCult.Eve.UnityScene.Fields +{ + public interface IEveUnityFieldsSplatsDocumentSource + { + event Action FieldsSplatsAvailable; + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs.meta new file mode 100644 index 0000000..608135d --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 01897652f3f84ae0be3a99c9f9837d9d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs new file mode 100644 index 0000000..69ed109 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using GameCult.Eve.PluginFields; +using UnityEngine; +using UnityEngine.Rendering; + +#nullable enable + +namespace GameCult.Eve.UnityScene.Fields +{ + public sealed class EveUnityFieldsVolumeRenderer : MonoBehaviour + { + private EveUnityPlayableWorldClientHost? _host; + private IEveUnityFieldsSplatsDocumentSource? _source; + private EveFieldsSplatsDocument? _document; + private EveFieldsSplatLayerRenderer? _layers; + private readonly List _targets = new List(); + private Material? _material; + private Shader? _sourceShader; + private RenderTexture? _cloudTexture; + private CommandBuffer? _commands; + private string _activeNodeId = ""; + + public long PresentedFrameId { get; private set; } = -1; + public int PresentedLayerCount => _targets.Count(target => target.TargetTexture != null); + + public void Bind(EveUnityPlayableWorldClientHost host, IEveUnityFieldsSplatsDocumentSource? source) + { + if (host == null) throw new ArgumentNullException(nameof(host)); + if (_source != null) _source.FieldsSplatsAvailable -= OnFieldsSplatsAvailable; + _host = host; + _source = source; + if (_source != null) _source.FieldsSplatsAvailable += OnFieldsSplatsAvailable; + enabled = _source != null; + } + + private void OnEnable() => RenderPipelineManager.endCameraRendering += OnEndCameraRendering; + + private void OnDisable() + { + RenderPipelineManager.endCameraRendering -= OnEndCameraRendering; + ReleaseRenderState(); + } + + private void OnDestroy() + { + if (_source != null) _source.FieldsSplatsAvailable -= OnFieldsSplatsAvailable; + _source = null; + _host = null; + } + + private void OnFieldsSplatsAvailable(EveFieldsSplatsDocument document) + { + if (document == null || document.FrameId < PresentedFrameId) return; + _document = document; + PresentedFrameId = document.FrameId; + RenderLayers(document); + } + + private void RenderLayers(EveFieldsSplatsDocument document) + { + var field = ActiveField(); + if (field == null) return; + EnsureLayerRenderer(); + EnsureTargets(field); + var width = PositiveInt(field, "textureWidth", 512); + var height = PositiveInt(field, "textureHeight", 512); + _layers!.Render(document, _targets, width, height); + if (!EnsureMaterial(field)) return; + ApplyFieldBindings(field, document); + } + + private void OnEndCameraRendering(ScriptableRenderContext context, Camera camera) + { + if (_host == null || _document == null || camera == null || + _host.ActiveCameraTransform != camera.transform) + return; + var field = ActiveField(); + if (field == null || !EnsureMaterial(field)) return; + ApplyFieldBindings(field, _document); + camera.depthTextureMode |= DepthTextureMode.Depth; + EnsureCloudTexture(camera, field); + if (_cloudTexture == null) return; + + var gpuProjection = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true); + SetMatrix(field, "cameraInverseViewProjectionProperty", (gpuProjection * camera.worldToCameraMatrix).inverse); + SetVector(field, "cameraProjectionExtentsProperty", ProjectionExtents(camera)); + SetFloat(field, "raymarchOffsetProperty", Halton(Time.frameCount & 1023, 3)); + foreach (var binding in ParseBindings(Prop(field, "screenTextureScaleBindings"))) + { + var texture = _material!.GetTexture(binding.Key); + if (texture != null) + _material.SetVector(binding.Value, new Vector4( + (float)Math.Max(1, camera.pixelWidth) / Math.Max(1, texture.width), + (float)Math.Max(1, camera.pixelHeight) / Math.Max(1, texture.height), + 0f, + 0f)); + } + + var raymarchPass = NonNegativeInt(field, "raymarchPass", 0); + var compositePass = NonNegativeInt(field, "compositePass", 2); + var cloudTextureProperty = Prop(field, "cloudTextureProperty"); + if (!string.IsNullOrWhiteSpace(cloudTextureProperty)) + _material!.SetTexture(cloudTextureProperty, _cloudTexture); + + _commands ??= new CommandBuffer { name = "Eve Fields Volume" }; + _commands.Clear(); + _commands.SetRenderTarget(_cloudTexture); + _commands.ClearRenderTarget(false, true, Color.clear); + _commands.DrawProcedural(Matrix4x4.identity, _material!, raymarchPass, MeshTopology.Triangles, 3, 1); + _commands.SetRenderTarget(BuiltinRenderTextureType.CameraTarget); + _commands.DrawProcedural(Matrix4x4.identity, _material!, compositePass, MeshTopology.Triangles, 3, 1); + context.ExecuteCommandBuffer(_commands); + } + + private EveUnityFieldVolumeProjection? ActiveField() => + _host?.ActiveWorld?.FieldVolumes.FirstOrDefault(field => + string.Equals(field.DocumentSchema, EveFieldsSchemas.Splats, StringComparison.Ordinal)); + + private bool EnsureMaterial(EveUnityFieldVolumeProjection field) + { + var provider = _host?.NativeAssetProvider; + if (provider == null || string.IsNullOrWhiteSpace(field.MaterialAssetRef)) return false; + var shader = provider.ResolveAsset( + new EveUnityPlayableWorldAssetBinding(field.MaterialAssetRef, "", "provider-asset-ref"), + typeof(Shader)) as Shader; + if (shader == null || !shader.isSupported) return false; + if (_material != null && ReferenceEquals(shader, _sourceShader) && + string.Equals(_activeNodeId, field.NodeId, StringComparison.Ordinal)) return true; + if (_material != null) Destroy(_material); + _sourceShader = shader; + _activeNodeId = field.NodeId; + _material = new Material(shader) { hideFlags = HideFlags.DontSave }; + foreach (var prop in field.Props) + { + if (prop.Key.StartsWith("materialFloat.", StringComparison.Ordinal) && + TryFloat(prop.Value, out var value)) + _material.SetFloat(prop.Key.Substring("materialFloat.".Length), value); + else if (prop.Key.StartsWith("materialVector.", StringComparison.Ordinal) && + TryVector(prop.Value, out var vector)) + _material.SetVector(prop.Key.Substring("materialVector.".Length), vector); + } + foreach (var binding in ParseBindings(Prop(field, "assetTextureBindings"))) + { + var texture = provider.ResolveAsset( + new EveUnityPlayableWorldAssetBinding(binding.Key, "", "provider-asset-ref"), + typeof(Texture)) as Texture; + if (texture != null) _material.SetTexture(binding.Value, texture); + } + foreach (var keyword in Prop(field, "shaderKeywords").Split(';')) + if (!string.IsNullOrWhiteSpace(keyword)) _material.EnableKeyword(keyword.Trim()); + return true; + } + + private void ApplyFieldBindings(EveUnityFieldVolumeProjection field, EveFieldsSplatsDocument document) + { + if (_material == null) return; + var bindings = ParseBindings(Prop(field, "layerBindings")); + foreach (var binding in bindings) + { + var target = _targets.FirstOrDefault(value => string.Equals(value.LayerKey, binding.Key, StringComparison.Ordinal)); + if (target?.TargetTexture != null) _material.SetTexture(binding.Value, target.TargetTexture); + } + var viewportProperty = Prop(field, "viewportTransformProperty"); + if (!string.IsNullOrWhiteSpace(viewportProperty)) + { + var viewport = document.Viewport; + var width = viewport.MaxX - viewport.MinX; + var height = viewport.MaxY - viewport.MinY; + _material.SetVector(viewportProperty, new Vector4( + (float)((viewport.MinX + viewport.MaxX) * 0.5), + (float)((viewport.MinY + viewport.MaxY) * 0.5), + (float)width, + (float)height)); + } + } + + private void EnsureTargets(EveUnityFieldVolumeProjection field) + { + var bindings = ParseBindings(Prop(field, "layerBindings")); + if (string.Equals(_activeNodeId, field.NodeId, StringComparison.Ordinal) && + _targets.Count == bindings.Count) return; + _targets.Clear(); + foreach (var binding in bindings) + _targets.Add(new EveFieldsSplatLayerTarget { LayerKey = binding.Key, UseMipMaps = true }); + } + + private void EnsureLayerRenderer() + { + if (_layers != null) return; + _layers = GetComponent(); + if (_layers == null) _layers = gameObject.AddComponent(); + } + + private void EnsureCloudTexture(Camera camera, EveUnityFieldVolumeProjection field) + { + var downsample = Mathf.Clamp(NonNegativeInt(field, "downsample", 1), 0, 3); + var width = Mathf.Max(1, camera.pixelWidth >> downsample); + var height = Mathf.Max(1, camera.pixelHeight >> downsample); + if (_cloudTexture != null && _cloudTexture.width == width && _cloudTexture.height == height) return; + if (_cloudTexture != null) { _cloudTexture.Release(); Destroy(_cloudTexture); } + _cloudTexture = new RenderTexture(width, height, 0, RenderTextureFormat.ARGBFloat) + { + name = "Eve Fields Volume", + filterMode = FilterMode.Bilinear, + wrapMode = TextureWrapMode.Clamp + }; + _cloudTexture.Create(); + } + + private void ReleaseRenderState() + { + if (_commands != null) { _commands.Release(); _commands = null; } + if (_cloudTexture != null) { _cloudTexture.Release(); Destroy(_cloudTexture); _cloudTexture = null; } + if (_material != null) { Destroy(_material); _material = null; } + _sourceShader = null; + _activeNodeId = ""; + PresentedFrameId = -1; + } + + private void SetMatrix(EveUnityFieldVolumeProjection field, string propKey, Matrix4x4 value) + { + var property = Prop(field, propKey); + if (!string.IsNullOrWhiteSpace(property)) _material!.SetMatrix(property, value); + } + + private void SetVector(EveUnityFieldVolumeProjection field, string propKey, Vector4 value) + { + var property = Prop(field, propKey); + if (!string.IsNullOrWhiteSpace(property)) _material!.SetVector(property, value); + } + + private void SetFloat(EveUnityFieldVolumeProjection field, string propKey, float value) + { + var property = Prop(field, propKey); + if (!string.IsNullOrWhiteSpace(property)) _material!.SetFloat(property, value); + } + + private static Vector4 ProjectionExtents(Camera camera) + { + var y = camera.orthographic ? camera.orthographicSize : Mathf.Tan(0.5f * Mathf.Deg2Rad * camera.fieldOfView); + return new Vector4(y * camera.aspect, y, 0f, 0f); + } + + private static float Halton(int index, int radix) + { + var result = 0f; + var fraction = 1f / radix; + while (index > 0) { result += (index % radix) * fraction; index /= radix; fraction /= radix; } + return result; + } + + private static Dictionary ParseBindings(string value) + { + var result = new Dictionary(StringComparer.Ordinal); + foreach (var pair in (value ?? "").Split(';')) + { + var separator = pair.IndexOf('='); + if (separator <= 0 || separator >= pair.Length - 1) continue; + result[pair.Substring(0, separator).Trim()] = pair.Substring(separator + 1).Trim(); + } + return result; + } + + private static string Prop(EveUnityFieldVolumeProjection field, string key) => + field.Props.TryGetValue(key, out var value) ? value ?? "" : ""; + + private static int PositiveInt(EveUnityFieldVolumeProjection field, string key, int fallback) => + int.TryParse(Prop(field, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && value > 0 ? value : fallback; + + private static int NonNegativeInt(EveUnityFieldVolumeProjection field, string key, int fallback) => + int.TryParse(Prop(field, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && value >= 0 ? value : fallback; + + private static bool TryFloat(string value, out float result) => + float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out result) && !float.IsNaN(result) && !float.IsInfinity(result); + + private static bool TryVector(string value, out Vector4 result) + { + result = default; + var values = (value ?? "").Split(','); + if (values.Length < 2 || values.Length > 4) return false; + var parsed = new float[4]; + for (var index = 0; index < values.Length; index++) + if (!TryFloat(values[index], out parsed[index])) return false; + result = new Vector4(parsed[0], parsed[1], parsed[2], parsed[3]); + return true; + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs.meta new file mode 100644 index 0000000..bf9a46e --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9c5d41ca3a3a4335b1b82104bba3e20a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index 959b571..0cbb1a8 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -23,6 +23,24 @@ namespace GameCult.Eve.UnityScene.Tests assembly.Name?.Contains("Aetheria", StringComparison.OrdinalIgnoreCase) == true), Is.False); } + [Test] + public void CanonicalFieldsPackageOwnsTheCultMeshWireDocument() + { + Assert.That(typeof(EveFieldsSplatsDocument).GetCustomAttributes(false) + .Any(attribute => attribute.GetType().Name == "CultDocumentAttribute"), Is.True); + Assert.That(new EveFieldsSplatsDocument().Schema, Is.EqualTo(EveFieldsSchemas.Splats)); + Assert.That(typeof(EveFieldsSplatsDocument).Assembly.GetReferencedAssemblies() + .Any(assembly => assembly.Name?.Contains("Aetheria", StringComparison.OrdinalIgnoreCase) == true), Is.False); + } + + [Test] + public void VolumeRendererConsumesCanonicalFieldsWithoutProviderCode() + { + Assert.That(typeof(EveUnityFieldsVolumeRenderer).Assembly.GetReferencedAssemblies() + .Any(assembly => assembly.Name?.Contains("Aetheria", StringComparison.OrdinalIgnoreCase) == true), Is.False); + Assert.That(typeof(IEveUnityFieldsSplatsDocumentSource).GetEvent("FieldsSplatsAvailable"), Is.Not.Null); + } + [Test] public void PackageContainsGenericFieldsShader() { diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 0f7d926..6bc1684 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using GameCult.Caching; +using GameCult.Eve.PluginFields; using GameCult.Eve.Surface; using GameCult.Mesh; using NUnit.Framework; @@ -421,6 +422,10 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(projection.PlayableWorld.FocusCommand, Is.EqualTo("aetheria.daemon.focus")); Assert.That(projection.PlayableWorld.TargetCommand, Is.EqualTo("aetheria.daemon.target")); Assert.That(projection.PlayableWorld.EntityCount, Is.EqualTo(3)); + Assert.That(projection.PlayableWorld.FieldVolumes.Count, Is.EqualTo(1)); + Assert.That(projection.PlayableWorld.FieldVolumes[0].DocumentRef, Is.EqualTo("cultmesh://aetheria/world/fog-splats")); + Assert.That(projection.PlayableWorld.FieldVolumes[0].MaterialAssetRef, Is.EqualTo("shader.environment.gravity-fog")); + Assert.That(projection.PlayableWorld.FieldVolumes[0].RenderChannel, Is.EqualTo("world.transparent")); var player = FindEntity(projection.PlayableWorld, "player-vanguard"); Assert.That(player.EntityKind, Is.EqualTo("player")); @@ -1864,6 +1869,21 @@ namespace GameCult.Eve.UnityScene.Tests }, Array.Empty())); + playableChildren.Add(new EveSurfaceComponent( + "aetheria.daemon.game.gravity-fog", + "field.volume3d", + new Dictionary(StringComparer.Ordinal) + { + ["documentRef"] = "cultmesh://aetheria/world/fog-splats", + ["documentSchema"] = EveFieldsSchemas.Splats, + ["materialAssetRef"] = "shader.environment.gravity-fog", + ["renderChannel"] = "world.transparent", + ["compositeMode"] = "premultiplied-alpha", + ["quality"] = "normal", + ["layerBindings"] = "fog.surface_height=_NebulaSurfaceHeight;fog.tint=_NebulaTint" + }, + Array.Empty())); + return new EveSurfaceDocument( "aetheria", "game.runtime", diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 4bfe911..1e161eb 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,12 +1,12 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.32", + "version": "0.3.33", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { "org.gamecult.cultlib": "1.0.13", - "org.gamecult.eve.plugin-fields": "0.1.1", + "org.gamecult.eve.plugin-fields": "0.2.0", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" From 717e051d20355affdd65f34c0b32ad86fb8d1cf3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 17:15:37 +0100 Subject: [PATCH 076/292] Pin volumetric fields release witness --- ReleaseConsumerProject/Packages/manifest.json | 4 ++-- ReleaseConsumerProject/Packages/packages-lock.json | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 0eca353..a7553f4 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -7,9 +7,9 @@ "com.unity.modules.uielements": "1.0.0", "com.unity.render-pipelines.universal": "17.4.0", "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", - "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", + "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.0", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.32", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.33", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index fbe0822..9f23367 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -126,11 +126,13 @@ "hash": "feb5c71513e71d681699f462fe3682b3168c6f73" }, "org.gamecult.eve.plugin-fields": { - "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", + "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.0", "depth": 0, "source": "git", - "dependencies": {}, - "hash": "0b099274a4e2b4c301eced4322166837792f585a" + "dependencies": { + "org.gamecult.cultlib": "1.0.13" + }, + "hash": "382a23d8f8a07e4b5eef5f81f84655861a858367" }, "org.gamecult.eve.surface": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", @@ -142,17 +144,17 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.32", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.33", "depth": 0, "source": "git", "dependencies": { "org.gamecult.cultlib": "1.0.13", - "org.gamecult.eve.plugin-fields": "0.1.1", + "org.gamecult.eve.plugin-fields": "0.2.0", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "817912a041ac1911e1e27a4b88617754d90ad4b7" + "hash": "852458dade1ae535d2ff3cfc9f8fe430e341ce2a" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", From 37d9f993e189dc6a0f3c01cdbabf5c7241ea0b1c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 17:31:02 +0100 Subject: [PATCH 077/292] Reconcile volumetric fields after surface arrival --- .../PlayMode/GenericWorldCaptureTests.cs | 21 +++++++++++++++++++ .../PlayMode/GenericWorldCaptureTests.cs | 21 +++++++++++++++++++ .../Fields/EveUnityFieldsVolumeRenderer.cs | 4 ++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 27875c4..ed3531c 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -8,6 +8,7 @@ using GameCult.Caching.MessagePack; using GameCult.Eve.Surface; using GameCult.Eve.UnityUIToolkit; using GameCult.Eve.UnityScene; +using GameCult.Eve.UnityScene.Fields; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; @@ -666,6 +667,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(beamRenderer.TryGetPower(beamPresentation.Id, out tractorReleasedPower), Is.True); Assert.That(tractorReleasedPower, Is.Zero, "Released held input did not return the daemon-authored tractor presentation to zero."); + var fieldVolume = root.GetComponent(); + Assert.That(fieldVolume, Is.Not.Null, "The generic client did not install its Fields volume lowerer."); + Assert.That(fieldVolume.PresentedFrameId, Is.GreaterThanOrEqualTo(0), + "The advertised Fields splat document never reached the generic volume lowerer."); + Assert.That(fieldVolume.PresentedLayerCount, Is.GreaterThan(0), + "The generic volume lowerer did not rasterize any advertised Fields layers."); + Assert.That(fieldVolume.CompositeCount, Is.GreaterThan(0), + "The generic volume lowerer never composited into the pilot camera."); WriteWitnessFacts( initialVersion, runtime.ActiveVersion, @@ -677,6 +686,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests (camera.cullingMask & (1 << mapLayer)) == 0, (mapCamera.cullingMask & (1 << mapLayer)) != 0, environmentPresentation, + fieldVolume.PresentedFrameId, + fieldVolume.PresentedLayerCount, + fieldVolume.CompositeCount, movementReceipt, lookReceipt, focusReceipt, @@ -733,6 +745,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests bool pilotCameraExcludesMapChannel, bool mapCameraIncludesMapChannel, bool environmentPresentation, + long fieldVolumeFrameId, + int fieldVolumeLayerCount, + long fieldVolumeCompositeCount, WitnessReceipt movement, WitnessReceipt look, WitnessReceipt targeting, @@ -769,6 +784,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests providerAdvertisement = true, providerAssets = true, environmentPresentation = environmentPresentation, + fieldVolumeFrameId = fieldVolumeFrameId, + fieldVolumeLayerCount = fieldVolumeLayerCount, + fieldVolumeCompositeCount = fieldVolumeCompositeCount, movement = movement != null, aimPresentation = look != null && aimDotDistance >= 49.9f, targeting = targeting != null, @@ -828,6 +846,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public bool providerAdvertisement; public bool providerAssets; public bool environmentPresentation; + public long fieldVolumeFrameId; + public int fieldVolumeLayerCount; + public long fieldVolumeCompositeCount; public bool movement; public bool aimPresentation; public bool targeting; diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 27875c4..ed3531c 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -8,6 +8,7 @@ using GameCult.Caching.MessagePack; using GameCult.Eve.Surface; using GameCult.Eve.UnityUIToolkit; using GameCult.Eve.UnityScene; +using GameCult.Eve.UnityScene.Fields; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; @@ -666,6 +667,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(beamRenderer.TryGetPower(beamPresentation.Id, out tractorReleasedPower), Is.True); Assert.That(tractorReleasedPower, Is.Zero, "Released held input did not return the daemon-authored tractor presentation to zero."); + var fieldVolume = root.GetComponent(); + Assert.That(fieldVolume, Is.Not.Null, "The generic client did not install its Fields volume lowerer."); + Assert.That(fieldVolume.PresentedFrameId, Is.GreaterThanOrEqualTo(0), + "The advertised Fields splat document never reached the generic volume lowerer."); + Assert.That(fieldVolume.PresentedLayerCount, Is.GreaterThan(0), + "The generic volume lowerer did not rasterize any advertised Fields layers."); + Assert.That(fieldVolume.CompositeCount, Is.GreaterThan(0), + "The generic volume lowerer never composited into the pilot camera."); WriteWitnessFacts( initialVersion, runtime.ActiveVersion, @@ -677,6 +686,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests (camera.cullingMask & (1 << mapLayer)) == 0, (mapCamera.cullingMask & (1 << mapLayer)) != 0, environmentPresentation, + fieldVolume.PresentedFrameId, + fieldVolume.PresentedLayerCount, + fieldVolume.CompositeCount, movementReceipt, lookReceipt, focusReceipt, @@ -733,6 +745,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests bool pilotCameraExcludesMapChannel, bool mapCameraIncludesMapChannel, bool environmentPresentation, + long fieldVolumeFrameId, + int fieldVolumeLayerCount, + long fieldVolumeCompositeCount, WitnessReceipt movement, WitnessReceipt look, WitnessReceipt targeting, @@ -769,6 +784,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests providerAdvertisement = true, providerAssets = true, environmentPresentation = environmentPresentation, + fieldVolumeFrameId = fieldVolumeFrameId, + fieldVolumeLayerCount = fieldVolumeLayerCount, + fieldVolumeCompositeCount = fieldVolumeCompositeCount, movement = movement != null, aimPresentation = look != null && aimDotDistance >= 49.9f, targeting = targeting != null, @@ -828,6 +846,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public bool providerAdvertisement; public bool providerAssets; public bool environmentPresentation; + public long fieldVolumeFrameId; + public int fieldVolumeLayerCount; + public long fieldVolumeCompositeCount; public bool movement; public bool aimPresentation; public bool targeting; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs index 69ed109..aeea49b 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -25,6 +25,7 @@ namespace GameCult.Eve.UnityScene.Fields public long PresentedFrameId { get; private set; } = -1; public int PresentedLayerCount => _targets.Count(target => target.TargetTexture != null); + public long CompositeCount { get; private set; } public void Bind(EveUnityPlayableWorldClientHost host, IEveUnityFieldsSplatsDocumentSource? source) { @@ -77,6 +78,7 @@ namespace GameCult.Eve.UnityScene.Fields if (_host == null || _document == null || camera == null || _host.ActiveCameraTransform != camera.transform) return; + if (PresentedLayerCount == 0) RenderLayers(_document); var field = ActiveField(); if (field == null || !EnsureMaterial(field)) return; ApplyFieldBindings(field, _document); @@ -113,6 +115,7 @@ namespace GameCult.Eve.UnityScene.Fields _commands.SetRenderTarget(BuiltinRenderTextureType.CameraTarget); _commands.DrawProcedural(Matrix4x4.identity, _material!, compositePass, MeshTopology.Triangles, 3, 1); context.ExecuteCommandBuffer(_commands); + CompositeCount++; } private EveUnityFieldVolumeProjection? ActiveField() => @@ -218,6 +221,7 @@ namespace GameCult.Eve.UnityScene.Fields _sourceShader = null; _activeNodeId = ""; PresentedFrameId = -1; + CompositeCount = 0; } private void SetMatrix(EveUnityFieldVolumeProjection field, string propKey, Matrix4x4 value) diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 1e161eb..2737004 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.33", + "version": "0.3.34", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 5af02fdaa4357130e3a05142510336711d3c09ae Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 18:43:52 +0100 Subject: [PATCH 078/292] Set volumetric render target viewports --- .../Runtime/Fields/EveUnityFieldsVolumeRenderer.cs | 2 ++ packages/org.gamecult.eve.unity-scene/package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs index aeea49b..483e3ce 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -110,9 +110,11 @@ namespace GameCult.Eve.UnityScene.Fields _commands ??= new CommandBuffer { name = "Eve Fields Volume" }; _commands.Clear(); _commands.SetRenderTarget(_cloudTexture); + _commands.SetViewport(new Rect(0f, 0f, _cloudTexture.width, _cloudTexture.height)); _commands.ClearRenderTarget(false, true, Color.clear); _commands.DrawProcedural(Matrix4x4.identity, _material!, raymarchPass, MeshTopology.Triangles, 3, 1); _commands.SetRenderTarget(BuiltinRenderTextureType.CameraTarget); + _commands.SetViewport(camera.pixelRect); _commands.DrawProcedural(Matrix4x4.identity, _material!, compositePass, MeshTopology.Triangles, 3, 1); context.ExecuteCommandBuffer(_commands); CompositeCount++; diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 2737004..fc244de 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.34", + "version": "0.3.35", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 126075697f98cdd448730f9bb67bb56cb8b24f78 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 18:50:48 +0100 Subject: [PATCH 079/292] Execute volumetric composition after camera rendering --- .../Runtime/Fields/EveUnityFieldsVolumeRenderer.cs | 2 +- packages/org.gamecult.eve.unity-scene/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs index 483e3ce..8f97a46 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -116,7 +116,7 @@ namespace GameCult.Eve.UnityScene.Fields _commands.SetRenderTarget(BuiltinRenderTextureType.CameraTarget); _commands.SetViewport(camera.pixelRect); _commands.DrawProcedural(Matrix4x4.identity, _material!, compositePass, MeshTopology.Triangles, 3, 1); - context.ExecuteCommandBuffer(_commands); + Graphics.ExecuteCommandBuffer(_commands); CompositeCount++; } diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index fc244de..6efc9a7 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.35", + "version": "0.3.36", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 5ce654800760203154132d4b0fd1475294283ba6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 19:59:37 +0100 Subject: [PATCH 080/292] Move volume shader ABI into runtime asset variants --- .../org.gamecult.eve.unity-scene/README.md | 8 ++ .../EveUnityCultMeshLiveProviderTransport.cs | 27 ++++ .../EveUnityCultMeshPlayableWorldProvider.cs | 10 ++ ...veUnityGameObjectPlayableWorldSceneSink.cs | 7 + .../Runtime/EveUnityPlayableWorldRuntime.cs | 15 ++- .../Fields/EveUnityFieldsVolumeRenderer.cs | 120 +++++++++++------- .../EveUnityCultMeshAssetLookupTests.cs | 25 ++++ .../EveUnitySceneSurfaceLowererTests.cs | 2 +- .../org.gamecult.eve.unity-scene/package.json | 2 +- 9 files changed, 166 insertions(+), 50 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index dd93898..f7d3d74 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -62,3 +62,11 @@ therefore reference the effect without importing Aetheria gameplay code. `org.gamecult.eve.plugin-fields` contracts. `EveFieldsSplatRasterizer` owns the Unity structured-buffer and RenderTexture lowering without importing provider document classes. + +`field.volume3d` surfaces name logical texture ports, feature names, quality, +and authored scalar/vector parameters. Concrete Unity shader properties, +keywords, and pass indices belong to the selected `unity-scene` asset variant +under `unity.volume.*` metadata. The lowerer fails closed when that native +program descriptor is absent or incomplete. This keeps provider shader ABIs +out of Eve's semantic surface while allowing a provider-owned shader to consume +portable Eve Fields documents. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index f90d0ed..21c535b 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -48,6 +48,8 @@ namespace GameCult.Eve.UnityScene private readonly HashSet _publishedReceiptIds = new HashSet(StringComparer.Ordinal); private readonly Dictionary _prefabs = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _nativeAssets = new Dictionary(StringComparer.Ordinal); + private readonly Dictionary> _nativeAssetMetadata = + new Dictionary>(StringComparer.Ordinal); private readonly List _assetBundles = new List(); private readonly Dictionary _renderChannelLayers = new Dictionary(StringComparer.Ordinal); private readonly ConcurrentQueue _liveDocuments = new ConcurrentQueue(); @@ -314,6 +316,7 @@ namespace GameCult.Eve.UnityScene _assetBundles.Clear(); _prefabs.Clear(); _nativeAssets.Clear(); + _nativeAssetMetadata.Clear(); _node?.Dispose(); _snapshot?.Dispose(); _subscriptions?.Dispose(); @@ -344,6 +347,14 @@ namespace GameCult.Eve.UnityScene ? value : null; } + public bool TryResolveAssetMetadata( + EveUnityPlayableWorldAssetBinding asset, + out IReadOnlyDictionary metadata) + { + if (asset == null) throw new ArgumentNullException(nameof(asset)); + return _nativeAssetMetadata.TryGetValue(asset.AssetRef, out metadata!); + } + public bool TryGetRenderChannelLayer(string channel, out int layer) { return _renderChannelLayers.TryGetValue(channel ?? "", out layer); @@ -679,6 +690,7 @@ namespace GameCult.Eve.UnityScene _assetBundles.Clear(); _prefabs.Clear(); _nativeAssets.Clear(); + _nativeAssetMetadata.Clear(); _renderChannelLayers.Clear(); var selected = catalog.Assets .Select(asset => new @@ -728,9 +740,14 @@ namespace GameCult.Eve.UnityScene $"'{selection.Variant.AssetKey}'. Available assets: " + string.Join(", ", bundleAssetNames)); _nativeAssets[selection.Asset.AssetRef] = value; + var metadata = MergeAssetMetadata(selection.Asset.Metadata, selection.Variant.Metadata); + _nativeAssetMetadata[selection.Asset.AssetRef] = metadata; if (selection.Asset.Metadata.TryGetValue("presentationRole", out var role) && !string.IsNullOrWhiteSpace(role)) + { _nativeAssets[role] = value; + _nativeAssetMetadata[role] = metadata; + } if (value is GameObject prefab) { _prefabs[selection.Asset.AssetRef] = prefab; @@ -748,6 +765,16 @@ namespace GameCult.Eve.UnityScene string.Equals(assetName, advertisedAssetKey, StringComparison.OrdinalIgnoreCase)); } + private static IReadOnlyDictionary MergeAssetMetadata( + IReadOnlyDictionary assetMetadata, + IReadOnlyDictionary variantMetadata) + { + var merged = new Dictionary(StringComparer.Ordinal); + foreach (var pair in assetMetadata) merged[pair.Key] = pair.Value; + foreach (var pair in variantMetadata) merged[pair.Key] = pair.Value; + return merged; + } + private void ReadCameraPolicies(IEnumerable variants) { _renderChannelLayers.Clear(); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs index fe07bb7..6596fb7 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Collections.Concurrent; +using System.Collections.Generic; using GameCult.Eve.Surface; using GameCult.Eve.PluginFields; using GameCult.Eve.UnityScene.Fields; @@ -22,6 +23,7 @@ namespace GameCult.Eve.UnityScene IEveUnityFieldsSplatsDocumentSource, IEveUnityCameraRenderPolicySource, IEveUnityNativeAssetProvider, + IEveUnityNativeAssetMetadataProvider, IEveUnityInputCapabilitySource { [SerializeField] private string rendezvousEndpoint = ""; @@ -124,6 +126,14 @@ namespace GameCult.Eve.UnityScene return _transport!.ResolveAsset(asset, assetType); } + public bool TryResolveAssetMetadata( + EveUnityPlayableWorldAssetBinding asset, + out IReadOnlyDictionary metadata) + { + EnsureTransport(); + return _transport!.TryResolveAssetMetadata(asset, out metadata); + } + public bool TryGetRenderChannelLayer(string channel, out int layer) { EnsureTransport(); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs index 1fafbe0..e1b5bf3 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs @@ -17,6 +17,13 @@ namespace GameCult.Eve.UnityScene UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType); } + public interface IEveUnityNativeAssetMetadataProvider + { + bool TryResolveAssetMetadata( + EveUnityPlayableWorldAssetBinding asset, + out IReadOnlyDictionary metadata); + } + public sealed class EveUnityGameObjectPlayableWorldSceneSink : IEveUnityPlayableWorldSceneSink, IEveUnityEntityGenerationSink, diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs index 7af275f..36b034a 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs @@ -234,7 +234,9 @@ namespace GameCult.Eve.UnityScene _entityPresenter.Apply(document, lease); } - public sealed class EveUnityLivePlayableWorldAssetProvider : IEveUnityNativeAssetProvider + public sealed class EveUnityLivePlayableWorldAssetProvider : + IEveUnityNativeAssetProvider, + IEveUnityNativeAssetMetadataProvider { private readonly EveUnityPlayableWorldAssetManifestCache _assetManifests; private readonly Func _activeWorld; @@ -281,5 +283,16 @@ namespace GameCult.Eve.UnityScene } return (_fallback as IEveUnityNativeAssetProvider)?.ResolveAsset(asset, assetType); } + + public bool TryResolveAssetMetadata( + EveUnityPlayableWorldAssetBinding asset, + out IReadOnlyDictionary metadata) + { + if (asset == null) throw new ArgumentNullException(nameof(asset)); + if (_fallback is IEveUnityNativeAssetMetadataProvider provider) + return provider.TryResolveAssetMetadata(asset, out metadata); + metadata = new Dictionary(StringComparer.Ordinal); + return false; + } } } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs index 8f97a46..37aeb7f 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -19,6 +19,7 @@ namespace GameCult.Eve.UnityScene.Fields private readonly List _targets = new List(); private Material? _material; private Shader? _sourceShader; + private IReadOnlyDictionary? _programMetadata; private RenderTexture? _cloudTexture; private CommandBuffer? _commands; private string _activeNodeId = ""; @@ -87,25 +88,13 @@ namespace GameCult.Eve.UnityScene.Fields if (_cloudTexture == null) return; var gpuProjection = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true); - SetMatrix(field, "cameraInverseViewProjectionProperty", (gpuProjection * camera.worldToCameraMatrix).inverse); - SetVector(field, "cameraProjectionExtentsProperty", ProjectionExtents(camera)); - SetFloat(field, "raymarchOffsetProperty", Halton(Time.frameCount & 1023, 3)); - foreach (var binding in ParseBindings(Prop(field, "screenTextureScaleBindings"))) - { - var texture = _material!.GetTexture(binding.Key); - if (texture != null) - _material.SetVector(binding.Value, new Vector4( - (float)Math.Max(1, camera.pixelWidth) / Math.Max(1, texture.width), - (float)Math.Max(1, camera.pixelHeight) / Math.Max(1, texture.height), - 0f, - 0f)); - } + SetMatrixPort("cameraInverseViewProjection", (gpuProjection * camera.worldToCameraMatrix).inverse); + SetVectorPort("cameraProjectionExtents", ProjectionExtents(camera)); + SetFloatPort("raymarchOffset", Halton(Time.frameCount & 1023, 3)); - var raymarchPass = NonNegativeInt(field, "raymarchPass", 0); - var compositePass = NonNegativeInt(field, "compositePass", 2); - var cloudTextureProperty = Prop(field, "cloudTextureProperty"); - if (!string.IsNullOrWhiteSpace(cloudTextureProperty)) - _material!.SetTexture(cloudTextureProperty, _cloudTexture); + var raymarchPass = ProgramPass("raymarch"); + var compositePass = ProgramPass("composite"); + SetTexturePort("cloud", _cloudTexture); _commands ??= new CommandBuffer { name = "Eve Fields Volume" }; _commands.Clear(); @@ -132,30 +121,49 @@ namespace GameCult.Eve.UnityScene.Fields new EveUnityPlayableWorldAssetBinding(field.MaterialAssetRef, "", "provider-asset-ref"), typeof(Shader)) as Shader; if (shader == null || !shader.isSupported) return false; + var shaderBinding = new EveUnityPlayableWorldAssetBinding(field.MaterialAssetRef, "", "provider-asset-ref"); + if (provider is not IEveUnityNativeAssetMetadataProvider metadataProvider || + !metadataProvider.TryResolveAssetMetadata(shaderBinding, out var metadata)) + return false; + _programMetadata = metadata; + if (ProgramPass("raymarch") < 0 || ProgramPass("composite") < 0 || + string.IsNullOrWhiteSpace(ProgramPort("texture", "cloud")) || + string.IsNullOrWhiteSpace(ProgramPort("matrix", "cameraInverseViewProjection")) || + string.IsNullOrWhiteSpace(ProgramPort("vector", "cameraProjectionExtents")) || + string.IsNullOrWhiteSpace(ProgramPort("vector", "viewportTransform")) || + string.IsNullOrWhiteSpace(ProgramPort("float", "raymarchOffset"))) + return false; if (_material != null && ReferenceEquals(shader, _sourceShader) && string.Equals(_activeNodeId, field.NodeId, StringComparison.Ordinal)) return true; if (_material != null) Destroy(_material); _sourceShader = shader; _activeNodeId = field.NodeId; _material = new Material(shader) { hideFlags = HideFlags.DontSave }; - foreach (var prop in field.Props) + foreach (var parameter in ParseBindings(Prop(field, "floatParameters"))) { - if (prop.Key.StartsWith("materialFloat.", StringComparison.Ordinal) && - TryFloat(prop.Value, out var value)) - _material.SetFloat(prop.Key.Substring("materialFloat.".Length), value); - else if (prop.Key.StartsWith("materialVector.", StringComparison.Ordinal) && - TryVector(prop.Value, out var vector)) - _material.SetVector(prop.Key.Substring("materialVector.".Length), vector); + if (TryFloat(parameter.Value, out var value)) SetFloatPort(parameter.Key, value); } + foreach (var parameter in ParseBindings(Prop(field, "vectorParameters"))) + if (TryVector(parameter.Value, out var vector)) SetVectorPort(parameter.Key, vector); foreach (var binding in ParseBindings(Prop(field, "assetTextureBindings"))) { var texture = provider.ResolveAsset( new EveUnityPlayableWorldAssetBinding(binding.Key, "", "provider-asset-ref"), typeof(Texture)) as Texture; - if (texture != null) _material.SetTexture(binding.Value, texture); + if (texture != null) SetTexturePort(binding.Value, texture); + } + var quality = Prop(field, "quality").Trim().ToLowerInvariant(); + if (TryProgramValue($"unity.volume.quality.{quality}.keyword", out var keyword) && + !string.IsNullOrWhiteSpace(keyword)) + _material.EnableKeyword(keyword); + foreach (var feature in Prop(field, "features").Split(';')) + { + var logicalFeature = feature.Trim(); + if (!string.IsNullOrWhiteSpace(logicalFeature) && + TryProgramValue($"unity.volume.feature.{logicalFeature}.keyword", out var featureKeyword) && + !string.IsNullOrWhiteSpace(featureKeyword)) + _material.EnableKeyword(featureKeyword); } - foreach (var keyword in Prop(field, "shaderKeywords").Split(';')) - if (!string.IsNullOrWhiteSpace(keyword)) _material.EnableKeyword(keyword.Trim()); return true; } @@ -166,20 +174,16 @@ namespace GameCult.Eve.UnityScene.Fields foreach (var binding in bindings) { var target = _targets.FirstOrDefault(value => string.Equals(value.LayerKey, binding.Key, StringComparison.Ordinal)); - if (target?.TargetTexture != null) _material.SetTexture(binding.Value, target.TargetTexture); - } - var viewportProperty = Prop(field, "viewportTransformProperty"); - if (!string.IsNullOrWhiteSpace(viewportProperty)) - { - var viewport = document.Viewport; - var width = viewport.MaxX - viewport.MinX; - var height = viewport.MaxY - viewport.MinY; - _material.SetVector(viewportProperty, new Vector4( - (float)((viewport.MinX + viewport.MaxX) * 0.5), - (float)((viewport.MinY + viewport.MaxY) * 0.5), - (float)width, - (float)height)); + if (target?.TargetTexture != null) SetTexturePort(binding.Value, target.TargetTexture); } + var viewport = document.Viewport; + var width = viewport.MaxX - viewport.MinX; + var height = viewport.MaxY - viewport.MinY; + SetVectorPort("viewportTransform", new Vector4( + (float)((viewport.MinX + viewport.MaxX) * 0.5), + (float)((viewport.MinY + viewport.MaxY) * 0.5), + (float)width, + (float)height)); } private void EnsureTargets(EveUnityFieldVolumeProjection field) @@ -221,29 +225,51 @@ namespace GameCult.Eve.UnityScene.Fields if (_cloudTexture != null) { _cloudTexture.Release(); Destroy(_cloudTexture); _cloudTexture = null; } if (_material != null) { Destroy(_material); _material = null; } _sourceShader = null; + _programMetadata = null; _activeNodeId = ""; PresentedFrameId = -1; CompositeCount = 0; } - private void SetMatrix(EveUnityFieldVolumeProjection field, string propKey, Matrix4x4 value) + private void SetMatrixPort(string port, Matrix4x4 value) { - var property = Prop(field, propKey); + var property = ProgramPort("matrix", port); if (!string.IsNullOrWhiteSpace(property)) _material!.SetMatrix(property, value); } - private void SetVector(EveUnityFieldVolumeProjection field, string propKey, Vector4 value) + private void SetVectorPort(string port, Vector4 value) { - var property = Prop(field, propKey); + var property = ProgramPort("vector", port); if (!string.IsNullOrWhiteSpace(property)) _material!.SetVector(property, value); } - private void SetFloat(EveUnityFieldVolumeProjection field, string propKey, float value) + private void SetFloatPort(string port, float value) { - var property = Prop(field, propKey); + var property = ProgramPort("float", port); if (!string.IsNullOrWhiteSpace(property)) _material!.SetFloat(property, value); } + private void SetTexturePort(string port, Texture value) + { + var property = ProgramPort("texture", port); + if (!string.IsNullOrWhiteSpace(property)) _material!.SetTexture(property, value); + } + + private string ProgramPort(string kind, string port) => + TryProgramValue($"unity.volume.{kind}Port.{port}", out var value) ? value : ""; + + private int ProgramPass(string pass) => + TryProgramValue($"unity.volume.pass.{pass}", out var value) && + int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) && parsed >= 0 + ? parsed + : -1; + + private bool TryProgramValue(string key, out string value) + { + value = ""; + return _programMetadata != null && _programMetadata.TryGetValue(key, out value!); + } + private static Vector4 ProjectionExtents(Camera camera) { var y = camera.orthographic ? camera.orthographicSize : Mathf.Tan(0.5f * Mathf.Deg2Rad * camera.fieldOfView); diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs index 17603f9..1fd2603 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Collections.Generic; using GameCult.Eve.UnityScene; using NUnit.Framework; @@ -38,5 +39,29 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(result, Is.Null); } + + [Test] + public void SelectedRuntimeVariantOwnsConcreteNativeProgramBindings() + { + var method = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod( + "MergeAssetMetadata", + BindingFlags.Static | BindingFlags.NonPublic); + + Assert.That(method, Is.Not.Null); + var result = method!.Invoke(null, new object[] + { + new Dictionary { ["presentationRole"] = "environment.volume" }, + new Dictionary + { + ["unity.volume.texturePort.surfaceHeight"] = "_NebulaSurfaceHeight", + ["unity.volume.pass.raymarch"] = "0" + } + }) as IReadOnlyDictionary; + + Assert.That(result, Is.Not.Null); + Assert.That(result!["presentationRole"], Is.EqualTo("environment.volume")); + Assert.That(result["unity.volume.texturePort.surfaceHeight"], Is.EqualTo("_NebulaSurfaceHeight")); + Assert.That(result["unity.volume.pass.raymarch"], Is.EqualTo("0")); + } } } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 6bc1684..1ab2b4a 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -1880,7 +1880,7 @@ namespace GameCult.Eve.UnityScene.Tests ["renderChannel"] = "world.transparent", ["compositeMode"] = "premultiplied-alpha", ["quality"] = "normal", - ["layerBindings"] = "fog.surface_height=_NebulaSurfaceHeight;fog.tint=_NebulaTint" + ["layerBindings"] = "fog.surface_height=surfaceHeight;fog.tint=tint" }, Array.Empty())); diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 6efc9a7..a70e5fd 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.36", + "version": "0.3.37", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 4a513f4264d9b7e70d2ea6ff4d1db677e00d47f0 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 20:07:35 +0100 Subject: [PATCH 081/292] Prove released provider volume lowering --- .../PlayMode/GenericWorldCaptureTests.cs | 118 +++++++++++++++++- ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 +- .../PlayMode/GenericWorldCaptureTests.cs | 118 +++++++++++++++++- TestProject/Packages/packages-lock.json | 6 +- docs/aetheria-released-package-witness.md | 43 ++++--- docs/authority-map.md | 10 +- scripts/run-aetheria-daemon-world-witness.ps1 | 33 ++++- 8 files changed, 299 insertions(+), 35 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index ed3531c..f39c7ed 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -461,6 +461,31 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(finalPickupEntityCount, Is.EqualTo(initialPickupEntityCount - 1), "Collected pickup remained in the provider-authored entity generation."); + targetFact = host.PresentedEntities.CurrentGeneration.Entities + .Single(entity => entity.EntityId == targetEntityId); + playerFact = host.PresentedEntities.CurrentGeneration.Entities + .Single(entity => entity.EntityId == playerId); + aim = targetFact.Position - playerFact.Position; + aim.y = 0; + Assert.That(aim.sqrMagnitude, Is.GreaterThan(0.0001f)); + aim.Normalize(); + lookVersion = runtime.ActiveVersion; + look = runtime.SubmitLookDirectionIntent(playerId, aim.x, 0f, aim.z); + lookDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < lookDeadline && + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != look.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || + runtime.ActiveVersion < runtime.LastReceipt.SourceVersion || + Vector3.Dot(FindMarker(root, playerId).transform.forward, aim) < 0.99f)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + AssertReconciledProviderReceipt(runtime.LastReceipt, look.CommandId, + provider.Selection.ProviderId, provider.Selection.SurfaceId, lookVersion); + Assert.That(Vector3.Dot(FindMarker(root, playerId).transform.forward, aim), Is.GreaterThan(0.99f)); + lookReceipt = WitnessReceipt.From("look", runtime.LastReceipt); + var focusVersion = runtime.ActiveVersion; var inputDriver = root.GetComponent() ?? root.AddComponent(); @@ -605,6 +630,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(capturePath))); File.WriteAllBytes(capturePath, pixels.EncodeToPNG()); Assert.That(new FileInfo(capturePath).Length, Is.GreaterThan(1024)); + WriteFieldLayerCaptures(root, host.Runtime.ActiveWorld, capturePath); + WriteFieldCloudCapture(root, capturePath); MeasureRendererOutputs( camera, target, @@ -1098,6 +1125,11 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests renderer.enabled = false; camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = Color.black; + camera.Render(); + RenderTexture.active = target; + pixels.ReadPixels(new Rect(0, 0, target.width, target.height), 0, 0); + pixels.Apply(); + var baseline = pixels.GetPixels(); for (var index = 0; index < measuredRenderers.Count; index++) { var fact = facts[index]; @@ -1106,9 +1138,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests RenderTexture.active = target; pixels.ReadPixels(new Rect(0, 0, target.width, target.height), 0, 0); pixels.Apply(); - fact.renderedPixelCount = CountChangedPixels(pixels, Color.black); - MeasureLuminance( - pixels, + MeasureDifference( + pixels.GetPixels(), + baseline, + out fact.renderedPixelCount, out fact.renderedAverageLuminance, out fact.renderedBrightPixelCount); measuredRenderers[index].enabled = false; @@ -1123,6 +1156,32 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } } + private static void MeasureDifference( + IReadOnlyList actual, + IReadOnlyList baseline, + out int changedPixelCount, + out float averageLuminance, + out int brightPixelCount) + { + changedPixelCount = 0; + brightPixelCount = 0; + double total = 0; + var count = Math.Min(actual.Count, baseline.Count); + for (var index = 0; index < count; index++) + { + var delta = new Color( + Mathf.Abs(actual[index].r - baseline[index].r), + Mathf.Abs(actual[index].g - baseline[index].g), + Mathf.Abs(actual[index].b - baseline[index].b)); + var magnitude = delta.r + delta.g + delta.b; + if (magnitude > 0.08f) changedPixelCount++; + var luminance = delta.r * 0.2126f + delta.g * 0.7152f + delta.b * 0.0722f; + total += luminance; + if (luminance >= 0.2f) brightPixelCount++; + } + averageLuminance = count == 0 ? 0f : (float)(total / count); + } + private static int CountChangedPixels(Texture2D texture, Color background) { var changed = 0; @@ -1136,6 +1195,59 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests return changed; } + private static void WriteFieldLayerCaptures(GameObject root, EveUnityPlayableWorldProjection world, string capturePath) + { + var renderer = root.GetComponent(); + if (renderer == null || world == null || string.IsNullOrWhiteSpace(capturePath)) return; + var directory = Path.GetDirectoryName(Path.GetFullPath(capturePath)); + foreach (var field in world.FieldVolumes) + { + if (!field.Props.TryGetValue("layerBindings", out var bindings)) continue; + foreach (var pair in (bindings ?? "").Split(';')) + { + var separator = pair.IndexOf('='); + if (separator <= 0) continue; + var layerKey = pair.Substring(0, separator).Trim(); + if (!renderer.TryGetTexture(layerKey, out var texture) || texture == null) continue; + var previous = RenderTexture.active; + RenderTexture.active = texture; + var copy = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false, true); + copy.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0); + copy.Apply(); + RenderTexture.active = previous; + File.WriteAllBytes(Path.Combine(directory, $"field-{layerKey}.png"), copy.EncodeToPNG()); + UnityEngine.Object.DestroyImmediate(copy); + + RenderTexture.active = texture; + var floatCopy = new Texture2D(texture.width, texture.height, TextureFormat.RGBAFloat, false, true); + floatCopy.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0); + floatCopy.Apply(); + RenderTexture.active = previous; + File.WriteAllBytes(Path.Combine(directory, $"field-{layerKey}.exr"), floatCopy.EncodeToEXR()); + UnityEngine.Object.DestroyImmediate(floatCopy); + } + } + } + + private static void WriteFieldCloudCapture(GameObject root, string capturePath) + { + var renderer = root.GetComponent(); + var field = typeof(EveUnityFieldsVolumeRenderer).GetField( + "_cloudTexture", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var texture = field?.GetValue(renderer) as RenderTexture; + if (texture == null || string.IsNullOrWhiteSpace(capturePath)) return; + var previous = RenderTexture.active; + RenderTexture.active = texture; + var copy = new Texture2D(texture.width, texture.height, TextureFormat.RGBAFloat, false, true); + copy.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0); + copy.Apply(); + RenderTexture.active = previous; + var directory = Path.GetDirectoryName(Path.GetFullPath(capturePath)); + File.WriteAllBytes(Path.Combine(directory, "field-volume-cloud.exr"), copy.EncodeToEXR()); + UnityEngine.Object.DestroyImmediate(copy); + } + private static EveUnitySceneProviderSurfaceDocument WorldDocument() { var entities = new[] diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index a7553f4..1d48aef 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.0", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.33", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.37", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 9f23367..13f2a6f 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.33", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.37", "depth": 0, "source": "git", "dependencies": { @@ -154,7 +154,7 @@ "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "852458dade1ae535d2ff3cfc9f8fe430e341ce2a" + "hash": "5ce654800760203154132d4b0fd1475294283ba6" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index ed3531c..f39c7ed 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -461,6 +461,31 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(finalPickupEntityCount, Is.EqualTo(initialPickupEntityCount - 1), "Collected pickup remained in the provider-authored entity generation."); + targetFact = host.PresentedEntities.CurrentGeneration.Entities + .Single(entity => entity.EntityId == targetEntityId); + playerFact = host.PresentedEntities.CurrentGeneration.Entities + .Single(entity => entity.EntityId == playerId); + aim = targetFact.Position - playerFact.Position; + aim.y = 0; + Assert.That(aim.sqrMagnitude, Is.GreaterThan(0.0001f)); + aim.Normalize(); + lookVersion = runtime.ActiveVersion; + look = runtime.SubmitLookDirectionIntent(playerId, aim.x, 0f, aim.z); + lookDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < lookDeadline && + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != look.CommandId || + !string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || + runtime.ActiveVersion < runtime.LastReceipt.SourceVersion || + Vector3.Dot(FindMarker(root, playerId).transform.forward, aim) < 0.99f)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + AssertReconciledProviderReceipt(runtime.LastReceipt, look.CommandId, + provider.Selection.ProviderId, provider.Selection.SurfaceId, lookVersion); + Assert.That(Vector3.Dot(FindMarker(root, playerId).transform.forward, aim), Is.GreaterThan(0.99f)); + lookReceipt = WitnessReceipt.From("look", runtime.LastReceipt); + var focusVersion = runtime.ActiveVersion; var inputDriver = root.GetComponent() ?? root.AddComponent(); @@ -605,6 +630,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(capturePath))); File.WriteAllBytes(capturePath, pixels.EncodeToPNG()); Assert.That(new FileInfo(capturePath).Length, Is.GreaterThan(1024)); + WriteFieldLayerCaptures(root, host.Runtime.ActiveWorld, capturePath); + WriteFieldCloudCapture(root, capturePath); MeasureRendererOutputs( camera, target, @@ -1098,6 +1125,11 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests renderer.enabled = false; camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = Color.black; + camera.Render(); + RenderTexture.active = target; + pixels.ReadPixels(new Rect(0, 0, target.width, target.height), 0, 0); + pixels.Apply(); + var baseline = pixels.GetPixels(); for (var index = 0; index < measuredRenderers.Count; index++) { var fact = facts[index]; @@ -1106,9 +1138,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests RenderTexture.active = target; pixels.ReadPixels(new Rect(0, 0, target.width, target.height), 0, 0); pixels.Apply(); - fact.renderedPixelCount = CountChangedPixels(pixels, Color.black); - MeasureLuminance( - pixels, + MeasureDifference( + pixels.GetPixels(), + baseline, + out fact.renderedPixelCount, out fact.renderedAverageLuminance, out fact.renderedBrightPixelCount); measuredRenderers[index].enabled = false; @@ -1123,6 +1156,32 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } } + private static void MeasureDifference( + IReadOnlyList actual, + IReadOnlyList baseline, + out int changedPixelCount, + out float averageLuminance, + out int brightPixelCount) + { + changedPixelCount = 0; + brightPixelCount = 0; + double total = 0; + var count = Math.Min(actual.Count, baseline.Count); + for (var index = 0; index < count; index++) + { + var delta = new Color( + Mathf.Abs(actual[index].r - baseline[index].r), + Mathf.Abs(actual[index].g - baseline[index].g), + Mathf.Abs(actual[index].b - baseline[index].b)); + var magnitude = delta.r + delta.g + delta.b; + if (magnitude > 0.08f) changedPixelCount++; + var luminance = delta.r * 0.2126f + delta.g * 0.7152f + delta.b * 0.0722f; + total += luminance; + if (luminance >= 0.2f) brightPixelCount++; + } + averageLuminance = count == 0 ? 0f : (float)(total / count); + } + private static int CountChangedPixels(Texture2D texture, Color background) { var changed = 0; @@ -1136,6 +1195,59 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests return changed; } + private static void WriteFieldLayerCaptures(GameObject root, EveUnityPlayableWorldProjection world, string capturePath) + { + var renderer = root.GetComponent(); + if (renderer == null || world == null || string.IsNullOrWhiteSpace(capturePath)) return; + var directory = Path.GetDirectoryName(Path.GetFullPath(capturePath)); + foreach (var field in world.FieldVolumes) + { + if (!field.Props.TryGetValue("layerBindings", out var bindings)) continue; + foreach (var pair in (bindings ?? "").Split(';')) + { + var separator = pair.IndexOf('='); + if (separator <= 0) continue; + var layerKey = pair.Substring(0, separator).Trim(); + if (!renderer.TryGetTexture(layerKey, out var texture) || texture == null) continue; + var previous = RenderTexture.active; + RenderTexture.active = texture; + var copy = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false, true); + copy.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0); + copy.Apply(); + RenderTexture.active = previous; + File.WriteAllBytes(Path.Combine(directory, $"field-{layerKey}.png"), copy.EncodeToPNG()); + UnityEngine.Object.DestroyImmediate(copy); + + RenderTexture.active = texture; + var floatCopy = new Texture2D(texture.width, texture.height, TextureFormat.RGBAFloat, false, true); + floatCopy.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0); + floatCopy.Apply(); + RenderTexture.active = previous; + File.WriteAllBytes(Path.Combine(directory, $"field-{layerKey}.exr"), floatCopy.EncodeToEXR()); + UnityEngine.Object.DestroyImmediate(floatCopy); + } + } + } + + private static void WriteFieldCloudCapture(GameObject root, string capturePath) + { + var renderer = root.GetComponent(); + var field = typeof(EveUnityFieldsVolumeRenderer).GetField( + "_cloudTexture", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var texture = field?.GetValue(renderer) as RenderTexture; + if (texture == null || string.IsNullOrWhiteSpace(capturePath)) return; + var previous = RenderTexture.active; + RenderTexture.active = texture; + var copy = new Texture2D(texture.width, texture.height, TextureFormat.RGBAFloat, false, true); + copy.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0); + copy.Apply(); + RenderTexture.active = previous; + var directory = Path.GetDirectoryName(Path.GetFullPath(capturePath)); + File.WriteAllBytes(Path.Combine(directory, "field-volume-cloud.exr"), copy.EncodeToEXR()); + UnityEngine.Object.DestroyImmediate(copy); + } + private static EveUnitySceneProviderSurfaceDocument WorldDocument() { var entities = new[] diff --git a/TestProject/Packages/packages-lock.json b/TestProject/Packages/packages-lock.json index 1f1ebaf..eb007d8 100644 --- a/TestProject/Packages/packages-lock.json +++ b/TestProject/Packages/packages-lock.json @@ -33,7 +33,9 @@ "version": "file:../../../EvePlugins/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields", "depth": 0, "source": "local", - "dependencies": {} + "dependencies": { + "org.gamecult.cultlib": "1.0.13" + } }, "org.gamecult.eve.surface": { "version": "file:../../packages/org.gamecult.eve.surface", @@ -49,7 +51,7 @@ "source": "local", "dependencies": { "org.gamecult.cultlib": "1.0.13", - "org.gamecult.eve.plugin-fields": "0.1.1", + "org.gamecult.eve.plugin-fields": "0.2.0", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 475d987..441c8ae 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,9 +4,12 @@ The warm-cache released-package witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.32`, commit - `817912a041ac1911e1e27a4b88617754d90ad4b7`; -- `org.gamecult.eve.surface` `0.2.2`, from the same commit; +- `org.gamecult.eve.unity-scene` `0.3.37`, commit + `5ce654800760203154132d4b0fd1475294283ba6`; +- `org.gamecult.eve.plugin-fields` `0.2.0`, commit + `382a23d8f8a07e4b5eef5f81f84655861a858367`; +- `org.gamecult.eve.surface` `0.2.2`, commit + `140e1bd963a0033e66777a3b2c5fe6e9c97dfe32`; - `org.gamecult.eve.unity-uitoolkit` `0.1.1`, commit `4d0cbe0185bdc4fc65eb63503a7c5cb578539669`; - `org.gamecult.cultlib` `1.0.13`, commit @@ -14,7 +17,7 @@ The warm-cache released-package witness passes with: - the generic `ReleaseConsumerProject` client connected directly to the Aetheria daemon. -Evidence is in `artifacts/aetheria-daemon-native-0326-warm`. The PlayMode test +Evidence is in `artifacts/aetheria-daemon-gravity-fog-warm`. The PlayMode test passed and recorded provider-owned reconciled movement, look, targeting, tractor press, tractor release, contact-gated cargo collection, and action receipts. The daemon-owned look direction reached the SoA body rotation, @@ -37,6 +40,14 @@ with `display: none` in the pilot presentation. The runnable generic-client launcher mounts the same advertised document and forwards its typed commands; it contains no Aetheria gameplay or client-authored world light. +The same surface publishes a `field.volume3d` node over four canonical Eve +Fields splat layers. Its surface contract contains logical ports such as +`surfaceHeight`, `patch`, and `tint`; it contains no Unity `_Nebula*` property +names or pass indices. The selected `unity-scene` provider asset variant owns +that concrete shader ABI through `unity.volume.*` metadata. The released +generic lowerer resolved the provider shader and dither texture, rasterized all +four layers, and composited `22` pilot-camera frames from daemon frame `220`. + The two PNGs below are deliberate camera-channel captures, not a composited screen capture, so they prove world/map isolation rather than HUD pixels. The native cockpit proof is the asserted UI Toolkit visual tree recorded as @@ -46,11 +57,11 @@ daemon lock progress `1.0`; Unity does not manufacture or smooth that value. Camera-channel facts: - provider-authored player renderers: `13`, including the lowered tractor effect; -- pilot changed pixels: `230,302`; -- pilot average luminance: `0.002843`; -- pilot bright pixels: `1,040`; +- pilot changed pixels: `230,323`; +- pilot average luminance: `0.0532403`; +- pilot bright pixels: `954`; - map-channel renderers: `11`; -- map changed pixels: `3,921`; +- map changed pixels: `4,115`; - native cockpit progress bars: `7`; - daemon tractor power at capture: `1.0` after the held input completed its authored ramp; @@ -66,12 +77,12 @@ Camera-channel facts: - the map camera renders exactly the advertised map layer. Visual inspection confirms that map glyphs are absent from the pilot frame and -present in the map-only frame. Restoring the authored ARPG camera in place of -the copied TestScene tuning increased hull-renderer pixel coverage by roughly -four times and bright pilot pixels from `502` to `1,319`. The pilot presentation -remains dark. Renderer-isolation facts prove that the hull draws under the -provider-owned light, so the next native-art work is available pre-generated -provider textures, not a client-side lighting fallback. +present in the map-only frame. The pilot frame also shows the provider-owned +gravity-shaped fog basin rather than a client-authored background effect. Its +current dark, broad shape is transport/lowering proof, not visual-parity proof; +restoring the fossil's cloud structure, scale, tint range, and scene readability +remains provider art work. EveUnity must not compensate with Aetheria-specific +lighting or fog rules. Substance is not part of this path; later texture baking belongs in Blender. Aetheria currently bundles ambientCG's 1K `Metal012` color, normal, and metalness maps under CC0. The same pre-generated maps now replace dead @@ -112,8 +123,8 @@ Primary artifacts: ## Cold delivery is not currently proven -The current `46,131,569` byte Unity bundle (SHA-256 -`a4b26fb234ae7d82eda26fb342d7f77a236e3bc1107420175eb1c626785db212`) +The current `46,413,003` byte Unity bundle (SHA-256 +`229a7833120d9b4fe7e4270f13adc8ce8e553246a99efcbf20bb50d3ca18494d`) times out from an empty cache. Its bytes are still transported through batched snapshot records rather than the intended mapped/network body transport. Increasing the Unity timeout would hide the diff --git a/docs/authority-map.md b/docs/authority-map.md index 635cd36..d55e25c 100644 --- a/docs/authority-map.md +++ b/docs/authority-map.md @@ -17,6 +17,7 @@ drivers, asset-provider hooks, package release, runtime tests, and captures. - `gamecult.eve.provider_advertisement.v1` - `gamecult.eve.surface.v1` - provider asset-manifest documents +- runtime-selected asset-variant metadata, including native rendering-program bindings - provider command receipts - semantic render-channel inclusion and exclusion policy - Unity input and local presentation assets @@ -31,15 +32,18 @@ drivers, asset-provider hooks, package release, runtime tests, and captures. ## Derived State `ActiveWorld`, scene objects, transforms, markers, camera pose, native -render-channel layer mappings, asset caches, presentation counters, and receipt -display state are projections. They are not world truth. +render-channel layer mappings, resolved native shader ports/passes, asset caches, +presentation counters, and receipt display state are projections. They are not +world truth. ## Forbidden Writers Unity transforms, input drivers, camera rigs, scene sinks, asset caches, and pending receipts may not mutate or simulate provider world state. Plugin projection shells may not implement Sai, Norn, or TeX semantics. Portable -surfaces may not carry Unity layer numbers or camera culling masks. +surfaces may not carry Unity layer numbers, camera culling masks, shader property +names, shader keywords, or native pass indices. Those bindings belong to the +selected runtime asset variant. ## Shared Paths diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index b412222..492bea3 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,8 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "817912a041ac1911e1e27a4b88617754d90ad4b7" +$expectedEveUnityCommit = "5ce654800760203154132d4b0fd1475294283ba6" +$expectedEveFieldsCommit = "382a23d8f8a07e4b5eef5f81f84655861a858367" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot @@ -118,8 +119,27 @@ if (-not $SkipAssetBundleBuild) { } } +$daemonBuildLogPath = Join-Path $outputRoot "aetheria-daemon-build.log" +$daemonBuildErrorLogPath = Join-Path $outputRoot "aetheria-daemon-build.error.log" +$daemonBuildArguments = @( + "build", $daemonProject, + "-p:CultLibRoot=$CultLibRoot", "-p:EveUnityRoot=$EveUnityRoot", "-p:YmirRoot=$YmirRoot" +) +$previousErrorActionPreference = $ErrorActionPreference +try { + $ErrorActionPreference = "Continue" + & dotnet @daemonBuildArguments 1> $daemonBuildLogPath 2> $daemonBuildErrorLogPath + $daemonBuildExitCode = $LASTEXITCODE +} +finally { + $ErrorActionPreference = $previousErrorActionPreference +} +if ($daemonBuildExitCode -ne 0) { + throw "Aetheria daemon prebuild failed with exit code $daemonBuildExitCode. See $daemonBuildErrorLogPath" +} + $daemonArguments = @( - "run", "--project", $daemonProject, + "run", "--project", $daemonProject, "--no-build", "-p:CultLibRoot=$CultLibRoot", "-p:EveUnityRoot=$EveUnityRoot", "-p:YmirRoot=$YmirRoot", "--", "--root", $AetheriaRoot, @@ -204,17 +224,19 @@ try { $releaseLock = Get-Content -LiteralPath (Join-Path $repoRoot "ReleaseConsumerProject\Packages\packages-lock.json") -Raw | ConvertFrom-Json $releasedPackageClient = $projectRoot -eq "ReleaseConsumerProject" $sceneLock = $releaseLock.dependencies.'org.gamecult.eve.unity-scene' + $fieldsLock = $releaseLock.dependencies.'org.gamecult.eve.plugin-fields' $uiToolkitLock = $releaseLock.dependencies.'org.gamecult.eve.unity-uitoolkit' $cultLibLock = $releaseLock.dependencies.'org.gamecult.cultlib' if (-not $releasedPackageClient) { throw "Released witness must run ReleaseConsumerProject, got '$projectRoot'." } - if ($sceneLock.source -ne "git" -or $uiToolkitLock.source -ne "git" -or $cultLibLock.source -ne "git" -or - $sceneLock.version -like "file:*" -or $uiToolkitLock.version -like "file:*" -or $cultLibLock.version -like "file:*") { + if ($sceneLock.source -ne "git" -or $fieldsLock.source -ne "git" -or $uiToolkitLock.source -ne "git" -or $cultLibLock.source -ne "git" -or + $sceneLock.version -like "file:*" -or $fieldsLock.version -like "file:*" -or $uiToolkitLock.version -like "file:*" -or $cultLibLock.version -like "file:*") { throw "Released witness resolved a non-git/local package dependency." } if ($sceneLock.hash -ne $expectedEveUnityCommit -or + $fieldsLock.hash -ne $expectedEveFieldsCommit -or $uiToolkitLock.hash -ne $expectedEveUnityUiToolkitCommit -or $cultLibLock.hash -ne $expectedCultLibCommit) { - throw "Released package commits do not match the witnessed releases. scene=$($sceneLock.hash) uitoolkit=$($uiToolkitLock.hash) cultlib=$($cultLibLock.hash)" + throw "Released package commits do not match the witnessed releases. scene=$($sceneLock.hash) fields=$($fieldsLock.hash) uitoolkit=$($uiToolkitLock.hash) cultlib=$($cultLibLock.hash)" } $finalBodies = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.body -File -Recurse -ErrorAction SilentlyContinue) $finalPartials = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.partial -File -Recurse -ErrorAction SilentlyContinue) @@ -234,6 +256,7 @@ try { $facts | Add-Member -NotePropertyName releasedPackageClient -NotePropertyValue $releasedPackageClient $facts | Add-Member -NotePropertyName clientProject -NotePropertyValue $projectRoot $facts | Add-Member -NotePropertyName eveUnityPackageCommit -NotePropertyValue $sceneLock.hash + $facts | Add-Member -NotePropertyName eveFieldsPackageCommit -NotePropertyValue $fieldsLock.hash $facts | Add-Member -NotePropertyName eveUnityUiToolkitPackageCommit -NotePropertyValue $uiToolkitLock.hash $facts | Add-Member -NotePropertyName cultLibPackageCommit -NotePropertyValue $cultLibLock.hash $facts | Add-Member -NotePropertyName contentDelivery -NotePropertyValue ([ordered]@{ From de580fe88350f422e30f1e4b64ded5c33070b118 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 20:59:30 +0100 Subject: [PATCH 082/292] Lower temporal field volumes generically --- docs/authority-map.md | 10 +- .../Fields/EveUnityFieldsVolumeRenderer.cs | 152 +++++++++++++++--- .../EveFieldsUnitySceneProjectionTests.cs | 44 +++++ .../eve-runtime-capability.json | 7 +- .../org.gamecult.eve.unity-scene/package.json | 2 +- 5 files changed, 188 insertions(+), 27 deletions(-) diff --git a/docs/authority-map.md b/docs/authority-map.md index d55e25c..17e065d 100644 --- a/docs/authority-map.md +++ b/docs/authority-map.md @@ -18,6 +18,7 @@ drivers, asset-provider hooks, package release, runtime tests, and captures. - `gamecult.eve.surface.v1` - provider asset-manifest documents - runtime-selected asset-variant metadata, including native rendering-program bindings +- generic field-volume lifecycle declarations and logical texture/scalar/matrix ports - provider command receipts - semantic render-channel inclusion and exclusion policy - Unity input and local presentation assets @@ -33,8 +34,10 @@ drivers, asset-provider hooks, package release, runtime tests, and captures. `ActiveWorld`, scene objects, transforms, markers, camera pose, native render-channel layer mappings, resolved native shader ports/passes, asset caches, +per-volume raymarch targets, temporal-history targets, previous camera matrices, presentation counters, and receipt display state are projections. They are not -world truth. +world truth. Temporal targets reset when the volume program, node, or render size +changes and can never become provider state. ## Forbidden Writers @@ -43,7 +46,10 @@ pending receipts may not mutate or simulate provider world state. Plugin projection shells may not implement Sai, Norn, or TeX semantics. Portable surfaces may not carry Unity layer numbers, camera culling masks, shader property names, shader keywords, or native pass indices. Those bindings belong to the -selected runtime asset variant. +selected runtime asset variant. A native volume program that advertises a +temporal pass must also advertise its current-sample, history, +previous-view-projection, and reset-history ports; partial temporal programs fail +closed. ## Shared Paths diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs index 37aeb7f..2509849 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -20,13 +20,17 @@ namespace GameCult.Eve.UnityScene.Fields private Material? _material; private Shader? _sourceShader; private IReadOnlyDictionary? _programMetadata; - private RenderTexture? _cloudTexture; + private RenderTexture? _raymarchTexture; + private readonly RenderTexture?[] _historyTextures = new RenderTexture?[2]; + private int _historyTextureIndex = -1; + private Matrix4x4 _previousViewProjection; private CommandBuffer? _commands; private string _activeNodeId = ""; public long PresentedFrameId { get; private set; } = -1; public int PresentedLayerCount => _targets.Count(target => target.TargetTexture != null); public long CompositeCount { get; private set; } + public bool UsesTemporalHistory => HasTemporalProgram(); public void Bind(EveUnityPlayableWorldClientHost host, IEveUnityFieldsSplatsDocumentSource? source) { @@ -84,28 +88,58 @@ namespace GameCult.Eve.UnityScene.Fields if (field == null || !EnsureMaterial(field)) return; ApplyFieldBindings(field, _document); camera.depthTextureMode |= DepthTextureMode.Depth; - EnsureCloudTexture(camera, field); - if (_cloudTexture == null) return; + EnsureVolumeTextures(camera, field); + if (_raymarchTexture == null) return; var gpuProjection = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true); - SetMatrixPort("cameraInverseViewProjection", (gpuProjection * camera.worldToCameraMatrix).inverse); + var viewProjection = gpuProjection * camera.worldToCameraMatrix; + SetMatrixPort("cameraInverseViewProjection", viewProjection.inverse); SetVectorPort("cameraProjectionExtents", ProjectionExtents(camera)); SetFloatPort("raymarchOffset", Halton(Time.frameCount & 1023, 3)); var raymarchPass = ProgramPass("raymarch"); var compositePass = ProgramPass("composite"); - SetTexturePort("cloud", _cloudTexture); + var temporalPass = ProgramPass("temporal"); + var useTemporalHistory = temporalPass >= 0; + var historyWriteIndex = -1; + RenderTexture finalCloudTexture = _raymarchTexture; + + if (useTemporalHistory) + { + historyWriteIndex = _historyTextureIndex < 0 ? 0 : (_historyTextureIndex + 1) % _historyTextures.Length; + var historyReadTexture = _historyTextureIndex < 0 + ? _raymarchTexture + : _historyTextures[_historyTextureIndex]; + finalCloudTexture = _historyTextures[historyWriteIndex]!; + SetTexturePort("currentSample", _raymarchTexture); + SetTexturePort("history", historyReadTexture!); + SetMatrixPort("previousViewProjection", _historyTextureIndex < 0 ? viewProjection : _previousViewProjection); + SetFloatPort("resetHistory", _historyTextureIndex < 0 ? 1f : 0f); + } + SetTexturePort("cloud", finalCloudTexture); _commands ??= new CommandBuffer { name = "Eve Fields Volume" }; _commands.Clear(); - _commands.SetRenderTarget(_cloudTexture); - _commands.SetViewport(new Rect(0f, 0f, _cloudTexture.width, _cloudTexture.height)); + _commands.SetRenderTarget(_raymarchTexture); + _commands.SetViewport(new Rect(0f, 0f, _raymarchTexture.width, _raymarchTexture.height)); _commands.ClearRenderTarget(false, true, Color.clear); _commands.DrawProcedural(Matrix4x4.identity, _material!, raymarchPass, MeshTopology.Triangles, 3, 1); + if (useTemporalHistory) + { + _commands.SetRenderTarget(finalCloudTexture); + _commands.SetViewport(new Rect(0f, 0f, finalCloudTexture.width, finalCloudTexture.height)); + _commands.ClearRenderTarget(false, true, Color.clear); + _commands.DrawProcedural(Matrix4x4.identity, _material!, temporalPass, MeshTopology.Triangles, 3, 1); + } _commands.SetRenderTarget(BuiltinRenderTextureType.CameraTarget); _commands.SetViewport(camera.pixelRect); _commands.DrawProcedural(Matrix4x4.identity, _material!, compositePass, MeshTopology.Triangles, 3, 1); Graphics.ExecuteCommandBuffer(_commands); + if (useTemporalHistory) + { + _historyTextureIndex = historyWriteIndex; + _previousViewProjection = viewProjection; + } CompositeCount++; } @@ -126,12 +160,7 @@ namespace GameCult.Eve.UnityScene.Fields !metadataProvider.TryResolveAssetMetadata(shaderBinding, out var metadata)) return false; _programMetadata = metadata; - if (ProgramPass("raymarch") < 0 || ProgramPass("composite") < 0 || - string.IsNullOrWhiteSpace(ProgramPort("texture", "cloud")) || - string.IsNullOrWhiteSpace(ProgramPort("matrix", "cameraInverseViewProjection")) || - string.IsNullOrWhiteSpace(ProgramPort("vector", "cameraProjectionExtents")) || - string.IsNullOrWhiteSpace(ProgramPort("vector", "viewportTransform")) || - string.IsNullOrWhiteSpace(ProgramPort("float", "raymarchOffset"))) + if (!TryValidateProgramMetadata(metadata, out _)) return false; if (_material != null && ReferenceEquals(shader, _sourceShader) && string.Equals(_activeNodeId, field.NodeId, StringComparison.Ordinal)) return true; @@ -139,6 +168,7 @@ namespace GameCult.Eve.UnityScene.Fields _sourceShader = shader; _activeNodeId = field.NodeId; _material = new Material(shader) { hideFlags = HideFlags.DontSave }; + ResetTemporalHistory(); foreach (var parameter in ParseBindings(Prop(field, "floatParameters"))) { if (TryFloat(parameter.Value, out var value)) SetFloatPort(parameter.Key, value); @@ -203,32 +233,108 @@ namespace GameCult.Eve.UnityScene.Fields if (_layers == null) _layers = gameObject.AddComponent(); } - private void EnsureCloudTexture(Camera camera, EveUnityFieldVolumeProjection field) + private void EnsureVolumeTextures(Camera camera, EveUnityFieldVolumeProjection field) { var downsample = Mathf.Clamp(NonNegativeInt(field, "downsample", 1), 0, 3); var width = Mathf.Max(1, camera.pixelWidth >> downsample); var height = Mathf.Max(1, camera.pixelHeight >> downsample); - if (_cloudTexture != null && _cloudTexture.width == width && _cloudTexture.height == height) return; - if (_cloudTexture != null) { _cloudTexture.Release(); Destroy(_cloudTexture); } - _cloudTexture = new RenderTexture(width, height, 0, RenderTextureFormat.ARGBFloat) + var resetHistory = EnsureRenderTexture(ref _raymarchTexture, width, height, "Eve Fields Volume Raymarch"); + if (HasTemporalProgram()) { - name = "Eve Fields Volume", - filterMode = FilterMode.Bilinear, - wrapMode = TextureWrapMode.Clamp - }; - _cloudTexture.Create(); + resetHistory |= EnsureRenderTexture(ref _historyTextures[0], width, height, "Eve Fields Volume History A"); + resetHistory |= EnsureRenderTexture(ref _historyTextures[1], width, height, "Eve Fields Volume History B"); + } + else + { + ReleaseRenderTexture(ref _historyTextures[0]); + ReleaseRenderTexture(ref _historyTextures[1]); + resetHistory = true; + } + if (resetHistory) ResetTemporalHistory(); } private void ReleaseRenderState() { if (_commands != null) { _commands.Release(); _commands = null; } - if (_cloudTexture != null) { _cloudTexture.Release(); Destroy(_cloudTexture); _cloudTexture = null; } + ReleaseRenderTexture(ref _raymarchTexture); + ReleaseRenderTexture(ref _historyTextures[0]); + ReleaseRenderTexture(ref _historyTextures[1]); if (_material != null) { Destroy(_material); _material = null; } _sourceShader = null; _programMetadata = null; _activeNodeId = ""; PresentedFrameId = -1; CompositeCount = 0; + ResetTemporalHistory(); + } + + private void ResetTemporalHistory() + { + _historyTextureIndex = -1; + _previousViewProjection = Matrix4x4.identity; + } + + private bool HasTemporalProgram() => ProgramPass("temporal") >= 0; + + private static bool EnsureRenderTexture(ref RenderTexture? texture, int width, int height, string name) + { + if (texture != null && texture.width == width && texture.height == height) return false; + ReleaseRenderTexture(ref texture); + texture = new RenderTexture(width, height, 0, RenderTextureFormat.ARGBFloat) + { + name = name, + filterMode = FilterMode.Bilinear, + wrapMode = TextureWrapMode.Clamp + }; + texture.Create(); + return true; + } + + private static void ReleaseRenderTexture(ref RenderTexture? texture) + { + if (texture == null) return; + texture.Release(); + Destroy(texture); + texture = null; + } + + public static bool TryValidateProgramMetadata( + IReadOnlyDictionary? metadata, + out string error) + { + error = ""; + if (metadata == null) return Fail("Native volume program metadata is missing.", out error); + if (!ValidPass(metadata, "raymarch")) return Fail("Native volume raymarch pass is missing.", out error); + if (!ValidPass(metadata, "composite")) return Fail("Native volume composite pass is missing.", out error); + if (!ValidPort(metadata, "texture", "cloud") || + !ValidPort(metadata, "matrix", "cameraInverseViewProjection") || + !ValidPort(metadata, "vector", "cameraProjectionExtents") || + !ValidPort(metadata, "vector", "viewportTransform") || + !ValidPort(metadata, "float", "raymarchOffset")) + return Fail("Native volume program is missing a required raymarch or composite port.", out error); + + if (!metadata.ContainsKey("unity.volume.pass.temporal")) return true; + if (!ValidPass(metadata, "temporal")) return Fail("Native volume temporal pass is invalid.", out error); + if (!ValidPort(metadata, "texture", "currentSample") || + !ValidPort(metadata, "texture", "history") || + !ValidPort(metadata, "matrix", "previousViewProjection") || + !ValidPort(metadata, "float", "resetHistory")) + return Fail("Native volume temporal pass is missing a required history port.", out error); + return true; + } + + private static bool ValidPass(IReadOnlyDictionary metadata, string pass) => + metadata.TryGetValue($"unity.volume.pass.{pass}", out var value) && + int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) && parsed >= 0; + + private static bool ValidPort(IReadOnlyDictionary metadata, string kind, string port) => + metadata.TryGetValue($"unity.volume.{kind}Port.{port}", out var value) && + !string.IsNullOrWhiteSpace(value); + + private static bool Fail(string message, out string error) + { + error = message; + return false; } private void SetMatrixPort(string port, Matrix4x4 value) diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index 0cbb1a8..25fd08b 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -41,6 +41,38 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(typeof(IEveUnityFieldsSplatsDocumentSource).GetEvent("FieldsSplatsAvailable"), Is.Not.Null); } + [Test] + public void VolumeProgramAcceptsPortableTwoPassLifecycle() + { + var metadata = RequiredVolumeProgramMetadata(); + + Assert.That(EveUnityFieldsVolumeRenderer.TryValidateProgramMetadata(metadata, out var error), Is.True, error); + } + + [Test] + public void VolumeProgramAcceptsCompleteTemporalLifecycle() + { + var metadata = RequiredVolumeProgramMetadata(); + metadata["unity.volume.pass.temporal"] = "1"; + metadata["unity.volume.texturePort.currentSample"] = "_CurrentVolume"; + metadata["unity.volume.texturePort.history"] = "_HistoryVolume"; + metadata["unity.volume.matrixPort.previousViewProjection"] = "_PreviousViewProjection"; + metadata["unity.volume.floatPort.resetHistory"] = "_ResetHistory"; + + Assert.That(EveUnityFieldsVolumeRenderer.TryValidateProgramMetadata(metadata, out var error), Is.True, error); + } + + [Test] + public void VolumeProgramRejectsPartialTemporalLifecycle() + { + var metadata = RequiredVolumeProgramMetadata(); + metadata["unity.volume.pass.temporal"] = "1"; + metadata["unity.volume.texturePort.currentSample"] = "_CurrentVolume"; + + Assert.That(EveUnityFieldsVolumeRenderer.TryValidateProgramMetadata(metadata, out var error), Is.False); + Assert.That(error, Does.Contain("history port")); + } + [Test] public void PackageContainsGenericFieldsShader() { @@ -90,6 +122,18 @@ namespace GameCult.Eve.UnityScene.Tests public IEveFieldsSplatSoa Splats { get; } = new CaptureSplats(); } + private static Dictionary RequiredVolumeProgramMetadata() => + new Dictionary(StringComparer.Ordinal) + { + ["unity.volume.pass.raymarch"] = "0", + ["unity.volume.pass.composite"] = "2", + ["unity.volume.texturePort.cloud"] = "_Cloud", + ["unity.volume.matrixPort.cameraInverseViewProjection"] = "_CameraInverseViewProjection", + ["unity.volume.vectorPort.cameraProjectionExtents"] = "_CameraProjectionExtents", + ["unity.volume.vectorPort.viewportTransform"] = "_ViewportTransform", + ["unity.volume.floatPort.raymarchOffset"] = "_RaymarchOffset" + }; + private sealed class CaptureViewport : IEveFieldsViewport { public double MinX => -1; diff --git a/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json b/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json index a7a7dd1..8603e65 100644 --- a/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json +++ b/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json @@ -46,12 +46,17 @@ "pluginId": "fields.surface", "capabilities": [ "field.surface2d", - "gravity.surface" + "gravity.surface", + "field.volume3d", + "volume.raymarch", + "volume.temporal-history", + "volume.composite" ], "projectionAdapter": "EveFieldsSplatRasterizer", "evidencePaths": [ "Runtime/Fields/EveFieldsSplatRasterizer.cs", "Runtime/Fields/EveFieldsSplatLayerRenderer.cs", + "Runtime/Fields/EveUnityFieldsVolumeRenderer.cs", "Tests/Editor/EveFieldsUnitySceneProjectionTests.cs", "artifacts/fields-surface/latest/unity-fields.png" ] diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index a70e5fd..ea0ecf5 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.37", + "version": "0.3.38", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 2a96e82e7ab308f36833bc3de1bce02f82ef7b2d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 21:01:19 +0100 Subject: [PATCH 083/292] Pin scene consumer to temporal volume release --- ReleaseConsumerProject/Packages/manifest.json | 2 +- ReleaseConsumerProject/Packages/packages-lock.json | 4 ++-- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 1d48aef..f214dae 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.0", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.37", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.38", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 13f2a6f..c08b7c2 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.37", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.38", "depth": 0, "source": "git", "dependencies": { @@ -154,7 +154,7 @@ "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "5ce654800760203154132d4b0fd1475294283ba6" + "hash": "de580fe88350f422e30f1e4b64ded5c33070b118" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 492bea3..6e06b9a 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "5ce654800760203154132d4b0fd1475294283ba6" +$expectedEveUnityCommit = "de580fe88350f422e30f1e4b64ded5c33070b118" $expectedEveFieldsCommit = "382a23d8f8a07e4b5eef5f81f84655861a858367" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" From eddcd4a9f914c0be6ad5c94280dbd85596af0fcf Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 21:09:08 +0100 Subject: [PATCH 084/292] Require current bundle for warm witness --- docs/aetheria-released-package-witness.md | 33 ++++++++++--------- scripts/run-aetheria-daemon-world-witness.ps1 | 13 +++++++- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 441c8ae..0bc6f5a 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,8 +4,8 @@ The warm-cache released-package witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.37`, commit - `5ce654800760203154132d4b0fd1475294283ba6`; +- `org.gamecult.eve.unity-scene` `0.3.38`, commit + `de580fe88350f422e30f1e4b64ded5c33070b118`; - `org.gamecult.eve.plugin-fields` `0.2.0`, commit `382a23d8f8a07e4b5eef5f81f84655861a858367`; - `org.gamecult.eve.surface` `0.2.2`, commit @@ -46,7 +46,10 @@ Fields splat layers. Its surface contract contains logical ports such as names or pass indices. The selected `unity-scene` provider asset variant owns that concrete shader ABI through `unity.volume.*` metadata. The released generic lowerer resolved the provider shader and dither texture, rasterized all -four layers, and composited `22` pilot-camera frames from daemon frame `220`. +four layers, and composited `22` pilot-camera frames from daemon frame `224`. +The provider variant advertises the fossil shader's raymarch, temporal-history, +and composite passes plus their logical ports. EveUnity allocates and resets the +history targets generically; a partial temporal ABI fails closed. The two PNGs below are deliberate camera-channel captures, not a composited screen capture, so they prove world/map isolation rather than HUD pixels. The @@ -57,11 +60,11 @@ daemon lock progress `1.0`; Unity does not manufacture or smooth that value. Camera-channel facts: - provider-authored player renderers: `13`, including the lowered tractor effect; -- pilot changed pixels: `230,323`; -- pilot average luminance: `0.0532403`; -- pilot bright pixels: `954`; +- pilot changed pixels: `230,310`; +- pilot average luminance: `0.0524399`; +- pilot bright pixels: `1,731`; - map-channel renderers: `11`; -- map changed pixels: `4,115`; +- map changed pixels: `4,937`; - native cockpit progress bars: `7`; - daemon tractor power at capture: `1.0` after the held input completed its authored ramp; @@ -77,12 +80,12 @@ Camera-channel facts: - the map camera renders exactly the advertised map layer. Visual inspection confirms that map glyphs are absent from the pilot frame and -present in the map-only frame. The pilot frame also shows the provider-owned -gravity-shaped fog basin rather than a client-authored background effect. Its -current dark, broad shape is transport/lowering proof, not visual-parity proof; -restoring the fossil's cloud structure, scale, tint range, and scene readability -remains provider art work. EveUnity must not compensate with Aetheria-specific -lighting or fog rules. +present in the map-only frame. The volume lifecycle is active, but the pilot +frame remains nearly black and geometric; it does not yet reproduce the fossil's +blue gravity-shaped fog sea. This is transport and lowering proof, not visual +parity. The next provider-side diagnosis must inspect published tint/height +field values, exposure/composition, camera depth, and the fossil tuning. EveUnity +must not compensate with Aetheria-specific lighting or fog rules. Substance is not part of this path; later texture baking belongs in Blender. Aetheria currently bundles ambientCG's 1K `Metal012` color, normal, and metalness maps under CC0. The same pre-generated maps now replace dead @@ -123,8 +126,8 @@ Primary artifacts: ## Cold delivery is not currently proven -The current `46,413,003` byte Unity bundle (SHA-256 -`229a7833120d9b4fe7e4270f13adc8ce8e553246a99efcbf20bb50d3ca18494d`) +The current `46,412,252` byte Unity bundle (SHA-256 +`bc45d84b54666e813bfccca12496ba4a3533ed08896dfc57de726749574fa796`) times out from an empty cache. Its bytes are still transported through batched snapshot records rather than the intended mapped/network body transport. Increasing the Unity timeout would hide the diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 6e06b9a..23f2f5d 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -32,6 +32,7 @@ $witnessPath = Join-Path $outputRoot "runtime-witness.json" $replicaPath = Join-Path $outputRoot "eve-unity-replica.cc" $assetCachePath = Join-Path $outputRoot "asset-cache" $statePath = Join-Path $outputRoot "aetheria-witness-state.cc" +$providerBundlePath = Join-Path $AetheriaRoot "Build\EveAssets\StandaloneWindows64\aetheria-world" $daemonProject = Join-Path $AetheriaRoot "Aetheria.State.Daemon\Aetheria.State.Daemon.csproj" $importProject = Join-Path $AetheriaRoot "Aetheria.State.Import\Aetheria.State.Import.csproj" @@ -118,6 +119,17 @@ if (-not $SkipAssetBundleBuild) { throw "Aetheria AssetBundle build failed with exit code $($bundleBuilder.ExitCode)" } } +if ($CacheState -eq "warm") { + if (-not (Test-Path -LiteralPath $providerBundlePath)) { + throw "Provider-owned Aetheria bundle is missing: $providerBundlePath" + } + $currentBundleHash = (Get-FileHash -LiteralPath $providerBundlePath -Algorithm SHA256).Hash.ToLowerInvariant() + $currentWarmBody = Get-ChildItem -LiteralPath $assetCachePath -Filter "$currentBundleHash.body" -File -Recurse -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($null -eq $currentWarmBody) { + throw "Warm witness cache does not contain the current provider bundle. expected=$currentBundleHash cache=$assetCachePath" + } +} $daemonBuildLogPath = Join-Path $outputRoot "aetheria-daemon-build.log" $daemonBuildErrorLogPath = Join-Path $outputRoot "aetheria-daemon-build.error.log" @@ -240,7 +252,6 @@ try { } $finalBodies = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.body -File -Recurse -ErrorAction SilentlyContinue) $finalPartials = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.partial -File -Recurse -ErrorAction SilentlyContinue) - $providerBundlePath = Join-Path $AetheriaRoot "Build\EveAssets\StandaloneWindows64\aetheria-world" if (-not (Test-Path -LiteralPath $providerBundlePath)) { throw "Provider-owned Aetheria bundle is missing: $providerBundlePath" } From b19ac8cc6c6c047a3389bee9ae47d0818ca45288 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 21:15:06 +0100 Subject: [PATCH 085/292] Capture volume render stages in live witness --- .../PlayMode/GenericWorldCaptureTests.cs | 33 +++++++++++++++---- .../PlayMode/GenericWorldCaptureTests.cs | 33 +++++++++++++++---- scripts/run-aetheria-daemon-world-witness.ps1 | 2 ++ 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index f39c7ed..75f12a9 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -1232,19 +1232,40 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests private static void WriteFieldCloudCapture(GameObject root, string capturePath) { var renderer = root.GetComponent(); - var field = typeof(EveUnityFieldsVolumeRenderer).GetField( - "_cloudTexture", + if (renderer == null || string.IsNullOrWhiteSpace(capturePath)) return; + var raymarchField = typeof(EveUnityFieldsVolumeRenderer).GetField( + "_raymarchTexture", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); - var texture = field?.GetValue(renderer) as RenderTexture; - if (texture == null || string.IsNullOrWhiteSpace(capturePath)) return; + var historyTexturesField = typeof(EveUnityFieldsVolumeRenderer).GetField( + "_historyTextures", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var historyIndexField = typeof(EveUnityFieldsVolumeRenderer).GetField( + "_historyTextureIndex", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var raymarchTexture = raymarchField?.GetValue(renderer) as RenderTexture; + var historyTextures = historyTexturesField?.GetValue(renderer) as RenderTexture[]; + var historyIndex = historyIndexField?.GetValue(renderer) is int value ? value : -1; + var previous = RenderTexture.active; + var directory = Path.GetDirectoryName(Path.GetFullPath(capturePath)); + WriteFloatTexture(raymarchTexture, Path.Combine(directory, "field-volume-raymarch.exr")); + var historyTexture = historyIndex >= 0 && historyTextures != null && historyIndex < historyTextures.Length + ? historyTextures[historyIndex] + : null; + WriteFloatTexture(historyTexture, Path.Combine(directory, "field-volume-history.exr")); + WriteFloatTexture(historyTexture ?? raymarchTexture, Path.Combine(directory, "field-volume-cloud.exr")); + RenderTexture.active = previous; + } + + private static void WriteFloatTexture(RenderTexture texture, string path) + { + if (texture == null || string.IsNullOrWhiteSpace(path)) return; var previous = RenderTexture.active; RenderTexture.active = texture; var copy = new Texture2D(texture.width, texture.height, TextureFormat.RGBAFloat, false, true); copy.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0); copy.Apply(); RenderTexture.active = previous; - var directory = Path.GetDirectoryName(Path.GetFullPath(capturePath)); - File.WriteAllBytes(Path.Combine(directory, "field-volume-cloud.exr"), copy.EncodeToEXR()); + File.WriteAllBytes(path, copy.EncodeToEXR()); UnityEngine.Object.DestroyImmediate(copy); } diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index f39c7ed..75f12a9 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -1232,19 +1232,40 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests private static void WriteFieldCloudCapture(GameObject root, string capturePath) { var renderer = root.GetComponent(); - var field = typeof(EveUnityFieldsVolumeRenderer).GetField( - "_cloudTexture", + if (renderer == null || string.IsNullOrWhiteSpace(capturePath)) return; + var raymarchField = typeof(EveUnityFieldsVolumeRenderer).GetField( + "_raymarchTexture", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); - var texture = field?.GetValue(renderer) as RenderTexture; - if (texture == null || string.IsNullOrWhiteSpace(capturePath)) return; + var historyTexturesField = typeof(EveUnityFieldsVolumeRenderer).GetField( + "_historyTextures", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var historyIndexField = typeof(EveUnityFieldsVolumeRenderer).GetField( + "_historyTextureIndex", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var raymarchTexture = raymarchField?.GetValue(renderer) as RenderTexture; + var historyTextures = historyTexturesField?.GetValue(renderer) as RenderTexture[]; + var historyIndex = historyIndexField?.GetValue(renderer) is int value ? value : -1; + var previous = RenderTexture.active; + var directory = Path.GetDirectoryName(Path.GetFullPath(capturePath)); + WriteFloatTexture(raymarchTexture, Path.Combine(directory, "field-volume-raymarch.exr")); + var historyTexture = historyIndex >= 0 && historyTextures != null && historyIndex < historyTextures.Length + ? historyTextures[historyIndex] + : null; + WriteFloatTexture(historyTexture, Path.Combine(directory, "field-volume-history.exr")); + WriteFloatTexture(historyTexture ?? raymarchTexture, Path.Combine(directory, "field-volume-cloud.exr")); + RenderTexture.active = previous; + } + + private static void WriteFloatTexture(RenderTexture texture, string path) + { + if (texture == null || string.IsNullOrWhiteSpace(path)) return; var previous = RenderTexture.active; RenderTexture.active = texture; var copy = new Texture2D(texture.width, texture.height, TextureFormat.RGBAFloat, false, true); copy.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0); copy.Apply(); RenderTexture.active = previous; - var directory = Path.GetDirectoryName(Path.GetFullPath(capturePath)); - File.WriteAllBytes(Path.Combine(directory, "field-volume-cloud.exr"), copy.EncodeToEXR()); + File.WriteAllBytes(path, copy.EncodeToEXR()); UnityEngine.Object.DestroyImmediate(copy); } diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 23f2f5d..750122b 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -50,6 +50,8 @@ New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null foreach ($priorArtifact in @($resultsPath, $capturePath, $mapCapturePath, $factsPath, $providerReadyPath, $witnessPath)) { if (Test-Path -LiteralPath $priorArtifact) { Remove-Item -LiteralPath $priorArtifact -Force } } +Get-ChildItem -LiteralPath $outputRoot -Filter "field-*" -File -ErrorAction SilentlyContinue | + Remove-Item -Force if ($CacheState -eq "cold" -and (Test-Path -LiteralPath $assetCachePath)) { Remove-Item -LiteralPath $assetCachePath -Recurse -Force } From 35ff288aabc03130a61228579f1b09bac4345b5c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 21:30:50 +0100 Subject: [PATCH 086/292] Lower entity-forward follow cameras generically --- docs/authority-map.md | 8 ++ .../Runtime/EveUnityPlayableWorldCameraRig.cs | 52 +++++++++++- .../EveUnitySceneSurfaceLowererTests.cs | 82 +++++++++++++++++-- .../org.gamecult.eve.unity-scene/package.json | 2 +- 4 files changed, 133 insertions(+), 11 deletions(-) diff --git a/docs/authority-map.md b/docs/authority-map.md index 17e065d..235f69e 100644 --- a/docs/authority-map.md +++ b/docs/authority-map.md @@ -51,6 +51,14 @@ temporal pass must also advertise its current-sample, history, previous-view-projection, and reset-history ports; partial temporal programs fail closed. +The provider selects camera semantics and framing from its authoritative mode. +`planar.top-down-follow.v1` derives a downward presentation from the advertised +target and framing values. `perspective.entity-forward-follow.v1` derives its +view direction from the presented entity rotation and applies the same generic +distance, screen-position, damping, and lens contract. EveUnity owns only the +native camera transform. It cannot collapse distinct provider modes into one +camera opinion or infer a product camera from entity kind. + ## Shared Paths Initial connect discovers a Verse endpoint and selects a provider surface by diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index 7b50fad..ba2d799 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -80,7 +80,9 @@ namespace GameCult.Eve.UnityScene return false; } - if (activeWorld.CameraRig == "planar.top-down-follow.v1" && !HasValidPlanarContract(activeWorld)) + if ((activeWorld.CameraRig == "planar.top-down-follow.v1" || + activeWorld.CameraRig == "perspective.entity-forward-follow.v1") && + !HasValidFramedFollowContract(activeWorld)) { ReleaseCamera(); RestoreAmbientEnvironment(); @@ -88,6 +90,7 @@ namespace GameCult.Eve.UnityScene } if (!string.IsNullOrWhiteSpace(activeWorld.CameraRig) && activeWorld.CameraRig != "planar.top-down-follow.v1" && + activeWorld.CameraRig != "perspective.entity-forward-follow.v1" && activeWorld.CameraRig != "arpg.orbital-follow.v1" && activeWorld.CameraRig != "third-person-orbit") { @@ -139,6 +142,8 @@ namespace GameCult.Eve.UnityScene } if (activeWorld.CameraRig == "planar.top-down-follow.v1") return ApplyPlanarTopDown(activeWorld, camera, cameraComponent, player.position, deltaTime); + if (activeWorld.CameraRig == "perspective.entity-forward-follow.v1") + return ApplyEntityForwardFollow(activeWorld, camera, cameraComponent, player, deltaTime); var bounds = CalculateVisualBounds( player, @@ -197,7 +202,50 @@ namespace GameCult.Eve.UnityScene return true; } - private static bool HasValidPlanarContract(EveUnityPlayableWorldProjection world) => + private static bool ApplyEntityForwardFollow( + EveUnityPlayableWorldProjection world, + Transform camera, + Camera? cameraComponent, + Transform target, + float deltaTime) + { + var resolvedDistance = world.CameraDistance; + var verticalFov = world.CameraVerticalFieldOfViewDegrees; + if (cameraComponent != null) + { + cameraComponent.orthographic = false; + cameraComponent.lensShift = Vector2.zero; + cameraComponent.fieldOfView = verticalFov; + cameraComponent.nearClipPlane = world.CameraNearClipPlane; + cameraComponent.farClipPlane = world.CameraFarClipPlane; + } + + var forward = target.forward; + forward.y = 0f; + if (forward.sqrMagnitude <= 0.0001f) + forward = Vector3.forward; + forward.Normalize(); + var rotation = Quaternion.LookRotation(forward, Vector3.up); + var right = rotation * Vector3.right; + var aspect = cameraComponent != null ? cameraComponent.aspect : 16f / 9f; + var verticalHalfExtent = resolvedDistance * Mathf.Tan(verticalFov * 0.5f * Mathf.Deg2Rad); + var horizontalHalfExtent = verticalHalfExtent * Mathf.Max(0.01f, aspect); + var horizontalOffset = (Mathf.Clamp01(world.CameraTargetScreenX) - 0.5f) * 2f * horizontalHalfExtent; + var verticalOffset = (Mathf.Clamp01(world.CameraTargetScreenY) - 0.5f) * 2f * verticalHalfExtent; + var desiredPosition = target.position - + (forward * resolvedDistance) - + (right * horizontalOffset) - + (Vector3.up * verticalOffset); + var damping = Mathf.Max(0f, world.CameraPositionDamping); + var t = deltaTime <= 0f || damping <= 0f + ? 1f + : 1f - Mathf.Exp(-damping * deltaTime); + camera.position = Vector3.Lerp(camera.position, desiredPosition, t); + camera.rotation = rotation; + return true; + } + + private static bool HasValidFramedFollowContract(EveUnityPlayableWorldProjection world) => world.CameraDistance > 0f && world.CameraVerticalFieldOfViewDegrees > 0f && world.CameraVerticalFieldOfViewDegrees < 180f && diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 1ab2b4a..891f07e 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -1265,6 +1265,65 @@ namespace GameCult.Eve.UnityScene.Tests } } + [Test] + public void PlayableWorldCameraRigFramesEntityForwardPerspectiveWithoutProviderTypes() + { + var rootObject = new GameObject("generic-forward-world-root"); + var hostObject = new GameObject("generic-forward-client"); + var cameraObject = new GameObject("generic-forward-camera"); + hostObject.SetActive(false); + try + { + var provider = hostObject.AddComponent(); + var nativeAssets = hostObject.AddComponent(); + provider.Set( + new EveUnitySceneProviderSurfaceDocument( + PlayableArpgDocument( + cameraRig: "perspective.entity-forward-follow.v1", + cameraDistance: "30", + cameraTargetScreenX: "0.64", + cameraTargetScreenY: "0.81", + cameraPositionDamping: "0", + cameraNearClipPlane: "1", + cameraFarClipPlane: "4096"), + Advertisement("aetheria.daemon.game"), + "cultmesh://generic/eve/surfaces/forward", + 1), + new EveUnityPlayableWorldAssetManifestDocument( + "cultmesh://generic/assets/manifest", + Array.Empty(), + "generic")); + + var host = hostObject.AddComponent(); + host.Configure(rootObject.transform, provider, provider, provider, provider, nativeAssets); + host.Connect(); + hostObject.SetActive(true); + var camera = cameraObject.AddComponent(); + camera.aspect = 16f / 9f; + var rig = hostObject.AddComponent(); + rig.Host = host; + rig.CameraTransform = cameraObject.transform; + + var player = rootObject.GetComponentInChildren(); + Assert.That(player, Is.Not.Null); + Assert.That(rig.ApplyRig(0f), Is.True); + Assert.That(Vector3.Dot(cameraObject.transform.forward, player!.transform.forward), Is.GreaterThan(0.999f)); + Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); + Assert.That(camera.nearClipPlane, Is.EqualTo(1f).Within(0.001f)); + Assert.That(camera.farClipPlane, Is.EqualTo(4096f).Within(0.001f)); + var viewport = camera.WorldToViewportPoint(player.transform.position); + Assert.That(viewport.x, Is.EqualTo(0.64f).Within(0.001f)); + Assert.That(viewport.y, Is.EqualTo(0.81f).Within(0.001f)); + Assert.That(cameraObject.transform.position.y, Is.LessThan(player.transform.position.y)); + } + finally + { + UnityEngine.Object.DestroyImmediate(cameraObject); + UnityEngine.Object.DestroyImmediate(hostObject); + UnityEngine.Object.DestroyImmediate(rootObject); + } + } + [Test] public void ProviderPrefabKeepsAuthoredScaleWhileFallbackUsesSemanticRadius() { @@ -1773,7 +1832,14 @@ namespace GameCult.Eve.UnityScene.Tests bool includeRaider = true, string playerPosition = "0,0,0", string skyboxAssetRef = "", - string reflectionAssetRef = "") + string reflectionAssetRef = "", + string cameraRig = "planar.top-down-follow.v1", + string cameraDistance = "150", + string cameraTargetScreenX = "0.9", + string cameraTargetScreenY = "0.55", + string cameraPositionDamping = "5", + string cameraNearClipPlane = "0.3", + string cameraFarClipPlane = "4096") { var playableChildren = new List { @@ -1910,15 +1976,15 @@ namespace GameCult.Eve.UnityScene.Tests ["zoneRenderSchema"] = "gamecult.aetheria.zone_render.v1", ["assetManifest"] = "cultmesh://aetheria/assets/manifest", ["inputProfile"] = "arpg-third-person", - ["cameraRig"] = "planar.top-down-follow.v1", + ["cameraRig"] = cameraRig, ["cameraTargetEntityId"] = "player-vanguard", - ["cameraDistance"] = "150", + ["cameraDistance"] = cameraDistance, ["cameraVerticalFieldOfViewDegrees"] = "60", - ["cameraTargetScreenX"] = "0.9", - ["cameraTargetScreenY"] = "0.55", - ["cameraPositionDamping"] = "5", - ["cameraNearClipPlane"] = "0.3", - ["cameraFarClipPlane"] = "4096", + ["cameraTargetScreenX"] = cameraTargetScreenX, + ["cameraTargetScreenY"] = cameraTargetScreenY, + ["cameraPositionDamping"] = cameraPositionDamping, + ["cameraNearClipPlane"] = cameraNearClipPlane, + ["cameraFarClipPlane"] = cameraFarClipPlane, ["ambientLightColor"] = "0.2,0.2,0.2", ["ambientLightIntensity"] = "1.46", ["skyboxAssetRef"] = skyboxAssetRef, diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index ea0ecf5..b2951a7 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.38", + "version": "0.3.39", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 9799dbe9fe744771c73d4f22ce4657a9b5a508c6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 21:40:16 +0100 Subject: [PATCH 087/292] Pin released client to entity-forward camera --- .../PlayMode/GenericWorldCaptureTests.cs | 9 ++-- ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 +- .../PlayMode/GenericWorldCaptureTests.cs | 9 ++-- docs/aetheria-released-package-witness.md | 42 ++++++++++--------- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 6 files changed, 37 insertions(+), 31 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 75f12a9..d2dd834 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -591,12 +591,13 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests aimDotDistance = Vector3.Distance(playerMarker.transform.position, aimRenderer.ViewDotPosition); Assert.That(aimDotDistance, Is.GreaterThanOrEqualTo(49.9f)); Assert.That(camera.cullingMask & (1 << mapLayer), Is.Zero); - Assert.That(runtime.ActiveWorld.CameraDistance, Is.EqualTo(70f).Within(0.001f)); + Assert.That(runtime.ActiveWorld.CameraRig, Is.EqualTo("perspective.entity-forward-follow.v1")); + Assert.That(runtime.ActiveWorld.CameraDistance, Is.EqualTo(30f).Within(0.001f)); Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); - Assert.That(runtime.ActiveWorld.CameraPositionDamping, Is.EqualTo(2f).Within(0.001f)); + Assert.That(runtime.ActiveWorld.CameraPositionDamping, Is.EqualTo(0f).Within(0.001f)); var playerViewport = camera.WorldToViewportPoint(playerMarker.transform.position); - Assert.That(playerViewport.x, Is.EqualTo(0.66f).Within(0.01f)); - Assert.That(playerViewport.y, Is.EqualTo(0.55f).Within(0.01f)); + Assert.That(playerViewport.x, Is.EqualTo(0.64f).Within(0.01f)); + Assert.That(playerViewport.y, Is.EqualTo(0.81f).Within(0.01f)); MeasureViewportCoverage(camera, playerRenderers, out var playerViewportWidth, out var playerViewportHeight); var playerRendererFacts = playerRenderers .Select(renderer => RendererFact.From(camera, renderer)) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index f214dae..9b8f7a6 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.0", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.38", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.39", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index c08b7c2..b4362f2 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.38", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.39", "depth": 0, "source": "git", "dependencies": { @@ -154,7 +154,7 @@ "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "de580fe88350f422e30f1e4b64ded5c33070b118" + "hash": "35ff288aabc03130a61228579f1b09bac4345b5c" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 75f12a9..d2dd834 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -591,12 +591,13 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests aimDotDistance = Vector3.Distance(playerMarker.transform.position, aimRenderer.ViewDotPosition); Assert.That(aimDotDistance, Is.GreaterThanOrEqualTo(49.9f)); Assert.That(camera.cullingMask & (1 << mapLayer), Is.Zero); - Assert.That(runtime.ActiveWorld.CameraDistance, Is.EqualTo(70f).Within(0.001f)); + Assert.That(runtime.ActiveWorld.CameraRig, Is.EqualTo("perspective.entity-forward-follow.v1")); + Assert.That(runtime.ActiveWorld.CameraDistance, Is.EqualTo(30f).Within(0.001f)); Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); - Assert.That(runtime.ActiveWorld.CameraPositionDamping, Is.EqualTo(2f).Within(0.001f)); + Assert.That(runtime.ActiveWorld.CameraPositionDamping, Is.EqualTo(0f).Within(0.001f)); var playerViewport = camera.WorldToViewportPoint(playerMarker.transform.position); - Assert.That(playerViewport.x, Is.EqualTo(0.66f).Within(0.01f)); - Assert.That(playerViewport.y, Is.EqualTo(0.55f).Within(0.01f)); + Assert.That(playerViewport.x, Is.EqualTo(0.64f).Within(0.01f)); + Assert.That(playerViewport.y, Is.EqualTo(0.81f).Within(0.01f)); MeasureViewportCoverage(camera, playerRenderers, out var playerViewportWidth, out var playerViewportHeight); var playerRendererFacts = playerRenderers .Select(renderer => RendererFact.From(camera, renderer)) diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 0bc6f5a..235c136 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,8 +4,8 @@ The warm-cache released-package witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.38`, commit - `de580fe88350f422e30f1e4b64ded5c33070b118`; +- `org.gamecult.eve.unity-scene` `0.3.39`, commit + `35ff288aabc03130a61228579f1b09bac4345b5c`; - `org.gamecult.eve.plugin-fields` `0.2.0`, commit `382a23d8f8a07e4b5eef5f81f84655861a858367`; - `org.gamecult.eve.surface` `0.2.2`, commit @@ -17,7 +17,7 @@ The warm-cache released-package witness passes with: - the generic `ReleaseConsumerProject` client connected directly to the Aetheria daemon. -Evidence is in `artifacts/aetheria-daemon-gravity-fog-warm`. The PlayMode test +Evidence is in `artifacts/aetheria-daemon-gravity-fog-forward`. The PlayMode test passed and recorded provider-owned reconciled movement, look, targeting, tractor press, tractor release, contact-gated cargo collection, and action receipts. The daemon-owned look direction reached the SoA body rotation, @@ -28,10 +28,12 @@ active camera rig's leased Unity environment. The witness supplies no client-authored light. Aetheria advertises the key-light direction, color, and intensity; the generic camera rig lowers that contract to the only live directional light (`0.75` intensity). -Aetheria also advertises the original `ARPG.unity` follow-camera composition: -`70` unit distance, `60` degree vertical field of view, target screen position -`0.66,0.55`, and position damping `2`. The generic camera rig lowers those -values without Aetheria-specific camera code. +Aetheria also advertises the original undocked `ARPG.unity` Third Person Rig: +entity-forward perspective follow, `30` unit distance, `60` degree vertical +field of view, target screen position `0.64,0.81`, zero position damping, and +`1`-`4096` clip planes. The generic camera rig lowers those values without +Aetheria-specific camera code. The distinct `70` unit Docked Rig no longer +incorrectly owns the flight view. Aetheria's pilot surface also publishes a transparent cockpit overlay. The released generic UI Toolkit lowerer produced seven native `ProgressBar` elements for hull, shield, capacitor, weapon cooldown, target lock, and target state. Daemon frame, @@ -46,7 +48,7 @@ Fields splat layers. Its surface contract contains logical ports such as names or pass indices. The selected `unity-scene` provider asset variant owns that concrete shader ABI through `unity.volume.*` metadata. The released generic lowerer resolved the provider shader and dither texture, rasterized all -four layers, and composited `22` pilot-camera frames from daemon frame `224`. +four layers, and composited `22` pilot-camera frames from daemon frame `226`. The provider variant advertises the fossil shader's raymarch, temporal-history, and composite passes plus their logical ports. EveUnity allocates and resets the history targets generically; a partial temporal ABI fails closed. @@ -60,11 +62,11 @@ daemon lock progress `1.0`; Unity does not manufacture or smooth that value. Camera-channel facts: - provider-authored player renderers: `13`, including the lowered tractor effect; -- pilot changed pixels: `230,310`; -- pilot average luminance: `0.0524399`; -- pilot bright pixels: `1,731`; +- pilot changed pixels: `230,400`; +- pilot average luminance: `0.1889651`; +- pilot bright pixels: `13,302`; - map-channel renderers: `11`; -- map changed pixels: `4,937`; +- map changed pixels: `4,213`; - native cockpit progress bars: `7`; - daemon tractor power at capture: `1.0` after the held input completed its authored ramp; @@ -80,12 +82,14 @@ Camera-channel facts: - the map camera renders exactly the advertised map layer. Visual inspection confirms that map glyphs are absent from the pilot frame and -present in the map-only frame. The volume lifecycle is active, but the pilot -frame remains nearly black and geometric; it does not yet reproduce the fossil's -blue gravity-shaped fog sea. This is transport and lowering proof, not visual -parity. The next provider-side diagnosis must inspect published tint/height -field values, exposure/composition, camera depth, and the fossil tuning. EveUnity -must not compensate with Aetheria-specific lighting or fog rules. +present in the map-only frame. Correcting the fossil camera mode removed the +false top-down dark disk, but the active volume now resolves as a nearly uniform +saturated orange horizon rather than the fossil's blue gravity-shaped fog sea. +This is transport and lowering proof, not visual parity. The first concrete ABI +divergence is that the fossil derives dither sampling coordinates from the +render target and dither texture dimensions while the current surface supplies +zeroes. That lifecycle input must be lowered generically; EveUnity must not +compensate with Aetheria-specific lighting or fog rules. Substance is not part of this path; later texture baking belongs in Blender. Aetheria currently bundles ambientCG's 1K `Metal012` color, normal, and metalness maps under CC0. The same pre-generated maps now replace dead @@ -119,7 +123,7 @@ an Aetheria-specific beam. Primary artifacts: - `results.xml`: one passing PlayMode witness; -- `runtime-witness.warm.json`: package, content, receipt, aim, combat, and +- `runtime-witness.json`: package, content, receipt, aim, combat, and camera facts; - `aetheria-daemon-world.png`: pilot-camera capture; - `aetheria-daemon-map.png`: map-only capture. diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 750122b..110a91c 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "de580fe88350f422e30f1e4b64ded5c33070b118" +$expectedEveUnityCommit = "35ff288aabc03130a61228579f1b09bac4345b5c" $expectedEveFieldsCommit = "382a23d8f8a07e4b5eef5f81f84655861a858367" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" From 3c8d127ef8d0d8e6c9a45f92a8bbd553d87e6a07 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 21:47:50 +0100 Subject: [PATCH 088/292] Derive native volume texture scale from viewport --- docs/authority-map.md | 8 ++++ .../Fields/EveUnityFieldsVolumeRenderer.cs | 42 ++++++++++++++++++- .../EveFieldsUnitySceneProjectionTests.cs | 19 +++++++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/docs/authority-map.md b/docs/authority-map.md index 235f69e..3731576 100644 --- a/docs/authority-map.md +++ b/docs/authority-map.md @@ -19,6 +19,7 @@ drivers, asset-provider hooks, package release, runtime tests, and captures. - provider asset-manifest documents - runtime-selected asset-variant metadata, including native rendering-program bindings - generic field-volume lifecycle declarations and logical texture/scalar/matrix ports +- generic viewport-to-texture scale bindings between logical vector and texture ports - provider command receipts - semantic render-channel inclusion and exclusion policy - Unity input and local presentation assets @@ -51,6 +52,13 @@ temporal pass must also advertise its current-sample, history, previous-view-projection, and reset-history ports; partial temporal programs fail closed. +Viewport-sized shader inputs are likewise presentation state. A provider may +relate a logical vector port to a logical native texture port through +`viewportTextureScaleBindings`; EveUnity derives the vector from the active +camera viewport and resolved texture dimensions on every render. A provider +literal is not allowed to impersonate those runtime dimensions, and an +unresolvable relation fails closed. + The provider selects camera semantics and framing from its authoritative mode. `planar.top-down-follow.v1` derives a downward presentation from the advertised target and framing values. `perspective.entity-forward-follow.v1` derives its diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs index 2509849..add27f9 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -20,6 +20,8 @@ namespace GameCult.Eve.UnityScene.Fields private Material? _material; private Shader? _sourceShader; private IReadOnlyDictionary? _programMetadata; + private readonly Dictionary _assetTexturesByPort = + new Dictionary(StringComparer.Ordinal); private RenderTexture? _raymarchTexture; private readonly RenderTexture?[] _historyTextures = new RenderTexture?[2]; private int _historyTextureIndex = -1; @@ -89,7 +91,7 @@ namespace GameCult.Eve.UnityScene.Fields ApplyFieldBindings(field, _document); camera.depthTextureMode |= DepthTextureMode.Depth; EnsureVolumeTextures(camera, field); - if (_raymarchTexture == null) return; + if (_raymarchTexture == null || !ApplyViewportTextureScaleBindings(field, camera)) return; var gpuProjection = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true); var viewProjection = gpuProjection * camera.worldToCameraMatrix; @@ -168,6 +170,7 @@ namespace GameCult.Eve.UnityScene.Fields _sourceShader = shader; _activeNodeId = field.NodeId; _material = new Material(shader) { hideFlags = HideFlags.DontSave }; + _assetTexturesByPort.Clear(); ResetTemporalHistory(); foreach (var parameter in ParseBindings(Prop(field, "floatParameters"))) { @@ -180,7 +183,11 @@ namespace GameCult.Eve.UnityScene.Fields var texture = provider.ResolveAsset( new EveUnityPlayableWorldAssetBinding(binding.Key, "", "provider-asset-ref"), typeof(Texture)) as Texture; - if (texture != null) SetTexturePort(binding.Value, texture); + if (texture != null) + { + _assetTexturesByPort[binding.Value] = texture; + SetTexturePort(binding.Value, texture); + } } var quality = Prop(field, "quality").Trim().ToLowerInvariant(); if (TryProgramValue($"unity.volume.quality.{quality}.keyword", out var keyword) && @@ -262,6 +269,7 @@ namespace GameCult.Eve.UnityScene.Fields if (_material != null) { Destroy(_material); _material = null; } _sourceShader = null; _programMetadata = null; + _assetTexturesByPort.Clear(); _activeNodeId = ""; PresentedFrameId = -1; CompositeCount = 0; @@ -276,6 +284,36 @@ namespace GameCult.Eve.UnityScene.Fields private bool HasTemporalProgram() => ProgramPass("temporal") >= 0; + private bool ApplyViewportTextureScaleBindings(EveUnityFieldVolumeProjection field, Camera camera) + { + foreach (var binding in ParseBindings(Prop(field, "viewportTextureScaleBindings"))) + { + if (string.IsNullOrWhiteSpace(ProgramPort("vector", binding.Key)) || + string.IsNullOrWhiteSpace(ProgramPort("texture", binding.Value)) || + !_assetTexturesByPort.TryGetValue(binding.Value, out var texture) || + !TryComputeViewportTextureScale(camera.pixelWidth, camera.pixelHeight, texture, out var scale)) + return false; + SetVectorPort(binding.Key, scale); + } + return true; + } + + public static bool TryComputeViewportTextureScale( + int viewportWidth, + int viewportHeight, + Texture? texture, + out Vector4 scale) + { + scale = default; + if (texture == null || texture.width <= 0 || texture.height <= 0) return false; + scale = new Vector4( + (float)Math.Max(1, viewportWidth) / texture.width, + (float)Math.Max(1, viewportHeight) / texture.height, + 0f, + 0f); + return true; + } + private static bool EnsureRenderTexture(ref RenderTexture? texture, int width, int height, string name) { if (texture != null && texture.width == width && texture.height == height) return false; diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index 25fd08b..cf10e64 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -73,6 +73,25 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(error, Does.Contain("history port")); } + [Test] + public void VolumeRendererDerivesViewportTextureScaleFromNativeDimensions() + { + var texture = new Texture2D(128, 64, TextureFormat.RGBA32, false); + try + { + Assert.That(EveUnityFieldsVolumeRenderer.TryComputeViewportTextureScale( + 640, + 360, + texture, + out var scale), Is.True); + Assert.That(scale, Is.EqualTo(new Vector4(5f, 5.625f, 0f, 0f))); + } + finally + { + UnityEngine.Object.DestroyImmediate(texture); + } + } + [Test] public void PackageContainsGenericFieldsShader() { diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index b2951a7..5e9720a 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.39", + "version": "0.3.40", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 2629334e63cde30facc7b17a8623b915784a5d40 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 21:54:03 +0100 Subject: [PATCH 089/292] Pin released witness to viewport texture scaling --- ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 +-- docs/aetheria-released-package-witness.md | 25 +++++++++++-------- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 9b8f7a6..bfb6fa3 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.0", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.39", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.40", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index b4362f2..dd2dc39 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.39", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.40", "depth": 0, "source": "git", "dependencies": { @@ -154,7 +154,7 @@ "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "35ff288aabc03130a61228579f1b09bac4345b5c" + "hash": "3c8d127ef8d0d8e6c9a45f92a8bbd553d87e6a07" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 235c136..aa1b984 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,8 +4,8 @@ The warm-cache released-package witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.39`, commit - `35ff288aabc03130a61228579f1b09bac4345b5c`; +- `org.gamecult.eve.unity-scene` `0.3.40`, commit + `3c8d127ef8d0d8e6c9a45f92a8bbd553d87e6a07`; - `org.gamecult.eve.plugin-fields` `0.2.0`, commit `382a23d8f8a07e4b5eef5f81f84655861a858367`; - `org.gamecult.eve.surface` `0.2.2`, commit @@ -17,7 +17,7 @@ The warm-cache released-package witness passes with: - the generic `ReleaseConsumerProject` client connected directly to the Aetheria daemon. -Evidence is in `artifacts/aetheria-daemon-gravity-fog-forward`. The PlayMode test +Evidence is in `artifacts/aetheria-daemon-gravity-fog-dither`. The PlayMode test passed and recorded provider-owned reconciled movement, look, targeting, tractor press, tractor release, contact-gated cargo collection, and action receipts. The daemon-owned look direction reached the SoA body rotation, @@ -48,7 +48,7 @@ Fields splat layers. Its surface contract contains logical ports such as names or pass indices. The selected `unity-scene` provider asset variant owns that concrete shader ABI through `unity.volume.*` metadata. The released generic lowerer resolved the provider shader and dither texture, rasterized all -four layers, and composited `22` pilot-camera frames from daemon frame `226`. +four layers, and composited `22` pilot-camera frames from daemon frame `232`. The provider variant advertises the fossil shader's raymarch, temporal-history, and composite passes plus their logical ports. EveUnity allocates and resets the history targets generically; a partial temporal ABI fails closed. @@ -63,10 +63,10 @@ Camera-channel facts: - provider-authored player renderers: `13`, including the lowered tractor effect; - pilot changed pixels: `230,400`; -- pilot average luminance: `0.1889651`; -- pilot bright pixels: `13,302`; +- pilot average luminance: `0.1893132`; +- pilot bright pixels: `13,948`; - map-channel renderers: `11`; -- map changed pixels: `4,213`; +- map changed pixels: `4,365`; - native cockpit progress bars: `7`; - daemon tractor power at capture: `1.0` after the held input completed its authored ramp; @@ -85,10 +85,13 @@ Visual inspection confirms that map glyphs are absent from the pilot frame and present in the map-only frame. Correcting the fossil camera mode removed the false top-down dark disk, but the active volume now resolves as a nearly uniform saturated orange horizon rather than the fossil's blue gravity-shaped fog sea. -This is transport and lowering proof, not visual parity. The first concrete ABI -divergence is that the fossil derives dither sampling coordinates from the -render target and dither texture dimensions while the current surface supplies -zeroes. That lifecycle input must be lowered generically; EveUnity must not +This is transport and lowering proof, not visual parity. Release `0.3.40` now +derives dither sampling coordinates from the active viewport and provider +texture dimensions through a generic logical-port relation. The resulting +raymarch remains nearly flat orange (`R 0.8896-0.8960`, `G 0.5493-0.5532`, +`B 0.3096-0.3118`, alpha `0`), so that real ABI correction is not the missing +visual mechanism. The next diagnosis must compare camera/world sampling and +field viewport placement before changing authored tuning. EveUnity must not compensate with Aetheria-specific lighting or fog rules. Substance is not part of this path; later texture baking belongs in Blender. Aetheria currently bundles ambientCG's 1K `Metal012` color, normal, and diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 110a91c..3350577 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "35ff288aabc03130a61228579f1b09bac4345b5c" +$expectedEveUnityCommit = "3c8d127ef8d0d8e6c9a45f92a8bbd553d87e6a07" $expectedEveFieldsCommit = "382a23d8f8a07e4b5eef5f81f84655861a858367" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" From f0883e9644674849e03127d4d7a231905092f1de Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 22:05:25 +0100 Subject: [PATCH 090/292] Lower convergence-point camera look-at --- docs/authority-map.md | 10 +++++-- .../Runtime/EveUnityAimPresentation.cs | 29 +++++++++++++++++++ .../EveUnityAimPresentationRenderer.cs | 22 ++------------ .../Runtime/EveUnityPlayableWorldCameraRig.cs | 28 +++++++++++++++--- .../Runtime/EveUnitySceneSurfaceLowerer.cs | 9 ++++-- .../EveUnitySceneSurfaceLowererTests.cs | 15 +++++++--- .../org.gamecult.eve.unity-scene/package.json | 2 +- 7 files changed, 81 insertions(+), 34 deletions(-) diff --git a/docs/authority-map.md b/docs/authority-map.md index 3731576..7c6d139 100644 --- a/docs/authority-map.md +++ b/docs/authority-map.md @@ -63,9 +63,13 @@ The provider selects camera semantics and framing from its authoritative mode. `planar.top-down-follow.v1` derives a downward presentation from the advertised target and framing values. `perspective.entity-forward-follow.v1` derives its view direction from the presented entity rotation and applies the same generic -distance, screen-position, damping, and lens contract. EveUnity owns only the -native camera transform. It cannot collapse distinct provider modes into one -camera opinion or infer a product camera from entity kind. +distance, screen-position, damping, and lens contract. When the provider also +advertises `aim.convergence-point.v1`, the camera and aim marker consume one +derived convergence point from the `aim.presentation` node. The native camera +rotation and position are solved together so the optical axis reaches that +point while the followed entity remains at the advertised screen coordinate. +EveUnity owns only this native projection. It cannot collapse distinct provider +modes into one camera opinion or infer a product camera from entity kind. ## Shared Paths diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs index fbe8720..4b9bdf4 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using UnityEngine; #nullable enable @@ -23,6 +24,24 @@ namespace GameCult.Eve.UnityScene public float MinimumConvergenceDistance { get; } public float ViewDotRadius { get; } + public bool TryResolveViewPoint(EveUnityPlayableWorldClientHost? host, out Vector3 viewPoint) + { + viewPoint = default; + if (host == null || string.IsNullOrWhiteSpace(ControlledEntityId)) return false; + var controlled = FindMarker(host, ControlledEntityId); + if (controlled == null) return false; + var distance = MinimumConvergenceDistance; + var target = FindMarker(host, ConvergenceTargetEntityId); + if (target != null) + distance = Mathf.Max(distance, Vector3.Distance(controlled.transform.position, target.transform.position)); + var forward = controlled.transform.forward; + forward.y = 0f; + if (forward.sqrMagnitude <= 0.0001f) forward = Vector3.forward; + forward.Normalize(); + viewPoint = controlled.transform.position + forward * distance; + return true; + } + public static EveUnityAimPresentation? Find(EveUnitySceneProjection? projection) => projection == null ? null : Find(projection.Root); @@ -38,6 +57,16 @@ namespace GameCult.Eve.UnityScene return null; } + private static EveUnityPlayableWorldEntityMarker? FindMarker( + EveUnityPlayableWorldClientHost host, + string entityId) + { + if (string.IsNullOrWhiteSpace(entityId)) return null; + foreach (var marker in host.SceneRoot.GetComponentsInChildren(true)) + if (string.Equals(marker.EntityId, entityId, StringComparison.Ordinal)) return marker; + return null; + } + private static string Get(EveUnitySceneNode node, string key) => node.Props.TryGetValue(key, out var value) ? value ?? "" : ""; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs index 78c7788..321ca96 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs @@ -34,22 +34,12 @@ namespace GameCult.Eve.UnityScene return; } - var controlled = FindMarker(Current.ControlledEntityId); - if (controlled == null) + if (!Current.TryResolveViewPoint(_host, out var viewPoint)) { SetVisible(false); return; } - - var distance = Current.MinimumConvergenceDistance; - var target = FindMarker(Current.ConvergenceTargetEntityId); - if (target != null) - distance = Mathf.Max(distance, Vector3.Distance(controlled.transform.position, target.transform.position)); - var forward = controlled.transform.forward; - forward.y = 0f; - if (forward.sqrMagnitude <= 0.0001f) forward = Vector3.forward; - forward.Normalize(); - ViewDotPosition = controlled.transform.position + forward * distance; + ViewDotPosition = viewPoint; EnsureVisuals(); var camera = _host?.ActiveCameraTransform; @@ -69,14 +59,6 @@ namespace GameCult.Eve.UnityScene private void LateUpdate() => RefreshNow(); - private EveUnityPlayableWorldEntityMarker? FindMarker(string entityId) - { - if (_host == null || string.IsNullOrWhiteSpace(entityId)) return null; - foreach (var marker in _host.SceneRoot.GetComponentsInChildren(true)) - if (string.Equals(marker.EntityId, entityId, StringComparison.Ordinal)) return marker; - return null; - } - private void EnsureVisuals() { if (_ring != null) return; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index ba2d799..ded0d3a 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -143,7 +143,7 @@ namespace GameCult.Eve.UnityScene if (activeWorld.CameraRig == "planar.top-down-follow.v1") return ApplyPlanarTopDown(activeWorld, camera, cameraComponent, player.position, deltaTime); if (activeWorld.CameraRig == "perspective.entity-forward-follow.v1") - return ApplyEntityForwardFollow(activeWorld, camera, cameraComponent, player, deltaTime); + return ApplyEntityForwardFollow(activeWorld, resolvedHost, camera, cameraComponent, player, deltaTime); var bounds = CalculateVisualBounds( player, @@ -204,6 +204,7 @@ namespace GameCult.Eve.UnityScene private static bool ApplyEntityForwardFollow( EveUnityPlayableWorldProjection world, + EveUnityPlayableWorldClientHost host, Transform camera, Camera? cameraComponent, Transform target, @@ -226,16 +227,35 @@ namespace GameCult.Eve.UnityScene forward = Vector3.forward; forward.Normalize(); var rotation = Quaternion.LookRotation(forward, Vector3.up); - var right = rotation * Vector3.right; var aspect = cameraComponent != null ? cameraComponent.aspect : 16f / 9f; var verticalHalfExtent = resolvedDistance * Mathf.Tan(verticalFov * 0.5f * Mathf.Deg2Rad); var horizontalHalfExtent = verticalHalfExtent * Mathf.Max(0.01f, aspect); var horizontalOffset = (Mathf.Clamp01(world.CameraTargetScreenX) - 0.5f) * 2f * horizontalHalfExtent; var verticalOffset = (Mathf.Clamp01(world.CameraTargetScreenY) - 0.5f) * 2f * verticalHalfExtent; + if (string.Equals(world.CameraLookAt, "aim.convergence-point.v1", System.StringComparison.Ordinal)) + { + var aim = EveUnityAimPresentation.Find(host.ActiveProjection); + if (aim == null || !aim.TryResolveViewPoint(host, out var aimPoint)) return false; + var targetToAim = aimPoint - target.position; + var targetToAimDistanceSquared = targetToAim.sqrMagnitude; + var screenOffsetSquared = horizontalOffset * horizontalOffset + verticalOffset * verticalOffset; + if (targetToAimDistanceSquared <= screenOffsetSquared + 0.0001f) return false; + var targetToAimDirection = targetToAim.normalized; + var opticalDepthDelta = Mathf.Sqrt(targetToAimDistanceSquared - screenOffsetSquared); + var cameraLocalTargetToAim = new Vector3( + -horizontalOffset, + -verticalOffset, + opticalDepthDelta); + rotation = Quaternion.LookRotation(targetToAimDirection, Vector3.up) * + Quaternion.FromToRotation(cameraLocalTargetToAim.normalized, Vector3.forward); + } + var right = rotation * Vector3.right; + var up = rotation * Vector3.up; + var opticalForward = rotation * Vector3.forward; var desiredPosition = target.position - - (forward * resolvedDistance) - + (opticalForward * resolvedDistance) - (right * horizontalOffset) - - (Vector3.up * verticalOffset); + (up * verticalOffset); var damping = Mathf.Max(0f, world.CameraPositionDamping); var t = deltaTime <= 0f || damping <= 0f ? 1f diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index 3482d66..bb2536d 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -150,7 +150,8 @@ namespace GameCult.Eve.UnityScene ParseVector3(worldRoot.GetProp("keyLightDirection")), ParseVector3(worldRoot.GetProp("keyLightColor")), ParseFloat(worldRoot.GetProp("keyLightIntensity"), 0f), - fieldVolumes); + fieldVolumes, + worldRoot.GetProp("cameraLookAt")); } private static EveUnityFieldVolumeProjection BuildFieldVolume(EveSurfaceComponent component) => @@ -392,7 +393,8 @@ namespace GameCult.Eve.UnityScene (float x, float y, float z) keyLightDirection = default, (float r, float g, float b) keyLightColor = default, float keyLightIntensity = 0f, - IReadOnlyList? fieldVolumes = null) + IReadOnlyList? fieldVolumes = null, + string cameraLookAt = "") { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -437,6 +439,7 @@ namespace GameCult.Eve.UnityScene KeyLightColorB = keyLightColor.b; KeyLightIntensity = keyLightIntensity; FieldVolumes = fieldVolumes ?? Array.Empty(); + CameraLookAt = cameraLookAt ?? ""; } public string WorldRootId { get; } @@ -459,6 +462,8 @@ namespace GameCult.Eve.UnityScene public string CameraTargetEntityId { get; } + public string CameraLookAt { get; } + public float CameraDistance { get; } public float CameraVerticalFieldOfViewDegrees { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 891f07e..881c883 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -401,6 +401,7 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(projection.PlayableWorld.AssetManifest, Is.EqualTo("cultmesh://aetheria/assets/manifest")); Assert.That(projection.PlayableWorld.InputProfile, Is.EqualTo("arpg-third-person")); Assert.That(projection.PlayableWorld.CameraRig, Is.EqualTo("planar.top-down-follow.v1")); + Assert.That(projection.PlayableWorld.CameraLookAt, Is.Empty); Assert.That(projection.PlayableWorld.CameraTargetEntityId, Is.EqualTo("player-vanguard")); Assert.That(projection.PlayableWorld.CameraDistance, Is.EqualTo(150f)); Assert.That(projection.PlayableWorld.CameraVerticalFieldOfViewDegrees, Is.EqualTo(60f)); @@ -1266,7 +1267,7 @@ namespace GameCult.Eve.UnityScene.Tests } [Test] - public void PlayableWorldCameraRigFramesEntityForwardPerspectiveWithoutProviderTypes() + public void PlayableWorldCameraRigFramesEntityAndLooksAtAdvertisedConvergencePointWithoutProviderTypes() { var rootObject = new GameObject("generic-forward-world-root"); var hostObject = new GameObject("generic-forward-client"); @@ -1285,7 +1286,8 @@ namespace GameCult.Eve.UnityScene.Tests cameraTargetScreenY: "0.81", cameraPositionDamping: "0", cameraNearClipPlane: "1", - cameraFarClipPlane: "4096"), + cameraFarClipPlane: "4096", + cameraLookAt: "aim.convergence-point.v1"), Advertisement("aetheria.daemon.game"), "cultmesh://generic/eve/surfaces/forward", 1), @@ -1307,7 +1309,10 @@ namespace GameCult.Eve.UnityScene.Tests var player = rootObject.GetComponentInChildren(); Assert.That(player, Is.Not.Null); Assert.That(rig.ApplyRig(0f), Is.True); - Assert.That(Vector3.Dot(cameraObject.transform.forward, player!.transform.forward), Is.GreaterThan(0.999f)); + var aimPoint = player!.transform.position + player.transform.forward * 50f; + var cameraToAim = (aimPoint - cameraObject.transform.position).normalized; + Assert.That(Vector3.Dot(cameraObject.transform.forward, cameraToAim), Is.GreaterThan(0.999f)); + Assert.That(cameraObject.transform.forward.y, Is.GreaterThan(0f)); Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); Assert.That(camera.nearClipPlane, Is.EqualTo(1f).Within(0.001f)); Assert.That(camera.farClipPlane, Is.EqualTo(4096f).Within(0.001f)); @@ -1839,7 +1844,8 @@ namespace GameCult.Eve.UnityScene.Tests string cameraTargetScreenY = "0.55", string cameraPositionDamping = "5", string cameraNearClipPlane = "0.3", - string cameraFarClipPlane = "4096") + string cameraFarClipPlane = "4096", + string cameraLookAt = "") { var playableChildren = new List { @@ -1977,6 +1983,7 @@ namespace GameCult.Eve.UnityScene.Tests ["assetManifest"] = "cultmesh://aetheria/assets/manifest", ["inputProfile"] = "arpg-third-person", ["cameraRig"] = cameraRig, + ["cameraLookAt"] = cameraLookAt, ["cameraTargetEntityId"] = "player-vanguard", ["cameraDistance"] = cameraDistance, ["cameraVerticalFieldOfViewDegrees"] = "60", diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 5e9720a..1dc9489 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.40", + "version": "0.3.41", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 6ff45d0bbf3b1f2ea70d6a808504139342d432c9 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 22:32:26 +0100 Subject: [PATCH 091/292] Pin witness to convergence camera release --- .../PlayMode/GenericWorldCaptureTests.cs | 47 ++++++++++++++++++- ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 +- .../PlayMode/GenericWorldCaptureTests.cs | 47 ++++++++++++++++++- docs/aetheria-released-package-witness.md | 36 ++++++++------ scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 6 files changed, 117 insertions(+), 21 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index d2dd834..57d685b 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -521,7 +521,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests "A reconciled fire request never reached daemon-owned lock acquisition and shot resolution."); observedCombat = EveUnityCombatPresentation.Find(runtime.ActiveProjection); Assert.That(observedShot.TargetEntityIndex, Is.EqualTo(targetEntityIndex)); - Assert.That(observedShot.LockQuality, Is.GreaterThan(0.99)); + Assert.That(observedShot.LockQuality, Is.GreaterThanOrEqualTo(0.99)); var trajectories = root.GetComponent(); var combatRenderer = root.GetComponent(); Assert.That(trajectories, Is.Not.Null); @@ -592,6 +592,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(aimDotDistance, Is.GreaterThanOrEqualTo(49.9f)); Assert.That(camera.cullingMask & (1 << mapLayer), Is.Zero); Assert.That(runtime.ActiveWorld.CameraRig, Is.EqualTo("perspective.entity-forward-follow.v1")); + Assert.That(runtime.ActiveWorld.CameraLookAt, Is.EqualTo("aim.convergence-point.v1")); Assert.That(runtime.ActiveWorld.CameraDistance, Is.EqualTo(30f).Within(0.001f)); Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); Assert.That(runtime.ActiveWorld.CameraPositionDamping, Is.EqualTo(0f).Within(0.001f)); @@ -703,6 +704,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests "The generic volume lowerer did not rasterize any advertised Fields layers."); Assert.That(fieldVolume.CompositeCount, Is.GreaterThan(0), "The generic volume lowerer never composited into the pilot camera."); + var fieldDocument = typeof(EveUnityFieldsVolumeRenderer) + .GetField("_document", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) + ?.GetValue(fieldVolume); + Assert.That(fieldDocument, Is.Not.Null, "The volume lowerer did not retain its presented Fields document."); + var fieldViewport = fieldDocument.GetType().GetProperty("Viewport")?.GetValue(fieldDocument); + var fieldSplats = fieldDocument.GetType().GetProperty("Splats")?.GetValue(fieldDocument); + Assert.That(fieldViewport, Is.Not.Null); + Assert.That(fieldSplats, Is.Not.Null); WriteWitnessFacts( initialVersion, runtime.ActiveVersion, @@ -744,6 +753,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests finalPickupEntityCount, pickupCollectionEventCount, pickupCollection, + camera.transform.position, + camera.transform.forward, + playerMarker.transform.position, + aimRenderer.ViewDotPosition, + Convert.ToSingle(fieldViewport.GetType().GetProperty("MinX")?.GetValue(fieldViewport)), + Convert.ToSingle(fieldViewport.GetType().GetProperty("MinY")?.GetValue(fieldViewport)), + Convert.ToSingle(fieldViewport.GetType().GetProperty("MaxX")?.GetValue(fieldViewport)), + Convert.ToSingle(fieldViewport.GetType().GetProperty("MaxY")?.GetValue(fieldViewport)), + Convert.ToInt32(fieldSplats.GetType().GetProperty("Count")?.GetValue(fieldSplats)), playerRendererFacts); } finally @@ -803,6 +821,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests int finalPickupEntityCount, int pickupCollectionEventCount, EveUnityFeedbackEvent pickupCollection, + Vector3 cameraPosition, + Vector3 cameraForward, + Vector3 playerPosition, + Vector3 aimPosition, + float fieldViewportMinX, + float fieldViewportMinY, + float fieldViewportMaxX, + float fieldViewportMaxY, + int fieldSplatCount, RendererFact[] playerRendererFacts) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); @@ -857,6 +884,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pickupQuantity = (float)(pickupCollection?.ScalarValue ?? 0), cargoQuantityBeforePickup = (float)(pickupCollection?.CargoQuantityBefore ?? 0), cargoQuantityAfterPickup = (float)(pickupCollection?.CargoQuantityAfter ?? 0), + cameraPosition = cameraPosition, + cameraForward = cameraForward, + playerPosition = playerPosition, + aimPosition = aimPosition, + fieldViewportMinX = fieldViewportMinX, + fieldViewportMinY = fieldViewportMinY, + fieldViewportMaxX = fieldViewportMaxX, + fieldViewportMaxY = fieldViewportMaxY, + fieldSplatCount = fieldSplatCount, playerRenderers = playerRendererFacts, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, @@ -918,6 +954,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float pickupQuantity; public float cargoQuantityBeforePickup; public float cargoQuantityAfterPickup; + public Vector3 cameraPosition; + public Vector3 cameraForward; + public Vector3 playerPosition; + public Vector3 aimPosition; + public float fieldViewportMinX; + public float fieldViewportMinY; + public float fieldViewportMaxX; + public float fieldViewportMaxY; + public int fieldSplatCount; public RendererFact[] playerRenderers; public int mapChannelRendererCount; public int mapChangedPixels; diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index bfb6fa3..cdfcff1 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.0", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.40", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.41", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index dd2dc39..cdae045 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.40", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.41", "depth": 0, "source": "git", "dependencies": { @@ -154,7 +154,7 @@ "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "3c8d127ef8d0d8e6c9a45f92a8bbd553d87e6a07" + "hash": "f0883e9644674849e03127d4d7a231905092f1de" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index d2dd834..57d685b 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -521,7 +521,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests "A reconciled fire request never reached daemon-owned lock acquisition and shot resolution."); observedCombat = EveUnityCombatPresentation.Find(runtime.ActiveProjection); Assert.That(observedShot.TargetEntityIndex, Is.EqualTo(targetEntityIndex)); - Assert.That(observedShot.LockQuality, Is.GreaterThan(0.99)); + Assert.That(observedShot.LockQuality, Is.GreaterThanOrEqualTo(0.99)); var trajectories = root.GetComponent(); var combatRenderer = root.GetComponent(); Assert.That(trajectories, Is.Not.Null); @@ -592,6 +592,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(aimDotDistance, Is.GreaterThanOrEqualTo(49.9f)); Assert.That(camera.cullingMask & (1 << mapLayer), Is.Zero); Assert.That(runtime.ActiveWorld.CameraRig, Is.EqualTo("perspective.entity-forward-follow.v1")); + Assert.That(runtime.ActiveWorld.CameraLookAt, Is.EqualTo("aim.convergence-point.v1")); Assert.That(runtime.ActiveWorld.CameraDistance, Is.EqualTo(30f).Within(0.001f)); Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); Assert.That(runtime.ActiveWorld.CameraPositionDamping, Is.EqualTo(0f).Within(0.001f)); @@ -703,6 +704,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests "The generic volume lowerer did not rasterize any advertised Fields layers."); Assert.That(fieldVolume.CompositeCount, Is.GreaterThan(0), "The generic volume lowerer never composited into the pilot camera."); + var fieldDocument = typeof(EveUnityFieldsVolumeRenderer) + .GetField("_document", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) + ?.GetValue(fieldVolume); + Assert.That(fieldDocument, Is.Not.Null, "The volume lowerer did not retain its presented Fields document."); + var fieldViewport = fieldDocument.GetType().GetProperty("Viewport")?.GetValue(fieldDocument); + var fieldSplats = fieldDocument.GetType().GetProperty("Splats")?.GetValue(fieldDocument); + Assert.That(fieldViewport, Is.Not.Null); + Assert.That(fieldSplats, Is.Not.Null); WriteWitnessFacts( initialVersion, runtime.ActiveVersion, @@ -744,6 +753,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests finalPickupEntityCount, pickupCollectionEventCount, pickupCollection, + camera.transform.position, + camera.transform.forward, + playerMarker.transform.position, + aimRenderer.ViewDotPosition, + Convert.ToSingle(fieldViewport.GetType().GetProperty("MinX")?.GetValue(fieldViewport)), + Convert.ToSingle(fieldViewport.GetType().GetProperty("MinY")?.GetValue(fieldViewport)), + Convert.ToSingle(fieldViewport.GetType().GetProperty("MaxX")?.GetValue(fieldViewport)), + Convert.ToSingle(fieldViewport.GetType().GetProperty("MaxY")?.GetValue(fieldViewport)), + Convert.ToInt32(fieldSplats.GetType().GetProperty("Count")?.GetValue(fieldSplats)), playerRendererFacts); } finally @@ -803,6 +821,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests int finalPickupEntityCount, int pickupCollectionEventCount, EveUnityFeedbackEvent pickupCollection, + Vector3 cameraPosition, + Vector3 cameraForward, + Vector3 playerPosition, + Vector3 aimPosition, + float fieldViewportMinX, + float fieldViewportMinY, + float fieldViewportMaxX, + float fieldViewportMaxY, + int fieldSplatCount, RendererFact[] playerRendererFacts) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); @@ -857,6 +884,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests pickupQuantity = (float)(pickupCollection?.ScalarValue ?? 0), cargoQuantityBeforePickup = (float)(pickupCollection?.CargoQuantityBefore ?? 0), cargoQuantityAfterPickup = (float)(pickupCollection?.CargoQuantityAfter ?? 0), + cameraPosition = cameraPosition, + cameraForward = cameraForward, + playerPosition = playerPosition, + aimPosition = aimPosition, + fieldViewportMinX = fieldViewportMinX, + fieldViewportMinY = fieldViewportMinY, + fieldViewportMaxX = fieldViewportMaxX, + fieldViewportMaxY = fieldViewportMaxY, + fieldSplatCount = fieldSplatCount, playerRenderers = playerRendererFacts, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, @@ -918,6 +954,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float pickupQuantity; public float cargoQuantityBeforePickup; public float cargoQuantityAfterPickup; + public Vector3 cameraPosition; + public Vector3 cameraForward; + public Vector3 playerPosition; + public Vector3 aimPosition; + public float fieldViewportMinX; + public float fieldViewportMinY; + public float fieldViewportMaxX; + public float fieldViewportMaxY; + public int fieldSplatCount; public RendererFact[] playerRenderers; public int mapChannelRendererCount; public int mapChangedPixels; diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index aa1b984..d0f3761 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,8 +4,8 @@ The warm-cache released-package witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.40`, commit - `3c8d127ef8d0d8e6c9a45f92a8bbd553d87e6a07`; +- `org.gamecult.eve.unity-scene` `0.3.41`, commit + `f0883e9644674849e03127d4d7a231905092f1de`; - `org.gamecult.eve.plugin-fields` `0.2.0`, commit `382a23d8f8a07e4b5eef5f81f84655861a858367`; - `org.gamecult.eve.surface` `0.2.2`, commit @@ -17,7 +17,7 @@ The warm-cache released-package witness passes with: - the generic `ReleaseConsumerProject` client connected directly to the Aetheria daemon. -Evidence is in `artifacts/aetheria-daemon-gravity-fog-dither`. The PlayMode test +Evidence is in `artifacts/aetheria-daemon-gravity-fog-convergence-facts`. The PlayMode test passed and recorded provider-owned reconciled movement, look, targeting, tractor press, tractor release, contact-gated cargo collection, and action receipts. The daemon-owned look direction reached the SoA body rotation, @@ -31,7 +31,9 @@ the only live directional light (`0.75` intensity). Aetheria also advertises the original undocked `ARPG.unity` Third Person Rig: entity-forward perspective follow, `30` unit distance, `60` degree vertical field of view, target screen position `0.64,0.81`, zero position damping, and -`1`-`4096` clip planes. The generic camera rig lowers those values without +`1`-`4096` clip planes. Its generic `aim.convergence-point.v1` relation makes +the optical axis reach the daemon-published aim point while preserving that +entity framing. The generic camera rig lowers both constraints without Aetheria-specific camera code. The distinct `70` unit Docked Rig no longer incorrectly owns the flight view. Aetheria's pilot surface also publishes a transparent cockpit overlay. The @@ -48,7 +50,7 @@ Fields splat layers. Its surface contract contains logical ports such as names or pass indices. The selected `unity-scene` provider asset variant owns that concrete shader ABI through `unity.volume.*` metadata. The released generic lowerer resolved the provider shader and dither texture, rasterized all -four layers, and composited `22` pilot-camera frames from daemon frame `232`. +four layers, and composited `21` pilot-camera frames from daemon frame `220`. The provider variant advertises the fossil shader's raymarch, temporal-history, and composite passes plus their logical ports. EveUnity allocates and resets the history targets generically; a partial temporal ABI fails closed. @@ -63,10 +65,10 @@ Camera-channel facts: - provider-authored player renderers: `13`, including the lowered tractor effect; - pilot changed pixels: `230,400`; -- pilot average luminance: `0.1893132`; -- pilot bright pixels: `13,948`; +- pilot average luminance: `0.1912306`; +- pilot bright pixels: `13,715`; - map-channel renderers: `11`; -- map changed pixels: `4,365`; +- map changed pixels: `4,867`; - native cockpit progress bars: `7`; - daemon tractor power at capture: `1.0` after the held input completed its authored ramp; @@ -85,14 +87,18 @@ Visual inspection confirms that map glyphs are absent from the pilot frame and present in the map-only frame. Correcting the fossil camera mode removed the false top-down dark disk, but the active volume now resolves as a nearly uniform saturated orange horizon rather than the fossil's blue gravity-shaped fog sea. -This is transport and lowering proof, not visual parity. Release `0.3.40` now +This is transport and lowering proof, not visual parity. Release `0.3.41` now derives dither sampling coordinates from the active viewport and provider -texture dimensions through a generic logical-port relation. The resulting -raymarch remains nearly flat orange (`R 0.8896-0.8960`, `G 0.5493-0.5532`, -`B 0.3096-0.3118`, alpha `0`), so that real ABI correction is not the missing -visual mechanism. The next diagnosis must compare camera/world sampling and -field viewport placement before changing authored tuning. EveUnity must not -compensate with Aetheria-specific lighting or fog rules. +texture dimensions through a generic logical-port relation and solves the +advertised finite aim look-at exactly. The camera (`6.57,-13.83,1.44`), player +(`-3.20,0,38.77`), and aim (`-46.55,0,119.19`) all lie inside the advertised +field viewport, but the resulting raymarch remains nearly flat orange +(`R 0.8896-0.8960`, `G 0.5493-0.5532`, `B 0.3096-0.3118`, alpha `0`). The +remaining cause is now isolated to the daemon's field document: it flattened +the fossil radial gravity terrain into a full-viewport solid height splat, so +the first ray sample saturates floor density. EveUnity must not compensate with +Aetheria-specific lighting or fog rules; the document must publish the actual +portable radial falloff. Substance is not part of this path; later texture baking belongs in Blender. Aetheria currently bundles ambientCG's 1K `Metal012` color, normal, and metalness maps under CC0. The same pre-generated maps now replace dead diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 3350577..9202306 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "3c8d127ef8d0d8e6c9a45f92a8bbd553d87e6a07" +$expectedEveUnityCommit = "f0883e9644674849e03127d4d7a231905092f1de" $expectedEveFieldsCommit = "382a23d8f8a07e4b5eef5f81f84655861a858367" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" From 441471949a22a0183da1ecac4a4a1d0dc2a7618e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 22:42:05 +0100 Subject: [PATCH 092/292] Lower portable power pulse field splats --- .../Runtime/Fields/EveFieldsSplatBuffer.cs | 4 +- .../Runtime/Fields/EveFieldsSplatsCore.hlsl | 14 +++++- .../EveFieldsUnitySceneProjectionTests.cs | 45 +++++++++++++++++++ .../org.gamecult.eve.unity-scene/package.json | 4 +- 4 files changed, 62 insertions(+), 5 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs index ddd6abb..ecbb2a4 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs @@ -12,7 +12,7 @@ namespace GameCult.Eve.UnityScene.Fields public const string ViewportToClipPropertyName = "_EveFieldsViewportToClip"; public const string ChannelFilterPropertyName = "_EveFieldsChannelFilter"; - private const int SplatStrideBytes = 80; + private const int SplatStrideBytes = 96; private EveFieldsGpuSplat[] splats = Array.Empty(); private GraphicsBuffer buffer; private int capacity; @@ -54,6 +54,7 @@ namespace GameCult.Eve.UnityScene.Fields RotationChannelFalloff = new Vector4((float)Read(source.RotationCos, index, 1), (float)Read(source.RotationSin, index, 0), channel, Read(source.Falloff, index, EveFieldsSplatFalloffs.Smooth)), LayerSource = new Vector4(layer, Read(source.SourceKind, index, EveFieldsSplatSourceKinds.Constant), (float)Read(source.AnimationSpeed, index, 0), (float)Read(source.SourceFlags, index, 0)), SourceFrequencyPhase = new Vector4((float)Read(source.FrequencyX, index, 1), (float)Read(source.FrequencyY, index, 1), (float)Read(source.PhaseX, index, 0), (float)Read(source.PhaseY, index, 0)), + FalloffParameters = new Vector4((float)Read(source.FalloffScale, index, 1), (float)Read(source.FalloffExponent, index, 1), 0, 0), Value = new Vector4((float)Read(source.ValueR, index, 0), (float)Read(source.ValueG, index, 0), (float)Read(source.ValueB, index, 0), (float)Read(source.ValueA, index, 1)) }; } @@ -99,6 +100,7 @@ namespace GameCult.Eve.UnityScene.Fields public Vector4 RotationChannelFalloff; public Vector4 LayerSource; public Vector4 SourceFrequencyPhase; + public Vector4 FalloffParameters; public Vector4 Value; } } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl index a7ec7a9..a37a411 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl @@ -7,6 +7,7 @@ struct EveFieldsSplat float4 rotationChannelFalloff; float4 layerSource; float4 sourceFrequencyPhase; + float4 falloffParameters; float4 value; }; @@ -58,12 +59,17 @@ Varyings Vert(uint vertexId : SV_VertexID, uint instanceId : SV_InstanceID) return output; } -float ResolveFalloff(float2 localUv, int falloff) +float ResolveFalloff(float2 localUv, int falloff, float scale, float exponent) { float distance01 = saturate(length(localUv)); if (falloff == 0) return 1; if (falloff == 1) return saturate(1 - distance01); if (falloff == 3) return smoothstep(0, 1, distance01); + if (falloff == 4) + { + float pulseDistance = distance01 * max(0, scale); + return pow(saturate(1 - pulseDistance * pulseDistance), max(0.0001, exponent)); + } return 1 - smoothstep(0, 1, distance01); } @@ -72,7 +78,11 @@ float4 Frag(Varyings input) : SV_Target EveFieldsSplat splat = _EveFieldsSplats[input.instanceId]; int channel = (int)round(splat.rotationChannelFalloff.z); if (_EveFieldsChannelFilter >= 0 && channel != _EveFieldsChannelFilter) discard; - float alpha = ResolveFalloff(input.localUv, (int)round(splat.rotationChannelFalloff.w)); + float alpha = ResolveFalloff( + input.localUv, + (int)round(splat.rotationChannelFalloff.w), + splat.falloffParameters.x, + splat.falloffParameters.y); clip(alpha - 0.0001); int sourceKind = (int)round(splat.layerSource.y); float source = 1; diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index cf10e64..365d2e4 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -131,6 +131,49 @@ namespace GameCult.Eve.UnityScene.Tests UnityEngine.Object.DestroyImmediate(host); } + [Test] + public void RasterizerLowersPortablePowerPulseParameters() + { + var document = new EveFieldsSplatsDocument + { + Viewport = new EveFieldsViewport { MinX = -1, MinY = -1, MaxX = 1, MaxY = 1 }, + Splats = new EveFieldsSplatSoa + { + Count = 1, + CenterX = new[] { 0d }, + CenterY = new[] { 0d }, + HalfExtentX = new[] { 1d }, + HalfExtentY = new[] { 1d }, + RotationCos = new[] { 1d }, + RotationSin = new[] { 0d }, + Channel = new[] { 0 }, + Falloff = new[] { EveFieldsSplatFalloffs.PowerPulse }, + FalloffScale = new[] { 2d }, + FalloffExponent = new[] { 1d }, + ValueR = new[] { 1d }, + ValueA = new[] { 1d }, + LayerIndex = new[] { 0 }, + SourceKind = new[] { EveFieldsSplatSourceKinds.Constant } + } + }; + var host = new GameObject("eve-fields-power-pulse"); + var rasterizer = host.AddComponent(); + var output = rasterizer.Render(document, 128, 128); + var previous = RenderTexture.active; + RenderTexture.active = output; + var readback = new Texture2D(128, 128, TextureFormat.RGBAFloat, false, true); + readback.ReadPixels(new Rect(0, 0, 128, 128), 0, 0); + readback.Apply(); + RenderTexture.active = previous; + + Assert.That(readback.GetPixel(64, 64).r, Is.GreaterThan(0.95f)); + Assert.That(readback.GetPixel(80, 64).r, Is.InRange(0.65f, 0.85f)); + Assert.That(readback.GetPixel(96, 64).r, Is.LessThan(0.02f)); + + UnityEngine.Object.DestroyImmediate(readback); + UnityEngine.Object.DestroyImmediate(host); + } + private sealed class CaptureDocument : IEveFieldsSplatsDocument { public string Schema => EveFieldsSchemas.Splats; @@ -188,6 +231,8 @@ namespace GameCult.Eve.UnityScene.Tests public IReadOnlyList PhaseY => Zero; public IReadOnlyList AnimationSpeed => Zero; public IReadOnlyList SourceFlags => Zero; + public IReadOnlyList FalloffScale => One; + public IReadOnlyList FalloffExponent => One; } } } diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 1dc9489..87d9d36 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,12 +1,12 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.41", + "version": "0.3.42", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { "org.gamecult.cultlib": "1.0.13", - "org.gamecult.eve.plugin-fields": "0.2.0", + "org.gamecult.eve.plugin-fields": "0.2.1", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" From 76126c27ca8edde8088cbcdd87dedd86a3d1ca35 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 23:00:06 +0100 Subject: [PATCH 093/292] Pin radial gravity fog witness releases --- ReleaseConsumerProject/Packages/manifest.json | 4 +- .../Packages/packages-lock.json | 10 ++--- docs/aetheria-released-package-witness.md | 43 ++++++++++--------- scripts/run-aetheria-daemon-world-witness.ps1 | 4 +- 4 files changed, 31 insertions(+), 30 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index cdfcff1..5ea971e 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -7,9 +7,9 @@ "com.unity.modules.uielements": "1.0.0", "com.unity.render-pipelines.universal": "17.4.0", "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", - "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.0", + "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.1", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.41", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.42", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index cdae045..3dea9de 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -126,13 +126,13 @@ "hash": "feb5c71513e71d681699f462fe3682b3168c6f73" }, "org.gamecult.eve.plugin-fields": { - "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.0", + "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.1", "depth": 0, "source": "git", "dependencies": { "org.gamecult.cultlib": "1.0.13" }, - "hash": "382a23d8f8a07e4b5eef5f81f84655861a858367" + "hash": "59924e324cd10f293c214c8e6bb2c794a32d39a0" }, "org.gamecult.eve.surface": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", @@ -144,17 +144,17 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.41", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.42", "depth": 0, "source": "git", "dependencies": { "org.gamecult.cultlib": "1.0.13", - "org.gamecult.eve.plugin-fields": "0.2.0", + "org.gamecult.eve.plugin-fields": "0.2.1", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "f0883e9644674849e03127d4d7a231905092f1de" + "hash": "441471949a22a0183da1ecac4a4a1d0dc2a7618e" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index d0f3761..f4d1ac1 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,10 +4,10 @@ The warm-cache released-package witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.41`, commit - `f0883e9644674849e03127d4d7a231905092f1de`; -- `org.gamecult.eve.plugin-fields` `0.2.0`, commit - `382a23d8f8a07e4b5eef5f81f84655861a858367`; +- `org.gamecult.eve.unity-scene` `0.3.42`, commit + `441471949a22a0183da1ecac4a4a1d0dc2a7618e`; +- `org.gamecult.eve.plugin-fields` `0.2.1`, commit + `59924e324cd10f293c214c8e6bb2c794a32d39a0`; - `org.gamecult.eve.surface` `0.2.2`, commit `140e1bd963a0033e66777a3b2c5fe6e9c97dfe32`; - `org.gamecult.eve.unity-uitoolkit` `0.1.1`, commit @@ -17,7 +17,7 @@ The warm-cache released-package witness passes with: - the generic `ReleaseConsumerProject` client connected directly to the Aetheria daemon. -Evidence is in `artifacts/aetheria-daemon-gravity-fog-convergence-facts`. The PlayMode test +Evidence is in `artifacts/aetheria-daemon-gravity-fog-power-pulse`. The PlayMode test passed and recorded provider-owned reconciled movement, look, targeting, tractor press, tractor release, contact-gated cargo collection, and action receipts. The daemon-owned look direction reached the SoA body rotation, @@ -50,7 +50,7 @@ Fields splat layers. Its surface contract contains logical ports such as names or pass indices. The selected `unity-scene` provider asset variant owns that concrete shader ABI through `unity.volume.*` metadata. The released generic lowerer resolved the provider shader and dither texture, rasterized all -four layers, and composited `21` pilot-camera frames from daemon frame `220`. +four layers, and composited `22` pilot-camera frames from daemon frame `223`. The provider variant advertises the fossil shader's raymarch, temporal-history, and composite passes plus their logical ports. EveUnity allocates and resets the history targets generically; a partial temporal ABI fails closed. @@ -65,10 +65,10 @@ Camera-channel facts: - provider-authored player renderers: `13`, including the lowered tractor effect; - pilot changed pixels: `230,400`; -- pilot average luminance: `0.1912306`; -- pilot bright pixels: `13,715`; +- pilot average luminance: `0.0910102`; +- pilot bright pixels: `969`; - map-channel renderers: `11`; -- map changed pixels: `4,867`; +- map changed pixels: `4,850`; - native cockpit progress bars: `7`; - daemon tractor power at capture: `1.0` after the held input completed its authored ramp; @@ -87,18 +87,19 @@ Visual inspection confirms that map glyphs are absent from the pilot frame and present in the map-only frame. Correcting the fossil camera mode removed the false top-down dark disk, but the active volume now resolves as a nearly uniform saturated orange horizon rather than the fossil's blue gravity-shaped fog sea. -This is transport and lowering proof, not visual parity. Release `0.3.41` now -derives dither sampling coordinates from the active viewport and provider -texture dimensions through a generic logical-port relation and solves the -advertised finite aim look-at exactly. The camera (`6.57,-13.83,1.44`), player -(`-3.20,0,38.77`), and aim (`-46.55,0,119.19`) all lie inside the advertised -field viewport, but the resulting raymarch remains nearly flat orange -(`R 0.8896-0.8960`, `G 0.5493-0.5532`, `B 0.3096-0.3118`, alpha `0`). The -remaining cause is now isolated to the daemon's field document: it flattened -the fossil radial gravity terrain into a full-viewport solid height splat, so -the first ray sample saturates floor density. EveUnity must not compensate with -Aetheria-specific lighting or fog rules; the document must publish the actual -portable radial falloff. +This is transport and lowering proof, not visual parity. Release `0.3.42` +adds the generic Eve Fields `PowerPulse` GPU falloff to the existing dither-scale, +temporal-history, and finite-look-at lowerers. Aetheria now publishes the fossil +radial zone/body brushes instead of a viewport-sized solid slab, keeps negative +`gravity.height` separate from positive `fog.surface_height`, and derives entity +altitude as fossil terrain height plus hull grid offset after each Ymir XZ step. +The witness records player Y `-92.82` and camera Y `-109.97`; Unity does not +sample terrain to invent either value. The volume is no longer a flat saturated +orange: temporal cloud RGB spans `0.325-0.537`, `0.201-0.332`, `0.113-0.187` +with decoded density `0.334-0.673`. Visual inspection still shows a dark brown, +low-contrast field rather than the fossil's blue gravity-fog sea, so color, +lighting, patch structure, and gravity-wave parity remain open. EveUnity must +not compensate with Aetheria-specific lighting or fog rules. Substance is not part of this path; later texture baking belongs in Blender. Aetheria currently bundles ambientCG's 1K `Metal012` color, normal, and metalness maps under CC0. The same pre-generated maps now replace dead diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 9202306..8065c74 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,8 +13,8 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "f0883e9644674849e03127d4d7a231905092f1de" -$expectedEveFieldsCommit = "382a23d8f8a07e4b5eef5f81f84655861a858367" +$expectedEveUnityCommit = "441471949a22a0183da1ecac4a4a1d0dc2a7618e" +$expectedEveFieldsCommit = "59924e324cd10f293c214c8e6bb2c794a32d39a0" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot From 8feeefe6a50922af7ea613c943149b1f9c98c54b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 23:15:45 +0100 Subject: [PATCH 094/292] Lower world-anchored procedural fields --- .../Runtime/Fields/EveFieldsSplatBuffer.cs | 1 + .../Fields/EveFieldsSplatRasterizer.cs | 2 + .../Runtime/Fields/EveFieldsSplatsCore.hlsl | 117 +++++++++++++++--- .../EveFieldsUnitySceneProjectionTests.cs | 88 +++++++++++++ .../Third Party Notices.md | 27 ++++ .../Third Party Notices.md.meta | 7 ++ .../org.gamecult.eve.unity-scene/package.json | 4 +- 7 files changed, 228 insertions(+), 18 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-scene/Third Party Notices.md create mode 100644 packages/org.gamecult.eve.unity-scene/Third Party Notices.md.meta diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs index ecbb2a4..dc94d57 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs @@ -11,6 +11,7 @@ namespace GameCult.Eve.UnityScene.Fields public const string SplatCountPropertyName = "_EveFieldsSplatCount"; public const string ViewportToClipPropertyName = "_EveFieldsViewportToClip"; public const string ChannelFilterPropertyName = "_EveFieldsChannelFilter"; + public const string SimulationTimePropertyName = "_EveFieldsSimulationTimeSeconds"; private const int SplatStrideBytes = 96; private EveFieldsGpuSplat[] splats = Array.Empty(); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs index b8638a5..c87f90b 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs @@ -26,6 +26,7 @@ namespace GameCult.Eve.UnityScene.Fields private static readonly int SplatCountId = Shader.PropertyToID(EveFieldsSplatBuffer.SplatCountPropertyName); private static readonly int ViewportToClipId = Shader.PropertyToID(EveFieldsSplatBuffer.ViewportToClipPropertyName); private static readonly int ChannelFilterId = Shader.PropertyToID(EveFieldsSplatBuffer.ChannelFilterPropertyName); + private static readonly int SimulationTimeId = Shader.PropertyToID(EveFieldsSplatBuffer.SimulationTimePropertyName); public RenderTexture TargetTexture => targetTexture != null ? targetTexture : ownedTargetTexture; public int LastDrawnCount { get; private set; } @@ -48,6 +49,7 @@ namespace GameCult.Eve.UnityScene.Fields commandBuffer.SetGlobalBuffer(SplatBufferId, splatBuffer.Buffer); commandBuffer.SetGlobalInt(SplatCountId, splatBuffer.Count); commandBuffer.SetGlobalInt(ChannelFilterId, channelFilter); + commandBuffer.SetGlobalFloat(SimulationTimeId, (float)document.SimulationTimeSeconds); commandBuffer.SetGlobalMatrix(ViewportToClipId, BuildViewportToClip(document.Viewport)); commandBuffer.DrawProcedural(Matrix4x4.identity, material, Mathf.Max(0, materialPass), MeshTopology.Triangles, 6, splatBuffer.Count); Graphics.ExecuteCommandBuffer(commandBuffer); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl index a37a411..d40dce3 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl @@ -15,31 +15,105 @@ StructuredBuffer _EveFieldsSplats; float4x4 _EveFieldsViewportToClip; int _EveFieldsSplatCount; int _EveFieldsChannelFilter; +float _EveFieldsSimulationTimeSeconds; float _ValueScale; -float hash21(float2 p) +// Ashima Arts webgl-noise, translated by Keijiro Takahashi. +// Copyright (C) 2011 Ashima Arts. Distributed under the MIT License. +float3 EveFieldsMod289(float3 x) { - p = frac(p * float2(123.34, 456.21)); - p += dot(p, p + 45.32); - return frac(p.x * p.y); + return x - floor(x / 289.0) * 289.0; } -float valueNoise(float2 p) +float4 EveFieldsMod289(float4 x) { - float2 i = floor(p); - float2 f = frac(p); - float a = hash21(i); - float b = hash21(i + float2(1, 0)); - float c = hash21(i + float2(0, 1)); - float d = hash21(i + float2(1, 1)); - float2 u = f * f * (3 - 2 * f); - return lerp(lerp(a, b, u.x), lerp(c, d, u.x), u.y) * 2 - 1; + return x - floor(x / 289.0) * 289.0; +} + +float4 EveFieldsPermute(float4 x) +{ + return EveFieldsMod289((x * 34.0 + 1.0) * x); +} + +float4 EveFieldsTaylorInvSqrt(float4 r) +{ + return 1.79284291400159 - r * 0.85373472095314; +} + +float EveFieldsSimplexNoise3D(float3 v) +{ + const float2 C = float2(1.0 / 6.0, 1.0 / 3.0); + float3 i = floor(v + dot(v, C.yyy)); + float3 x0 = v - i + dot(i, C.xxx); + float3 g = step(x0.yzx, x0.xyz); + float3 l = 1.0 - g; + float3 i1 = min(g.xyz, l.zxy); + float3 i2 = max(g.xyz, l.zxy); + float3 x1 = x0 - i1 + C.xxx; + float3 x2 = x0 - i2 + C.yyy; + float3 x3 = x0 - 0.5; + i = EveFieldsMod289(i); + float4 p = EveFieldsPermute(EveFieldsPermute(EveFieldsPermute( + i.z + float4(0.0, i1.z, i2.z, 1.0)) + + i.y + float4(0.0, i1.y, i2.y, 1.0)) + + i.x + float4(0.0, i1.x, i2.x, 1.0)); + float4 j = p - 49.0 * floor(p / 49.0); + float4 x_ = floor(j / 7.0); + float4 y_ = floor(j - 7.0 * x_); + float4 x = (x_ * 2.0 + 0.5) / 7.0 - 1.0; + float4 y = (y_ * 2.0 + 0.5) / 7.0 - 1.0; + float4 h = 1.0 - abs(x) - abs(y); + float4 b0 = float4(x.xy, y.xy); + float4 b1 = float4(x.zw, y.zw); + float4 s0 = floor(b0) * 2.0 + 1.0; + float4 s1 = floor(b1) * 2.0 + 1.0; + float4 sh = -step(h, 0.0); + float4 a0 = b0.xzyw + s0.xzyw * sh.xxyy; + float4 a1 = b1.xzyw + s1.xzyw * sh.zzww; + float3 g0 = float3(a0.xy, h.x); + float3 g1 = float3(a0.zw, h.y); + float3 g2 = float3(a1.xy, h.z); + float3 g3 = float3(a1.zw, h.w); + float4 norm = EveFieldsTaylorInvSqrt(float4( + dot(g0, g0), dot(g1, g1), dot(g2, g2), dot(g3, g3))); + g0 *= norm.x; + g1 *= norm.y; + g2 *= norm.z; + g3 *= norm.w; + float4 m = max(0.6 - float4( + dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0); + m *= m; + m *= m; + return 42.0 * dot(m, float4( + dot(x0, g0), dot(x1, g1), dot(x2, g2), dot(x3, g3))); +} + +float EveFieldsCellDistanceB(float2 p) +{ + p = frac(p) - 0.5; + return (length(p) * 1.5 + 0.25) * + max(abs(p.x) * 0.866 + p.y * 0.5, -p.y); +} + +float EveFieldsAnimatedCellNoiseB(float2 p, float time) +{ + float2 o = sin(float2(1.93, 0.0) + time) * 0.166; + float a = EveFieldsCellDistanceB(p + float2(o.x, 0)); + float b = EveFieldsCellDistanceB(p + float2(0, 0.5 + o.y)); + p = mul(p + 0.5, -float2x2(0.5, -0.866, 0.866, 0.5)); + float c = EveFieldsCellDistanceB(p + float2(o.x, 0)); + float d = EveFieldsCellDistanceB(p + float2(0, 0.5 + o.y)); + p = mul(p + 0.5, -float2x2(0.5, -0.866, 0.866, 0.5)); + float e = EveFieldsCellDistanceB(p + float2(o.x, 0)); + float f = EveFieldsCellDistanceB(p + float2(0, 0.5 + o.y)); + return min(min(min(a, b), min(c, d)), min(e, f)) * 2.0; } struct Varyings { float4 position : SV_POSITION; float2 localUv : TEXCOORD0; + float2 fieldWorld : TEXCOORD2; nointerpolation uint instanceId : TEXCOORD1; }; @@ -55,6 +129,7 @@ Varyings Vert(uint vertexId : SV_VertexID, uint instanceId : SV_InstanceID) Varyings output; output.position = mul(_EveFieldsViewportToClip, float4(world, 0, 1)); output.localUv = local; + output.fieldWorld = world; output.instanceId = instanceId; return output; } @@ -88,10 +163,20 @@ float4 Frag(Varyings input) : SV_Target float source = 1; if (sourceKind == 1 || sourceKind == 2) { - float timeOffset = sourceKind == 2 ? _Time.y * splat.layerSource.z : 0; - source = valueNoise(input.localUv * splat.sourceFrequencyPhase.xy + splat.sourceFrequencyPhase.zw + timeOffset); - if (splat.layerSource.w != 0) source = abs(source); + float timeCoordinate = sourceKind == 2 + ? _EveFieldsSimulationTimeSeconds * splat.layerSource.z + : 0; + source = EveFieldsSimplexNoise3D(float3( + input.fieldWorld * splat.sourceFrequencyPhase.xy + splat.sourceFrequencyPhase.zw, + timeCoordinate)); } + else if (sourceKind == 3) + { + source = EveFieldsAnimatedCellNoiseB( + input.fieldWorld * splat.sourceFrequencyPhase.xy + splat.sourceFrequencyPhase.zw, + _EveFieldsSimulationTimeSeconds * splat.layerSource.z); + } + if (((int)round(splat.layerSource.w) & 1) != 0) source = abs(source); return splat.value * (alpha * source * _ValueScale); } #endif diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index 365d2e4..32d9451 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -174,6 +174,94 @@ namespace GameCult.Eve.UnityScene.Tests UnityEngine.Object.DestroyImmediate(host); } + [Test] + public void RasterizerAnchorsAnimatedSimplexToFieldWorldAndPublishedSimulationTime() + { + var first = ProceduralDocument( + 0, 100, 50, EveFieldsSplatSourceKinds.AnimatedSimplexNoise, 2); + var shifted = ProceduralDocument( + 25, 125, 75, EveFieldsSplatSourceKinds.AnimatedSimplexNoise, 2); + var later = ProceduralDocument( + 0, 100, 50, EveFieldsSplatSourceKinds.AnimatedSimplexNoise, 7); + + var firstValue = RenderFieldPixel(first, 64, 64); + var shiftedValue = RenderFieldPixel(shifted, 32, 64); + var laterValue = RenderFieldPixel(later, 64, 64); + + Assert.That(shiftedValue, Is.EqualTo(firstValue).Within(0.015f), + "the same field-world coordinate must not swim when the viewport moves"); + Assert.That(Mathf.Abs(laterValue - firstValue), Is.GreaterThan(0.02f), + "animation must use the document's simulation time rather than Unity wall time"); + } + + [Test] + public void RasterizerLowersPortableAnimatedCellNoiseB() + { + var first = ProceduralDocument( + 0, 100, 50, EveFieldsSplatSourceKinds.AnimatedCellNoiseB, 2); + var later = ProceduralDocument( + 0, 100, 50, EveFieldsSplatSourceKinds.AnimatedCellNoiseB, 11); + + var firstValue = RenderFieldPixel(first, 57, 71); + var laterValue = RenderFieldPixel(later, 57, 71); + + Assert.That(firstValue, Is.GreaterThanOrEqualTo(0)); + Assert.That(Mathf.Abs(laterValue - firstValue), Is.GreaterThan(0.005f)); + } + + private static EveFieldsSplatsDocument ProceduralDocument( + double min, + double max, + double center, + int sourceKind, + double simulationTime) + { + return new EveFieldsSplatsDocument + { + SimulationTimeSeconds = simulationTime, + Viewport = new EveFieldsViewport { MinX = min, MinY = 0, MaxX = max, MaxY = 100 }, + Splats = new EveFieldsSplatSoa + { + Count = 1, + CenterX = new[] { center }, + CenterY = new[] { 50d }, + HalfExtentX = new[] { 50d }, + HalfExtentY = new[] { 50d }, + RotationCos = new[] { 1d }, + RotationSin = new[] { 0d }, + Channel = new[] { 0 }, + Falloff = new[] { EveFieldsSplatFalloffs.Solid }, + ValueR = new[] { 1d }, + ValueA = new[] { 1d }, + LayerIndex = new[] { 0 }, + SourceKind = new[] { sourceKind }, + FrequencyX = new[] { 0.037d }, + FrequencyY = new[] { 0.041d }, + PhaseX = new[] { 0.17d }, + PhaseY = new[] { -0.23d }, + AnimationSpeed = new[] { 0.31d }, + SourceFlags = new[] { (double)EveFieldsSplatSourceFlags.AbsoluteValue } + } + }; + } + + private static float RenderFieldPixel(EveFieldsSplatsDocument document, int x, int y) + { + var host = new GameObject("eve-fields-procedural-capture"); + var rasterizer = host.AddComponent(); + var output = rasterizer.Render(document, 128, 128); + var previous = RenderTexture.active; + RenderTexture.active = output; + var readback = new Texture2D(128, 128, TextureFormat.RGBAFloat, false, true); + readback.ReadPixels(new Rect(0, 0, 128, 128), 0, 0); + readback.Apply(); + RenderTexture.active = previous; + var value = readback.GetPixel(x, y).r; + UnityEngine.Object.DestroyImmediate(readback); + UnityEngine.Object.DestroyImmediate(host); + return value; + } + private sealed class CaptureDocument : IEveFieldsSplatsDocument { public string Schema => EveFieldsSchemas.Splats; diff --git a/packages/org.gamecult.eve.unity-scene/Third Party Notices.md b/packages/org.gamecult.eve.unity-scene/Third Party Notices.md new file mode 100644 index 0000000..e66c613 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Third Party Notices.md @@ -0,0 +1,27 @@ +# Third-party notices + +## Ashima Arts webgl-noise + +The portable 3D simplex implementation in `Runtime/Fields/EveFieldsSplatsCore.hlsl` +is derived from Ashima Arts' `webgl-noise`, translated to Unity shader syntax by +Keijiro Takahashi. + +Copyright (C) 2011 Ashima Arts. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/org.gamecult.eve.unity-scene/Third Party Notices.md.meta b/packages/org.gamecult.eve.unity-scene/Third Party Notices.md.meta new file mode 100644 index 0000000..4745e42 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Third Party Notices.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5702faba00994033870c858bb2124d91 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 87d9d36..0c48786 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,12 +1,12 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.42", + "version": "0.3.43", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { "org.gamecult.cultlib": "1.0.13", - "org.gamecult.eve.plugin-fields": "0.2.1", + "org.gamecult.eve.plugin-fields": "0.2.2", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" From 1e0bbfe1c542fbecc09e6d1ef60a6e38a633f1f7 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 23:18:16 +0100 Subject: [PATCH 095/292] Pin procedural field releases in consumer --- ReleaseConsumerProject/Packages/manifest.json | 4 ++-- ReleaseConsumerProject/Packages/packages-lock.json | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 5ea971e..4247800 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -7,9 +7,9 @@ "com.unity.modules.uielements": "1.0.0", "com.unity.render-pipelines.universal": "17.4.0", "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", - "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.1", + "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.2", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.42", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.43", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 3dea9de..9e611ab 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -126,13 +126,13 @@ "hash": "feb5c71513e71d681699f462fe3682b3168c6f73" }, "org.gamecult.eve.plugin-fields": { - "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.1", + "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.2", "depth": 0, "source": "git", "dependencies": { "org.gamecult.cultlib": "1.0.13" }, - "hash": "59924e324cd10f293c214c8e6bb2c794a32d39a0" + "hash": "c4a61e818e0b15b4504d76952910738eeeaf6041" }, "org.gamecult.eve.surface": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", @@ -144,17 +144,17 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.42", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.43", "depth": 0, "source": "git", "dependencies": { "org.gamecult.cultlib": "1.0.13", - "org.gamecult.eve.plugin-fields": "0.2.1", + "org.gamecult.eve.plugin-fields": "0.2.2", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "441471949a22a0183da1ecac4a4a1d0dc2a7618e" + "hash": "8feeefe6a50922af7ea613c943149b1f9c98c54b" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", From 03ce78031f28d7bfae4dcdc477a7273c6ecae55b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 23:38:00 +0100 Subject: [PATCH 096/292] Lower portable radial cosine fields --- TestProject/Packages/packages-lock.json | 2 +- .../Runtime/Fields/EveFieldsSplatsCore.hlsl | 9 ++++ .../EveFieldsUnitySceneProjectionTests.cs | 48 +++++++++++++++++++ .../org.gamecult.eve.unity-scene/package.json | 4 +- 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/TestProject/Packages/packages-lock.json b/TestProject/Packages/packages-lock.json index eb007d8..ea3990d 100644 --- a/TestProject/Packages/packages-lock.json +++ b/TestProject/Packages/packages-lock.json @@ -51,7 +51,7 @@ "source": "local", "dependencies": { "org.gamecult.cultlib": "1.0.13", - "org.gamecult.eve.plugin-fields": "0.2.0", + "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl index d40dce3..316316d 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl @@ -176,6 +176,15 @@ float4 Frag(Varyings input) : SV_Target input.fieldWorld * splat.sourceFrequencyPhase.xy + splat.sourceFrequencyPhase.zw, _EveFieldsSimulationTimeSeconds * splat.layerSource.z); } + else if (sourceKind == 4) + { + float distance01 = length(input.localUv); + float radialExponent = max(0.0001, splat.sourceFrequencyPhase.y); + source = cos( + pow(distance01, radialExponent) * splat.sourceFrequencyPhase.x + + splat.sourceFrequencyPhase.z + + _EveFieldsSimulationTimeSeconds * splat.layerSource.z); + } if (((int)round(splat.layerSource.w) & 1) != 0) source = abs(source); return splat.value * (alpha * source * _ValueScale); } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index 32d9451..7be1fc4 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -209,6 +209,54 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(Mathf.Abs(laterValue - firstValue), Is.GreaterThan(0.005f)); } + [Test] + public void RasterizerLowersPortableAnimatedRadialCosine() + { + var initial = RadialCosineDocument(0); + var halfTurnLater = RadialCosineDocument(Math.PI); + + var center = RenderFieldPixel(initial, 64, 64); + var radial = RenderFieldPixel(initial, 80, 64); + var laterCenter = RenderFieldPixel(halfTurnLater, 64, 64); + var localX = (80.5f / 128f) * 2f - 1f; + var localY = (64.5f / 128f) * 2f - 1f; + var expected = Mathf.Cos(Mathf.Pow(Mathf.Sqrt(localX * localX + localY * localY), 1.25f) * Mathf.PI * 2f); + + Assert.That(center, Is.GreaterThan(0.98f)); + Assert.That(radial, Is.EqualTo(expected).Within(0.04f)); + Assert.That(laterCenter, Is.LessThan(-0.98f), + "radial phase animation must use provider-published simulation time"); + } + + private static EveFieldsSplatsDocument RadialCosineDocument(double simulationTime) + { + return new EveFieldsSplatsDocument + { + SimulationTimeSeconds = simulationTime, + Viewport = new EveFieldsViewport { MinX = -1, MinY = -1, MaxX = 1, MaxY = 1 }, + Splats = new EveFieldsSplatSoa + { + Count = 1, + CenterX = new[] { 0d }, + CenterY = new[] { 0d }, + HalfExtentX = new[] { 1d }, + HalfExtentY = new[] { 1d }, + RotationCos = new[] { 1d }, + RotationSin = new[] { 0d }, + Channel = new[] { 0 }, + Falloff = new[] { EveFieldsSplatFalloffs.Solid }, + ValueR = new[] { 1d }, + ValueA = new[] { 1d }, + LayerIndex = new[] { 0 }, + SourceKind = new[] { EveFieldsSplatSourceKinds.AnimatedRadialCosine }, + FrequencyX = new[] { Math.PI * 2d }, + FrequencyY = new[] { 1.25d }, + PhaseX = new[] { 0d }, + AnimationSpeed = new[] { 1d } + } + }; + } + private static EveFieldsSplatsDocument ProceduralDocument( double min, double max, diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 0c48786..4b65aa3 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,12 +1,12 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.43", + "version": "0.3.44", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { "org.gamecult.cultlib": "1.0.13", - "org.gamecult.eve.plugin-fields": "0.2.2", + "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" From 359dcbda9ad88f8400c71f7caa3125e566c53a7f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 23:40:53 +0100 Subject: [PATCH 097/292] Pin radial cosine field releases --- ReleaseConsumerProject/Packages/manifest.json | 4 ++-- ReleaseConsumerProject/Packages/packages-lock.json | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 4247800..5f86b43 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -7,9 +7,9 @@ "com.unity.modules.uielements": "1.0.0", "com.unity.render-pipelines.universal": "17.4.0", "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", - "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.2", + "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.43", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.44", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 9e611ab..2b81933 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -126,13 +126,13 @@ "hash": "feb5c71513e71d681699f462fe3682b3168c6f73" }, "org.gamecult.eve.plugin-fields": { - "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.2", + "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "depth": 0, "source": "git", "dependencies": { "org.gamecult.cultlib": "1.0.13" }, - "hash": "c4a61e818e0b15b4504d76952910738eeeaf6041" + "hash": "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" }, "org.gamecult.eve.surface": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", @@ -144,17 +144,17 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.43", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.44", "depth": 0, "source": "git", "dependencies": { "org.gamecult.cultlib": "1.0.13", - "org.gamecult.eve.plugin-fields": "0.2.2", + "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "8feeefe6a50922af7ea613c943149b1f9c98c54b" + "hash": "03ce78031f28d7bfae4dcdc477a7273c6ecae55b" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", From 8f3872079ef0089a051b225b049506ab9c86792b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 23:58:13 +0100 Subject: [PATCH 098/292] Record radial fog released witness --- docs/aetheria-released-package-witness.md | 52 +++++++++---------- scripts/run-aetheria-daemon-world-witness.ps1 | 4 +- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index f4d1ac1..b738502 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,10 +4,10 @@ The warm-cache released-package witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.42`, commit - `441471949a22a0183da1ecac4a4a1d0dc2a7618e`; -- `org.gamecult.eve.plugin-fields` `0.2.1`, commit - `59924e324cd10f293c214c8e6bb2c794a32d39a0`; +- `org.gamecult.eve.unity-scene` `0.3.44`, commit + `03ce78031f28d7bfae4dcdc477a7273c6ecae55b`; +- `org.gamecult.eve.plugin-fields` `0.2.3`, commit + `c5a4a75c1b727499b16c2dae1895f29e2a9f72f0`; - `org.gamecult.eve.surface` `0.2.2`, commit `140e1bd963a0033e66777a3b2c5fe6e9c97dfe32`; - `org.gamecult.eve.unity-uitoolkit` `0.1.1`, commit @@ -17,7 +17,7 @@ The warm-cache released-package witness passes with: - the generic `ReleaseConsumerProject` client connected directly to the Aetheria daemon. -Evidence is in `artifacts/aetheria-daemon-gravity-fog-power-pulse`. The PlayMode test +Evidence is in `artifacts/aetheria-daemon-fossil-fog-producers`. The PlayMode test passed and recorded provider-owned reconciled movement, look, targeting, tractor press, tractor release, contact-gated cargo collection, and action receipts. The daemon-owned look direction reached the SoA body rotation, @@ -50,7 +50,7 @@ Fields splat layers. Its surface contract contains logical ports such as names or pass indices. The selected `unity-scene` provider asset variant owns that concrete shader ABI through `unity.volume.*` metadata. The released generic lowerer resolved the provider shader and dither texture, rasterized all -four layers, and composited `22` pilot-camera frames from daemon frame `223`. +four layers, and composited `21` pilot-camera frames from daemon frame `223`. The provider variant advertises the fossil shader's raymarch, temporal-history, and composite passes plus their logical ports. EveUnity allocates and resets the history targets generically; a partial temporal ABI fails closed. @@ -64,11 +64,11 @@ daemon lock progress `1.0`; Unity does not manufacture or smooth that value. Camera-channel facts: - provider-authored player renderers: `13`, including the lowered tractor effect; -- pilot changed pixels: `230,400`; -- pilot average luminance: `0.0910102`; -- pilot bright pixels: `969`; +- pilot changed pixels: `191,353`; +- pilot average luminance: `0.0283105`; +- pilot bright pixels: `705`; - map-channel renderers: `11`; -- map changed pixels: `4,850`; +- map changed pixels: `4,444`; - native cockpit progress bars: `7`; - daemon tractor power at capture: `1.0` after the held input completed its authored ramp; @@ -84,22 +84,22 @@ Camera-channel facts: - the map camera renders exactly the advertised map layer. Visual inspection confirms that map glyphs are absent from the pilot frame and -present in the map-only frame. Correcting the fossil camera mode removed the -false top-down dark disk, but the active volume now resolves as a nearly uniform -saturated orange horizon rather than the fossil's blue gravity-shaped fog sea. -This is transport and lowering proof, not visual parity. Release `0.3.42` -adds the generic Eve Fields `PowerPulse` GPU falloff to the existing dither-scale, -temporal-history, and finite-look-at lowerers. Aetheria now publishes the fossil -radial zone/body brushes instead of a viewport-sized solid slab, keeps negative -`gravity.height` separate from positive `fog.surface_height`, and derives entity -altitude as fossil terrain height plus hull grid offset after each Ymir XZ step. -The witness records player Y `-92.82` and camera Y `-109.97`; Unity does not -sample terrain to invent either value. The volume is no longer a flat saturated -orange: temporal cloud RGB spans `0.325-0.537`, `0.201-0.332`, `0.113-0.187` -with decoded density `0.334-0.673`. Visual inspection still shows a dark brown, -low-contrast field rather than the fossil's blue gravity-fog sea, so color, -lighting, patch structure, and gravity-wave parity remain open. EveUnity must -not compensate with Aetheria-specific lighting or fog rules. +present in the map-only frame. This is transport, authority, field-production, +and lowering proof, not visual parity: the current pilot capture is nearly black +with sparse clipped geometry instead of the fossil's bright blue gravity-shaped +fog sea. Release `0.3.44` adds generic lowering for Eve Fields `0.2.3`'s +`AnimatedRadialCosine` source alongside `PowerPulse`, dither-scale, +temporal-history, and finite-look-at semantics. Aetheria publishes the fossil +global animated simplex/cellular/ambient producer stack, body-owned radial wave +frequency, separate negative `gravity.height` and positive +`fog.surface_height`, and the authored `ARPG.unity` zone-brush extent. The +witness records player Y `-76.7574` and camera Y `-94.3347`; Unity does not +sample terrain to invent either value. The four rasterized field layers and HDR +raymarch/history targets are populated, so the remaining failure is in native +presentation/composition or provider art calibration rather than missing state +delivery. Color, lighting, patch structure, readable world geometry, and +gravity-wave visual parity remain open. EveUnity must not compensate with +Aetheria-specific lighting or fog rules. Substance is not part of this path; later texture baking belongs in Blender. Aetheria currently bundles ambientCG's 1K `Metal012` color, normal, and metalness maps under CC0. The same pre-generated maps now replace dead diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 8065c74..c3465aa 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,8 +13,8 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "441471949a22a0183da1ecac4a4a1d0dc2a7618e" -$expectedEveFieldsCommit = "59924e324cd10f293c214c8e6bb2c794a32d39a0" +$expectedEveUnityCommit = "03ce78031f28d7bfae4dcdc477a7273c6ecae55b" +$expectedEveFieldsCommit = "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot From 2fa4524a7ff58105c34327e70cd64eb8df2eaec7 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 00:16:52 +0100 Subject: [PATCH 099/292] Lower provider post-process profiles --- TestProject/Packages/packages-lock.json | 102 ++++++++++++++++++ .../Runtime/EveUnityPlayableWorldCameraRig.cs | 97 ++++++++++++++++- .../Runtime/EveUnitySceneSurfaceLowerer.cs | 5 + .../Runtime/GameCult.Eve.UnityScene.asmdef | 4 +- .../EveUnitySceneSurfaceLowererTests.cs | 21 +++- .../GameCult.Eve.UnityScene.Tests.asmdef | 4 +- .../org.gamecult.eve.unity-scene/package.json | 5 +- 7 files changed, 228 insertions(+), 10 deletions(-) diff --git a/TestProject/Packages/packages-lock.json b/TestProject/Packages/packages-lock.json index ea3990d..f6390b8 100644 --- a/TestProject/Packages/packages-lock.json +++ b/TestProject/Packages/packages-lock.json @@ -1,5 +1,27 @@ { "dependencies": { + "com.unity.burst": { + "version": "1.8.29", + "depth": 3, + "source": "registry", + "dependencies": { + "com.unity.mathematics": "1.2.1", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.collections": { + "version": "6.4.0", + "depth": 3, + "source": "builtin", + "dependencies": { + "com.unity.burst": "1.8.23", + "com.unity.mathematics": "1.3.2", + "com.unity.nuget.mono-cecil": "1.11.5", + "com.unity.test-framework": "1.4.6", + "com.unity.test-framework.performance": "3.0.3" + } + }, "com.unity.ext.nunit": { "version": "2.0.5", "depth": 1, @@ -13,6 +35,60 @@ "dependencies": {}, "url": "https://packages.unity.com" }, + "com.unity.nuget.mono-cecil": { + "version": "1.11.6", + "depth": 4, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.render-pipelines.core": { + "version": "17.4.0", + "depth": 2, + "source": "builtin", + "dependencies": { + "com.unity.burst": "1.8.14", + "com.unity.mathematics": "1.3.2", + "com.unity.ugui": "2.0.0", + "com.unity.collections": "2.4.3", + "com.unity.modules.terrain": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.render-pipelines.universal": { + "version": "17.4.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.render-pipelines.core": "17.4.0", + "com.unity.shadergraph": "17.4.0", + "com.unity.render-pipelines.universal-config": "17.4.0" + } + }, + "com.unity.render-pipelines.universal-config": { + "version": "17.4.0", + "depth": 2, + "source": "builtin", + "dependencies": { + "com.unity.render-pipelines.core": "17.4.0" + } + }, + "com.unity.searcher": { + "version": "4.9.4", + "depth": 3, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.shadergraph": { + "version": "17.4.0", + "depth": 2, + "source": "builtin", + "dependencies": { + "com.unity.render-pipelines.core": "17.4.0", + "com.unity.searcher": "4.9.3" + } + }, "com.unity.test-framework": { "version": "1.6.0", "depth": 0, @@ -23,6 +99,25 @@ "com.unity.modules.jsonserialize": "1.0.0" } }, + "com.unity.test-framework.performance": { + "version": "3.4.0", + "depth": 4, + "source": "registry", + "dependencies": { + "com.unity.test-framework": "1.1.33", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ugui": { + "version": "2.0.0", + "depth": 3, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0" + } + }, "org.gamecult.cultlib": { "version": "file:../../artifacts/test-dependencies/org.gamecult.cultlib", "depth": 0, @@ -54,6 +149,7 @@ "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", + "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" } }, @@ -107,6 +203,12 @@ "source": "builtin", "dependencies": {} }, + "com.unity.modules.terrain": { + "version": "1.0.0", + "depth": 3, + "source": "builtin", + "dependencies": {} + }, "com.unity.modules.ui": { "version": "1.0.0", "depth": 1, diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index ded0d3a..b0944e1 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -1,5 +1,6 @@ using UnityEngine; using UnityEngine.Rendering; +using UnityEngine.Rendering.Universal; #nullable enable @@ -34,6 +35,11 @@ namespace GameCult.Eve.UnityScene private Camera? _environmentCamera; private CameraClearFlags _previousCameraClearFlags; private GameObject? _keyLightObject; + private GameObject? _postProcessVolumeObject; + private Volume? _postProcessVolume; + private UniversalAdditionalCameraData? _postProcessCameraData; + private bool _createdPostProcessCameraData; + private bool _previousRenderPostProcessing; public EveUnityPlayableWorldClientHost? Host { @@ -108,7 +114,7 @@ namespace GameCult.Eve.UnityScene ReleaseCamera(); return false; } - if (!TryResolveEnvironment(resolvedHost, activeWorld, out var skybox, out var reflection)) + if (!TryResolveEnvironment(resolvedHost, activeWorld, out var skybox, out var reflection, out var postProcessProfile)) { ReleaseRig(); return false; @@ -127,7 +133,12 @@ namespace GameCult.Eve.UnityScene _environmentOwner = this; var cameraComponent = camera.GetComponent(); - ApplyAmbientEnvironment(activeWorld, cameraComponent, skybox, reflection); + if (postProcessProfile != null && cameraComponent == null) + { + ReleaseRig(); + return false; + } + ApplyAmbientEnvironment(activeWorld, cameraComponent, skybox, reflection, postProcessProfile); ApplyKeyLight(activeWorld); if (cameraComponent != null && _renderPolicySource != null) { @@ -279,10 +290,12 @@ namespace GameCult.Eve.UnityScene EveUnityPlayableWorldClientHost host, EveUnityPlayableWorldProjection world, out Material? skybox, - out Cubemap? reflection) + out Cubemap? reflection, + out VolumeProfile? postProcessProfile) { skybox = null; reflection = null; + postProcessProfile = null; if (!IsFinite(world.AmbientLightR) || !IsFinite(world.AmbientLightG) || !IsFinite(world.AmbientLightB) || @@ -319,6 +332,17 @@ namespace GameCult.Eve.UnityScene if (reflection == null) return false; } + if (!string.IsNullOrWhiteSpace(world.PostProcessProfileAssetRef)) + { + postProcessProfile = assets?.ResolveAsset( + new EveUnityPlayableWorldAssetBinding( + world.PostProcessProfileAssetRef, + "", + "provider-asset-ref"), + typeof(VolumeProfile)) as VolumeProfile; + if (postProcessProfile == null) + return false; + } return true; } @@ -376,7 +400,8 @@ namespace GameCult.Eve.UnityScene EveUnityPlayableWorldProjection world, Camera? camera, Material? skybox, - Cubemap? reflection) + Cubemap? reflection, + VolumeProfile? postProcessProfile) { var environmentChanged = !ReferenceEquals(RenderSettings.skybox, skybox); if (!_ownsAmbientEnvironment) @@ -428,11 +453,75 @@ namespace GameCult.Eve.UnityScene } if (environmentChanged) DynamicGI.UpdateEnvironment(); + ApplyPostProcess(camera, postProcessProfile); + } + + private void ApplyPostProcess(Camera? camera, VolumeProfile? profile) + { + if (profile == null) + { + RestorePostProcess(); + return; + } + if (camera == null) + return; + if (_postProcessCameraData != null && !ReferenceEquals(_postProcessCameraData.gameObject, camera.gameObject)) + RestorePostProcess(); + if (_postProcessCameraData == null) + { + _postProcessCameraData = camera.GetComponent(); + _createdPostProcessCameraData = _postProcessCameraData == null; + if (_postProcessCameraData == null) + _postProcessCameraData = camera.gameObject.AddComponent(); + _previousRenderPostProcessing = _postProcessCameraData.renderPostProcessing; + } + _postProcessCameraData.renderPostProcessing = true; + if (_postProcessVolumeObject == null) + { + _postProcessVolumeObject = new GameObject("Eve World Post Process"); + _postProcessVolumeObject.transform.SetParent(transform, worldPositionStays: false); + _postProcessVolume = _postProcessVolumeObject.AddComponent(); + _postProcessVolume.isGlobal = true; + _postProcessVolume.priority = 1000f; + _postProcessVolume.weight = 1f; + } + _postProcessVolume!.sharedProfile = profile; + _postProcessVolumeObject.SetActive(true); + } + + private void RestorePostProcess() + { + if (_postProcessCameraData != null) + { + if (_createdPostProcessCameraData) + { + if (Application.isPlaying) + Destroy(_postProcessCameraData); + else + DestroyImmediate(_postProcessCameraData); + } + else + { + _postProcessCameraData.renderPostProcessing = _previousRenderPostProcessing; + } + } + _postProcessCameraData = null; + _createdPostProcessCameraData = false; + if (_postProcessVolumeObject != null) + { + if (Application.isPlaying) + Destroy(_postProcessVolumeObject); + else + DestroyImmediate(_postProcessVolumeObject); + } + _postProcessVolumeObject = null; + _postProcessVolume = null; } private void RestoreAmbientEnvironment() { ReleaseKeyLight(); + RestorePostProcess(); if (!_ownsAmbientEnvironment) return; var environmentChanged = !ReferenceEquals(RenderSettings.skybox, _previousSkybox); RenderSettings.ambientMode = _previousAmbientMode; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index bb2536d..462336f 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -147,6 +147,7 @@ namespace GameCult.Eve.UnityScene worldRoot.GetProp("skyboxAssetRef"), worldRoot.GetProp("reflectionAssetRef"), ParseFloat(worldRoot.GetProp("reflectionIntensity"), 1f), + worldRoot.GetProp("postProcessProfileAssetRef"), ParseVector3(worldRoot.GetProp("keyLightDirection")), ParseVector3(worldRoot.GetProp("keyLightColor")), ParseFloat(worldRoot.GetProp("keyLightIntensity"), 0f), @@ -390,6 +391,7 @@ namespace GameCult.Eve.UnityScene string skyboxAssetRef = "", string reflectionAssetRef = "", float reflectionIntensity = 1f, + string postProcessProfileAssetRef = "", (float x, float y, float z) keyLightDirection = default, (float r, float g, float b) keyLightColor = default, float keyLightIntensity = 0f, @@ -431,6 +433,7 @@ namespace GameCult.Eve.UnityScene SkyboxAssetRef = skyboxAssetRef ?? ""; ReflectionAssetRef = reflectionAssetRef ?? ""; ReflectionIntensity = reflectionIntensity; + PostProcessProfileAssetRef = postProcessProfileAssetRef ?? ""; KeyLightDirectionX = keyLightDirection.x; KeyLightDirectionY = keyLightDirection.y; KeyLightDirectionZ = keyLightDirection.z; @@ -504,6 +507,8 @@ namespace GameCult.Eve.UnityScene public float ReflectionIntensity { get; } + public string PostProcessProfileAssetRef { get; } + public float KeyLightDirectionX { get; } public float KeyLightDirectionY { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/GameCult.Eve.UnityScene.asmdef b/packages/org.gamecult.eve.unity-scene/Runtime/GameCult.Eve.UnityScene.asmdef index 784339a..334de2e 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/GameCult.Eve.UnityScene.asmdef +++ b/packages/org.gamecult.eve.unity-scene/Runtime/GameCult.Eve.UnityScene.asmdef @@ -4,7 +4,9 @@ "references": [ "GameCult.Eve.Surface", "GameCult.Eve.PluginFields", - "Unity.Mathematics" + "Unity.Mathematics", + "Unity.RenderPipelines.Core.Runtime", + "Unity.RenderPipelines.Universal.Runtime" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 881c883..0f10bce 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -9,6 +9,7 @@ using GameCult.Mesh; using NUnit.Framework; using UnityEngine; using UnityEngine.Rendering; +using UnityEngine.Rendering.Universal; #nullable enable @@ -416,6 +417,7 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(projection.PlayableWorld.AmbientLightIntensity, Is.EqualTo(1.46f)); Assert.That(projection.PlayableWorld.SkyboxAssetRef, Is.Empty); Assert.That(projection.PlayableWorld.ReflectionAssetRef, Is.Empty); + Assert.That(projection.PlayableWorld.PostProcessProfileAssetRef, Is.Empty); Assert.That(projection.PlayableWorld.ReflectionIntensity, Is.EqualTo(1f)); Assert.That(projection.PlayableWorld.ExcludedRenderChannels, Is.EqualTo(new[] { "map" })); Assert.That(projection.PlayableWorld.PlayerEntityId, Is.EqualTo("player-vanguard")); @@ -1100,6 +1102,7 @@ namespace GameCult.Eve.UnityScene.Tests var previousReflectionIntensity = RenderSettings.reflectionIntensity; Material? skyboxMaterial = null; Cubemap? reflectionCubemap = null; + VolumeProfile? postProcessProfile = null; try { @@ -1109,13 +1112,16 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(skyboxShader, Is.Not.Null); skyboxMaterial = new Material(skyboxShader!); reflectionCubemap = new Cubemap(4, TextureFormat.RGBA32, false); + postProcessProfile = ScriptableObject.CreateInstance(); nativeAssets.Set("material.environment.skybox", skyboxMaterial); nativeAssets.Set("texture.environment.reflection", reflectionCubemap); + nativeAssets.Set("profile.environment.flight", postProcessProfile); provider.Set( new EveUnitySceneProviderSurfaceDocument( PlayableArpgDocument( skyboxAssetRef: "material.environment.skybox", - reflectionAssetRef: "texture.environment.reflection"), + reflectionAssetRef: "texture.environment.reflection", + postProcessProfileAssetRef: "profile.environment.flight"), Advertisement("aetheria.daemon.game"), "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", 1), @@ -1165,6 +1171,11 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(RenderSettings.customReflectionTexture, Is.SameAs(reflectionCubemap)); Assert.That(RenderSettings.reflectionIntensity, Is.EqualTo(1f).Within(0.001f)); Assert.That(camera.clearFlags, Is.EqualTo(CameraClearFlags.Skybox)); + Assert.That(camera.GetComponent(), Is.Not.Null); + Assert.That(camera.GetComponent().renderPostProcessing, Is.True); + var postProcessTransform = hostObject.transform.Find("Eve World Post Process"); + Assert.That(postProcessTransform, Is.Not.Null); + Assert.That(postProcessTransform.GetComponent().sharedProfile, Is.SameAs(postProcessProfile)); var keyLightTransform = hostObject.transform.Find("Eve World Key Light"); Assert.That(keyLightTransform, Is.Not.Null); var keyLight = keyLightTransform.GetComponent(); @@ -1173,7 +1184,7 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(keyLight.color, Is.EqualTo(new Color(1f, 0.95f, 0.9f, 1f))); Assert.That(Vector3.Dot(keyLightTransform.forward, new Vector3(0.4f, -1f, 0.25f).normalized), Is.GreaterThan(0.999f)); - Assert.That(nativeAssets.LastBinding?.AssetRef, Is.EqualTo("texture.environment.reflection")); + Assert.That(nativeAssets.LastBinding?.AssetRef, Is.EqualTo("profile.environment.flight")); Assert.That(nativeAssets.LastBinding?.EntityKind, Is.Empty); var secondaryHost = secondaryHostObject.AddComponent(); @@ -1225,6 +1236,8 @@ namespace GameCult.Eve.UnityScene.Tests rig.CameraTransform = null; Assert.That(host.ActiveCameraTransform, Is.Null); Assert.That(hostObject.transform.Find("Eve World Key Light"), Is.Null); + Assert.That(hostObject.transform.Find("Eve World Post Process"), Is.Null); + Assert.That(camera.GetComponent(), Is.Null); aim.RefreshNow(); Assert.That(aim.ViewDotVisible, Is.False); Assert.That(RenderSettings.ambientMode, Is.EqualTo(ambientMode)); @@ -1242,6 +1255,7 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(host.ActiveCameraTransform, Is.Null); nativeAssets.Set("material.environment.skybox", reflectionCubemap); nativeAssets.Set("texture.environment.reflection", skyboxMaterial); + nativeAssets.Set("profile.environment.flight", skyboxMaterial); Assert.That(rig.ApplyRig(0f), Is.False); Assert.That(host.ActiveCameraTransform, Is.Null); } @@ -1256,6 +1270,7 @@ namespace GameCult.Eve.UnityScene.Tests RenderSettings.reflectionIntensity = previousReflectionIntensity; if (skyboxMaterial != null) UnityEngine.Object.DestroyImmediate(skyboxMaterial); if (reflectionCubemap != null) UnityEngine.Object.DestroyImmediate(reflectionCubemap); + if (postProcessProfile != null) UnityEngine.Object.DestroyImmediate(postProcessProfile); UnityEngine.Object.DestroyImmediate(secondaryCameraObject); UnityEngine.Object.DestroyImmediate(secondaryHostObject); UnityEngine.Object.DestroyImmediate(secondaryRootObject); @@ -1838,6 +1853,7 @@ namespace GameCult.Eve.UnityScene.Tests string playerPosition = "0,0,0", string skyboxAssetRef = "", string reflectionAssetRef = "", + string postProcessProfileAssetRef = "", string cameraRig = "planar.top-down-follow.v1", string cameraDistance = "150", string cameraTargetScreenX = "0.9", @@ -1997,6 +2013,7 @@ namespace GameCult.Eve.UnityScene.Tests ["skyboxAssetRef"] = skyboxAssetRef, ["reflectionAssetRef"] = reflectionAssetRef, ["reflectionIntensity"] = "1", + ["postProcessProfileAssetRef"] = postProcessProfileAssetRef, ["keyLightDirection"] = "0.4,-1,0.25", ["keyLightColor"] = "1,0.95,0.9", ["keyLightIntensity"] = "0.75", diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef b/packages/org.gamecult.eve.unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef index 4428a96..75ef6c3 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef @@ -5,7 +5,9 @@ "GameCult.Eve.Surface", "GameCult.Eve.UnityScene", "GameCult.Eve.UnityScene.CultMesh", - "GameCult.Eve.PluginFields" + "GameCult.Eve.PluginFields", + "Unity.RenderPipelines.Core.Runtime", + "Unity.RenderPipelines.Universal.Runtime" ], "includePlatforms": [ "Editor" diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 4b65aa3..5822ed0 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,14 +1,15 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.44", - "unity": "2021.3", + "version": "0.3.45", + "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { "org.gamecult.cultlib": "1.0.13", "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", + "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, "keywords": [ From 7c46abd05c24b7f52d5c48165dfb540f554c8fdf Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 00:18:36 +0100 Subject: [PATCH 100/292] Pin scene post-process release --- ReleaseConsumerProject/Packages/manifest.json | 2 +- ReleaseConsumerProject/Packages/packages-lock.json | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 5f86b43..27c5fe9 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.44", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.45", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 2b81933..e5377af 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.44", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.45", "depth": 0, "source": "git", "dependencies": { @@ -152,9 +152,10 @@ "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", + "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "03ce78031f28d7bfae4dcdc477a7273c6ecae55b" + "hash": "2fa4524a7ff58105c34327e70cd64eb8df2eaec7" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", From d3840c999620cb7aea69de23f2b353dad821c009 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 00:37:33 +0100 Subject: [PATCH 101/292] Composite provider volumes before post processing --- .../org.gamecult.eve.unity-scene/README.md | 4 +- .../Fields/EveUnityFieldsVolumeRenderer.cs | 98 +++++++++++++++---- .../EveFieldsUnitySceneProjectionTests.cs | 9 ++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 4 files changed, 92 insertions(+), 21 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index f7d3d74..9150c15 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -69,4 +69,6 @@ keywords, and pass indices belong to the selected `unity-scene` asset variant under `unity.volume.*` metadata. The lowerer fails closed when that native program descriptor is absent or incomplete. This keeps provider shader ABIs out of Eve's semantic surface while allowing a provider-owned shader to consume -portable Eve Fields documents. +portable Eve Fields documents. Volume composition runs as a generic URP render +pass before post-processing so the provider-advertised profile grades both the +native scene and the provider volume through the same camera pipeline. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs index add27f9..7848eb5 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -5,6 +5,8 @@ using System.Linq; using GameCult.Eve.PluginFields; using UnityEngine; using UnityEngine.Rendering; +using UnityEngine.Rendering.RenderGraphModule; +using UnityEngine.Rendering.Universal; #nullable enable @@ -26,13 +28,14 @@ namespace GameCult.Eve.UnityScene.Fields private readonly RenderTexture?[] _historyTextures = new RenderTexture?[2]; private int _historyTextureIndex = -1; private Matrix4x4 _previousViewProjection; - private CommandBuffer? _commands; + private EveUnityFieldsVolumePass? _renderPass; private string _activeNodeId = ""; public long PresentedFrameId { get; private set; } = -1; public int PresentedLayerCount => _targets.Count(target => target.TargetTexture != null); public long CompositeCount { get; private set; } public bool UsesTemporalHistory => HasTemporalProgram(); + public static RenderPassEvent CompositionRenderPassEvent => RenderPassEvent.BeforeRenderingPostProcessing; public void Bind(EveUnityPlayableWorldClientHost host, IEveUnityFieldsSplatsDocumentSource? source) { @@ -44,11 +47,11 @@ namespace GameCult.Eve.UnityScene.Fields enabled = _source != null; } - private void OnEnable() => RenderPipelineManager.endCameraRendering += OnEndCameraRendering; + private void OnEnable() => RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering; private void OnDisable() { - RenderPipelineManager.endCameraRendering -= OnEndCameraRendering; + RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering; ReleaseRenderState(); } @@ -80,7 +83,20 @@ namespace GameCult.Eve.UnityScene.Fields ApplyFieldBindings(field, document); } - private void OnEndCameraRendering(ScriptableRenderContext context, Camera camera) + private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera) + { + if (_host == null || _document == null || camera == null || + _host.ActiveCameraTransform != camera.transform) + return; + _renderPass ??= new EveUnityFieldsVolumePass(this); + camera.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(_renderPass); + } + + private void RenderVolume( + UnsafeCommandBuffer commands, + Camera camera, + TextureHandle colorTarget, + TextureHandle depthTarget) { if (_host == null || _document == null || camera == null || _host.ActiveCameraTransform != camera.transform) @@ -120,23 +136,23 @@ namespace GameCult.Eve.UnityScene.Fields } SetTexturePort("cloud", finalCloudTexture); - _commands ??= new CommandBuffer { name = "Eve Fields Volume" }; - _commands.Clear(); - _commands.SetRenderTarget(_raymarchTexture); - _commands.SetViewport(new Rect(0f, 0f, _raymarchTexture.width, _raymarchTexture.height)); - _commands.ClearRenderTarget(false, true, Color.clear); - _commands.DrawProcedural(Matrix4x4.identity, _material!, raymarchPass, MeshTopology.Triangles, 3, 1); + commands.SetRenderTarget(_raymarchTexture); + commands.SetViewport(new Rect(0f, 0f, _raymarchTexture.width, _raymarchTexture.height)); + commands.ClearRenderTarget(false, true, Color.clear); + commands.DrawProcedural(Matrix4x4.identity, _material!, raymarchPass, MeshTopology.Triangles, 3, 1); if (useTemporalHistory) { - _commands.SetRenderTarget(finalCloudTexture); - _commands.SetViewport(new Rect(0f, 0f, finalCloudTexture.width, finalCloudTexture.height)); - _commands.ClearRenderTarget(false, true, Color.clear); - _commands.DrawProcedural(Matrix4x4.identity, _material!, temporalPass, MeshTopology.Triangles, 3, 1); + commands.SetRenderTarget(finalCloudTexture); + commands.SetViewport(new Rect(0f, 0f, finalCloudTexture.width, finalCloudTexture.height)); + commands.ClearRenderTarget(false, true, Color.clear); + commands.DrawProcedural(Matrix4x4.identity, _material!, temporalPass, MeshTopology.Triangles, 3, 1); } - _commands.SetRenderTarget(BuiltinRenderTextureType.CameraTarget); - _commands.SetViewport(camera.pixelRect); - _commands.DrawProcedural(Matrix4x4.identity, _material!, compositePass, MeshTopology.Triangles, 3, 1); - Graphics.ExecuteCommandBuffer(_commands); + if (depthTarget.IsValid()) + commands.SetRenderTarget(colorTarget, depthTarget); + else + commands.SetRenderTarget(colorTarget); + commands.SetViewport(camera.pixelRect); + commands.DrawProcedural(Matrix4x4.identity, _material!, compositePass, MeshTopology.Triangles, 3, 1); if (useTemporalHistory) { _historyTextureIndex = historyWriteIndex; @@ -262,7 +278,6 @@ namespace GameCult.Eve.UnityScene.Fields private void ReleaseRenderState() { - if (_commands != null) { _commands.Release(); _commands = null; } ReleaseRenderTexture(ref _raymarchTexture); ReleaseRenderTexture(ref _historyTextures[0]); ReleaseRenderTexture(ref _historyTextures[1]); @@ -276,6 +291,51 @@ namespace GameCult.Eve.UnityScene.Fields ResetTemporalHistory(); } + private sealed class EveUnityFieldsVolumePass : ScriptableRenderPass + { + private readonly EveUnityFieldsVolumeRenderer _owner; + + private sealed class PassData + { + internal Camera Camera = null!; + internal EveUnityFieldsVolumeRenderer Owner = null!; + internal TextureHandle ColorTarget; + internal TextureHandle DepthTarget; + } + + internal EveUnityFieldsVolumePass(EveUnityFieldsVolumeRenderer owner) + { + _owner = owner; + renderPassEvent = CompositionRenderPassEvent; + ConfigureInput(ScriptableRenderPassInput.Depth); + } + + public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) + { + var cameraData = frameData.Get(); + if (cameraData.camera == null || _owner._host == null || + _owner._host.ActiveCameraTransform != cameraData.camera.transform) + return; + + var resourceData = frameData.Get(); + var colorTarget = resourceData.activeColorTexture; + if (!colorTarget.IsValid()) return; + + using var builder = renderGraph.AddUnsafePass("Eve Fields Volume", out var passData); + passData.Camera = cameraData.camera; + passData.Owner = _owner; + passData.ColorTarget = colorTarget; + passData.DepthTarget = resourceData.activeDepthTexture; + + builder.UseTexture(passData.ColorTarget, AccessFlags.ReadWrite); + if (passData.DepthTarget.IsValid()) + builder.UseTexture(passData.DepthTarget, AccessFlags.Read); + builder.AllowPassCulling(false); + builder.SetRenderFunc(static (PassData data, UnsafeGraphContext context) => + data.Owner.RenderVolume(context.cmd, data.Camera, data.ColorTarget, data.DepthTarget)); + } + } + private void ResetTemporalHistory() { _historyTextureIndex = -1; diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index 7be1fc4..a2b5398 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -6,6 +6,7 @@ using GameCult.Eve.PluginFields; using GameCult.Eve.UnityScene.Fields; using NUnit.Framework; using UnityEngine; +using UnityEngine.Rendering.Universal; namespace GameCult.Eve.UnityScene.Tests { @@ -41,6 +42,14 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(typeof(IEveUnityFieldsSplatsDocumentSource).GetEvent("FieldsSplatsAvailable"), Is.Not.Null); } + [Test] + public void VolumeRendererCompositesBeforeProviderPostProcessing() + { + Assert.That( + EveUnityFieldsVolumeRenderer.CompositionRenderPassEvent, + Is.EqualTo(RenderPassEvent.BeforeRenderingPostProcessing)); + } + [Test] public void VolumeProgramAcceptsPortableTwoPassLifecycle() { diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 5822ed0..e4f7449 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.45", + "version": "0.3.46", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 01d6dbef51c5a79aadb5dcd3a93835eeb032457a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 00:40:30 +0100 Subject: [PATCH 102/292] Pin release consumer to scene 0.3.46 --- ReleaseConsumerProject/Packages/manifest.json | 2 +- ReleaseConsumerProject/Packages/packages-lock.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 27c5fe9..72cb181 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.45", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.46", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index e5377af..7f885a7 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.45", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.46", "depth": 0, "source": "git", "dependencies": { @@ -155,7 +155,7 @@ "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "2fa4524a7ff58105c34327e70cd64eb8df2eaec7" + "hash": "d3840c999620cb7aea69de23f2b353dad821c009" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", From a7a656c3c536b08bfb58061945990c50a8a79926 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 00:53:35 +0100 Subject: [PATCH 103/292] Lower provider-shaped volume layer targets --- .../org.gamecult.eve.unity-scene/README.md | 3 ++ .../Fields/EveUnityFieldsVolumeRenderer.cs | 44 ++++++++++++++++++- .../EveFieldsUnitySceneProjectionTests.cs | 36 +++++++++++++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 4 files changed, 82 insertions(+), 3 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index 9150c15..84f394d 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -72,3 +72,6 @@ out of Eve's semantic surface while allowing a provider-owned shader to consume portable Eve Fields documents. Volume composition runs as a generic URP render pass before post-processing so the provider-advertised profile grades both the native scene and the provider volume through the same camera pipeline. +Providers may advertise per-layer resolution scales, mipmap use, and filtering +through `layerTargetDescriptors`; the Unity lowerer applies those native target +requirements without assigning gameplay or product meaning to the layer keys. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs index 7848eb5..c7e5c2a 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -242,11 +242,51 @@ namespace GameCult.Eve.UnityScene.Fields private void EnsureTargets(EveUnityFieldVolumeProjection field) { var bindings = ParseBindings(Prop(field, "layerBindings")); + var descriptors = ParseBindings(Prop(field, "layerTargetDescriptors")); if (string.Equals(_activeNodeId, field.NodeId, StringComparison.Ordinal) && - _targets.Count == bindings.Count) return; + _targets.Count == bindings.Count && + _targets.All(target => bindings.ContainsKey(target.LayerKey))) + { + foreach (var target in _targets) + ApplyLayerTargetDescriptor(target, descriptors); + return; + } _targets.Clear(); foreach (var binding in bindings) - _targets.Add(new EveFieldsSplatLayerTarget { LayerKey = binding.Key, UseMipMaps = true }); + { + var target = new EveFieldsSplatLayerTarget { LayerKey = binding.Key }; + ApplyLayerTargetDescriptor(target, descriptors); + _targets.Add(target); + } + } + + private static void ApplyLayerTargetDescriptor( + EveFieldsSplatLayerTarget target, + IReadOnlyDictionary descriptors) + { + target.WidthScale = 1f; + target.HeightScale = 1f; + target.UseMipMaps = false; + target.FilterMode = FilterMode.Bilinear; + if (descriptors.TryGetValue(target.LayerKey, out var descriptor)) + TryApplyLayerTargetDescriptor(target, descriptor); + } + + public static bool TryApplyLayerTargetDescriptor(EveFieldsSplatLayerTarget? target, string descriptor) + { + if (target == null) return false; + var values = (descriptor ?? "").Split(','); + if (values.Length != 4 || + !TryFloat(values[0].Trim(), out var widthScale) || widthScale <= 0 || + !TryFloat(values[1].Trim(), out var heightScale) || heightScale <= 0 || + !bool.TryParse(values[2].Trim(), out var useMipMaps) || + !Enum.TryParse(values[3].Trim(), true, out FilterMode filterMode)) + return false; + target.WidthScale = widthScale; + target.HeightScale = heightScale; + target.UseMipMaps = useMipMaps; + target.FilterMode = filterMode; + return true; } private void EnsureLayerRenderer() diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index a2b5398..1268480 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -101,6 +101,42 @@ namespace GameCult.Eve.UnityScene.Tests } } + [Test] + public void VolumeRendererAppliesProviderAdvertisedLayerTargetShape() + { + var target = new EveFieldsSplatLayerTarget { LayerKey = "fog.tint" }; + + Assert.That( + EveUnityFieldsVolumeRenderer.TryApplyLayerTargetDescriptor( + target, + "0.5,0.25,true,trilinear"), + Is.True); + Assert.That(target.WidthScale, Is.EqualTo(0.5f)); + Assert.That(target.HeightScale, Is.EqualTo(0.25f)); + Assert.That(target.UseMipMaps, Is.True); + Assert.That(target.FilterMode, Is.EqualTo(FilterMode.Trilinear)); + } + + [Test] + public void VolumeRendererRejectsMalformedLayerTargetShape() + { + var target = new EveFieldsSplatLayerTarget + { + WidthScale = 2f, + HeightScale = 2f, + UseMipMaps = true, + FilterMode = FilterMode.Trilinear + }; + + Assert.That( + EveUnityFieldsVolumeRenderer.TryApplyLayerTargetDescriptor(target, "0,1,true,bilinear"), + Is.False); + Assert.That(target.WidthScale, Is.EqualTo(2f)); + Assert.That(target.HeightScale, Is.EqualTo(2f)); + Assert.That(target.UseMipMaps, Is.True); + Assert.That(target.FilterMode, Is.EqualTo(FilterMode.Trilinear)); + } + [Test] public void PackageContainsGenericFieldsShader() { diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index e4f7449..5f43b5f 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.46", + "version": "0.3.47", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 840264e0ce7054ee3f1cba5a8c5f48fb472de95e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 00:55:09 +0100 Subject: [PATCH 104/292] Pin release consumer to scene 0.3.47 --- ReleaseConsumerProject/Packages/manifest.json | 2 +- ReleaseConsumerProject/Packages/packages-lock.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 72cb181..afbbf7f 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.46", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.47", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 7f885a7..3b8a90d 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.46", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.47", "depth": 0, "source": "git", "dependencies": { @@ -155,7 +155,7 @@ "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "d3840c999620cb7aea69de23f2b353dad821c009" + "hash": "a7a656c3c536b08bfb58061945990c50a8a79926" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", From c00ee0e9e33f63a9084b01255523ca21ab9368d2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 01:06:07 +0100 Subject: [PATCH 105/292] Bind volume programs to authoritative document time --- .../org.gamecult.eve.unity-scene/README.md | 4 ++ .../Fields/EveUnityFieldsVolumeRenderer.cs | 42 +++++++++++++++++++ .../EveFieldsUnitySceneProjectionTests.cs | 30 +++++++++++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 4 files changed, 77 insertions(+), 1 deletion(-) diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index 84f394d..42ebd93 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -75,3 +75,7 @@ native scene and the provider volume through the same camera pipeline. Providers may advertise per-layer resolution scales, mipmap use, and filtering through `layerTargetDescriptors`; the Unity lowerer applies those native target requirements without assigning gameplay or product meaning to the layer keys. +`documentFloatBindings` maps standard document scalars to logical float ports as +`source=port,scale,offset`. This lets animated native programs consume +provider-authoritative simulation time without making the client clock an +authority. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs index c7e5c2a..52e26f3 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -237,6 +237,48 @@ namespace GameCult.Eve.UnityScene.Fields (float)((viewport.MinY + viewport.MaxY) * 0.5), (float)width, (float)height)); + foreach (var binding in ParseBindings(Prop(field, "documentFloatBindings"))) + { + if (TryEvaluateDocumentFloatBinding( + document, + binding.Key, + binding.Value, + out var port, + out var value)) + SetFloatPort(port, value); + } + } + + public static bool TryEvaluateDocumentFloatBinding( + EveFieldsSplatsDocument? document, + string source, + string descriptor, + out string port, + out float value) + { + port = ""; + value = 0f; + if (document == null) return false; + var values = (descriptor ?? "").Split(','); + if (values.Length != 3 || + string.IsNullOrWhiteSpace(values[0]) || + !TryFloat(values[1].Trim(), out var scale) || + !TryFloat(values[2].Trim(), out var offset)) + return false; + + double sourceValue; + switch ((source ?? "").Trim()) + { + case "simulationTimeSeconds": + sourceValue = document.SimulationTimeSeconds; + break; + default: + return false; + } + + port = values[0].Trim(); + value = (float)(sourceValue * scale + offset); + return float.IsFinite(value); } private void EnsureTargets(EveUnityFieldVolumeProjection field) diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index 1268480..3845648 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -137,6 +137,36 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(target.FilterMode, Is.EqualTo(FilterMode.Trilinear)); } + [Test] + public void VolumeRendererBindsAuthoritativeDocumentTimeToLogicalPort() + { + var document = new EveFieldsSplatsDocument { SimulationTimeSeconds = 12.5 }; + + Assert.That( + EveUnityFieldsVolumeRenderer.TryEvaluateDocumentFloatBinding( + document, + "simulationTimeSeconds", + "flowScroll,0.025,-0.1", + out var port, + out var value), + Is.True); + Assert.That(port, Is.EqualTo("flowScroll")); + Assert.That(value, Is.EqualTo(0.2125f).Within(0.00001f)); + } + + [Test] + public void VolumeRendererRejectsUnknownDocumentFloatSource() + { + Assert.That( + EveUnityFieldsVolumeRenderer.TryEvaluateDocumentFloatBinding( + new EveFieldsSplatsDocument(), + "unityTime", + "flowScroll,1,0", + out _, + out _), + Is.False); + } + [Test] public void PackageContainsGenericFieldsShader() { diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 5f43b5f..a969c41 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.47", + "version": "0.3.48", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From d7ba6aa6510b6eb0d2a79290f2c513def74d4476 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 01:08:52 +0100 Subject: [PATCH 106/292] Pin scene consumer to 0.3.48 --- ReleaseConsumerProject/Packages/manifest.json | 2 +- ReleaseConsumerProject/Packages/packages-lock.json | 4 ++-- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index afbbf7f..74c3590 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.47", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.48", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 3b8a90d..486fd74 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.47", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.48", "depth": 0, "source": "git", "dependencies": { @@ -155,7 +155,7 @@ "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "a7a656c3c536b08bfb58061945990c50a8a79926" + "hash": "c00ee0e9e33f63a9084b01255523ca21ab9368d2" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index c3465aa..609b160 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "03ce78031f28d7bfae4dcdc477a7273c6ecae55b" +$expectedEveUnityCommit = "c00ee0e9e33f63a9084b01255523ca21ab9368d2" $expectedEveFieldsCommit = "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" From e6ba1a36f6f0bce8df2c9834c25a8a980f03413c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 01:44:29 +0100 Subject: [PATCH 107/292] Expose explicit camera transform to volume programs --- packages/org.gamecult.eve.unity-scene/README.md | 3 +++ .../Runtime/Fields/EveUnityFieldsVolumeRenderer.cs | 1 + packages/org.gamecult.eve.unity-scene/package.json | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index 42ebd93..22359e7 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -79,3 +79,6 @@ requirements without assigning gameplay or product meaning to the layer keys. `source=port,scale,offset`. This lets animated native programs consume provider-authoritative simulation time without making the client clock an authority. +Native volume programs may advertise a `cameraToWorld` matrix port when their +procedural passes require an explicit camera transform instead of relying on +render-pipeline globals. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs index 52e26f3..a132030 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -112,6 +112,7 @@ namespace GameCult.Eve.UnityScene.Fields var gpuProjection = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true); var viewProjection = gpuProjection * camera.worldToCameraMatrix; SetMatrixPort("cameraInverseViewProjection", viewProjection.inverse); + SetMatrixPort("cameraToWorld", camera.cameraToWorldMatrix); SetVectorPort("cameraProjectionExtents", ProjectionExtents(camera)); SetFloatPort("raymarchOffset", Halton(Time.frameCount & 1023, 3)); diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index a969c41..9efba20 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.48", + "version": "0.3.49", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 0118c35b57433c851ed485b1093ef7decf20d10a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 01:54:09 +0100 Subject: [PATCH 108/292] Preserve volume witness diagnostics --- .../PlayMode/GenericWorldCaptureTests.cs | 38 ++++++++++++++++++- ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 +- .../PlayMode/GenericWorldCaptureTests.cs | 38 ++++++++++++++++++- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 5 files changed, 76 insertions(+), 8 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 57d685b..892df97 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -1269,7 +1269,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests floatCopy.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0); floatCopy.Apply(); RenderTexture.active = previous; - File.WriteAllBytes(Path.Combine(directory, $"field-{layerKey}.exr"), floatCopy.EncodeToEXR()); + File.WriteAllBytes( + Path.Combine(directory, $"field-{layerKey}.exr"), + floatCopy.EncodeToEXR(Texture2D.EXRFlags.OutputAsFloat)); UnityEngine.Object.DestroyImmediate(floatCopy); } } @@ -1288,6 +1290,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var historyIndexField = typeof(EveUnityFieldsVolumeRenderer).GetField( "_historyTextureIndex", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var materialField = typeof(EveUnityFieldsVolumeRenderer).GetField( + "_material", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var metadataField = typeof(EveUnityFieldsVolumeRenderer).GetField( + "_programMetadata", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); var raymarchTexture = raymarchField?.GetValue(renderer) as RenderTexture; var historyTextures = historyTexturesField?.GetValue(renderer) as RenderTexture[]; var historyIndex = historyIndexField?.GetValue(renderer) is int value ? value : -1; @@ -1299,9 +1307,35 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests : null; WriteFloatTexture(historyTexture, Path.Combine(directory, "field-volume-history.exr")); WriteFloatTexture(historyTexture ?? raymarchTexture, Path.Combine(directory, "field-volume-cloud.exr")); + WriteVolumeMaterialFacts( + materialField?.GetValue(renderer) as Material, + metadataField?.GetValue(renderer) as IReadOnlyDictionary, + Path.Combine(directory, "field-volume-material.txt")); RenderTexture.active = previous; } + private static void WriteVolumeMaterialFacts( + Material material, + IReadOnlyDictionary metadata, + string path) + { + if (material == null || metadata == null) return; + var facts = new List(); + foreach (var binding in metadata.OrderBy(value => value.Key, StringComparer.Ordinal)) + { + if (binding.Key.Contains(".floatPort.", StringComparison.Ordinal)) + facts.Add($"{binding.Key}={binding.Value}:{material.GetFloat(binding.Value):R}"); + else if (binding.Key.Contains(".vectorPort.", StringComparison.Ordinal)) + facts.Add($"{binding.Key}={binding.Value}:{material.GetVector(binding.Value)}"); + else if (binding.Key.Contains(".texturePort.", StringComparison.Ordinal)) + { + var texture = material.HasProperty(binding.Value) ? material.GetTexture(binding.Value) : null; + facts.Add($"{binding.Key}={binding.Value}:{texture?.name ?? ""}:{texture?.width ?? 0}x{texture?.height ?? 0}"); + } + } + File.WriteAllLines(path, facts); + } + private static void WriteFloatTexture(RenderTexture texture, string path) { if (texture == null || string.IsNullOrWhiteSpace(path)) return; @@ -1311,7 +1345,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests copy.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0); copy.Apply(); RenderTexture.active = previous; - File.WriteAllBytes(path, copy.EncodeToEXR()); + File.WriteAllBytes(path, copy.EncodeToEXR(Texture2D.EXRFlags.OutputAsFloat)); UnityEngine.Object.DestroyImmediate(copy); } diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 74c3590..01675bc 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.48", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.49", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 486fd74..cc2cc9d 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.48", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.49", "depth": 0, "source": "git", "dependencies": { @@ -155,7 +155,7 @@ "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "c00ee0e9e33f63a9084b01255523ca21ab9368d2" + "hash": "e6ba1a36f6f0bce8df2c9834c25a8a980f03413c" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 57d685b..892df97 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -1269,7 +1269,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests floatCopy.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0); floatCopy.Apply(); RenderTexture.active = previous; - File.WriteAllBytes(Path.Combine(directory, $"field-{layerKey}.exr"), floatCopy.EncodeToEXR()); + File.WriteAllBytes( + Path.Combine(directory, $"field-{layerKey}.exr"), + floatCopy.EncodeToEXR(Texture2D.EXRFlags.OutputAsFloat)); UnityEngine.Object.DestroyImmediate(floatCopy); } } @@ -1288,6 +1290,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var historyIndexField = typeof(EveUnityFieldsVolumeRenderer).GetField( "_historyTextureIndex", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var materialField = typeof(EveUnityFieldsVolumeRenderer).GetField( + "_material", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + var metadataField = typeof(EveUnityFieldsVolumeRenderer).GetField( + "_programMetadata", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); var raymarchTexture = raymarchField?.GetValue(renderer) as RenderTexture; var historyTextures = historyTexturesField?.GetValue(renderer) as RenderTexture[]; var historyIndex = historyIndexField?.GetValue(renderer) is int value ? value : -1; @@ -1299,9 +1307,35 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests : null; WriteFloatTexture(historyTexture, Path.Combine(directory, "field-volume-history.exr")); WriteFloatTexture(historyTexture ?? raymarchTexture, Path.Combine(directory, "field-volume-cloud.exr")); + WriteVolumeMaterialFacts( + materialField?.GetValue(renderer) as Material, + metadataField?.GetValue(renderer) as IReadOnlyDictionary, + Path.Combine(directory, "field-volume-material.txt")); RenderTexture.active = previous; } + private static void WriteVolumeMaterialFacts( + Material material, + IReadOnlyDictionary metadata, + string path) + { + if (material == null || metadata == null) return; + var facts = new List(); + foreach (var binding in metadata.OrderBy(value => value.Key, StringComparer.Ordinal)) + { + if (binding.Key.Contains(".floatPort.", StringComparison.Ordinal)) + facts.Add($"{binding.Key}={binding.Value}:{material.GetFloat(binding.Value):R}"); + else if (binding.Key.Contains(".vectorPort.", StringComparison.Ordinal)) + facts.Add($"{binding.Key}={binding.Value}:{material.GetVector(binding.Value)}"); + else if (binding.Key.Contains(".texturePort.", StringComparison.Ordinal)) + { + var texture = material.HasProperty(binding.Value) ? material.GetTexture(binding.Value) : null; + facts.Add($"{binding.Key}={binding.Value}:{texture?.name ?? ""}:{texture?.width ?? 0}x{texture?.height ?? 0}"); + } + } + File.WriteAllLines(path, facts); + } + private static void WriteFloatTexture(RenderTexture texture, string path) { if (texture == null || string.IsNullOrWhiteSpace(path)) return; @@ -1311,7 +1345,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests copy.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0); copy.Apply(); RenderTexture.active = previous; - File.WriteAllBytes(path, copy.EncodeToEXR()); + File.WriteAllBytes(path, copy.EncodeToEXR(Texture2D.EXRFlags.OutputAsFloat)); UnityEngine.Object.DestroyImmediate(copy); } diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 609b160..20d049a 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "c00ee0e9e33f63a9084b01255523ca21ab9368d2" +$expectedEveUnityCommit = "e6ba1a36f6f0bce8df2c9834c25a8a980f03413c" $expectedEveFieldsCommit = "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" From 94f330b7aba89a15e81bc08694446c7162543783 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 02:23:22 +0100 Subject: [PATCH 109/292] Correct released Aetheria camera witness semantics --- .../Tests/PlayMode/GenericWorldCaptureTests.cs | 15 +++++++++++++-- .../Tests/PlayMode/GenericWorldCaptureTests.cs | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 892df97..4615ea8 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -597,8 +597,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); Assert.That(runtime.ActiveWorld.CameraPositionDamping, Is.EqualTo(0f).Within(0.001f)); var playerViewport = camera.WorldToViewportPoint(playerMarker.transform.position); - Assert.That(playerViewport.x, Is.EqualTo(0.64f).Within(0.01f)); - Assert.That(playerViewport.y, Is.EqualTo(0.81f).Within(0.01f)); + Assert.That(runtime.ActiveWorld.CameraTargetScreenX, Is.EqualTo(0.64f).Within(0.001f)); + Assert.That(runtime.ActiveWorld.CameraTargetScreenY, Is.EqualTo(0.19f).Within(0.001f)); + Assert.That(playerViewport.x, Is.EqualTo(runtime.ActiveWorld.CameraTargetScreenX).Within(0.01f)); + Assert.That(playerViewport.y, Is.EqualTo(runtime.ActiveWorld.CameraTargetScreenY).Within(0.01f)); MeasureViewportCoverage(camera, playerRenderers, out var playerViewportWidth, out var playerViewportHeight); var playerRendererFacts = playerRenderers .Select(renderer => RendererFact.From(camera, renderer)) @@ -1179,6 +1181,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests for (var index = 0; index < measuredRenderers.Count; index++) { var fact = facts[index]; + var rendererLayer = measuredRenderers[index].gameObject.layer; + if (rendererLayer >= 0 && rendererLayer < 32 && + (camera.cullingMask & (1 << rendererLayer)) == 0) + { + fact.renderedPixelCount = 0; + fact.renderedAverageLuminance = 0; + fact.renderedBrightPixelCount = 0; + continue; + } measuredRenderers[index].enabled = true; camera.Render(); RenderTexture.active = target; diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 892df97..4615ea8 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -597,8 +597,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); Assert.That(runtime.ActiveWorld.CameraPositionDamping, Is.EqualTo(0f).Within(0.001f)); var playerViewport = camera.WorldToViewportPoint(playerMarker.transform.position); - Assert.That(playerViewport.x, Is.EqualTo(0.64f).Within(0.01f)); - Assert.That(playerViewport.y, Is.EqualTo(0.81f).Within(0.01f)); + Assert.That(runtime.ActiveWorld.CameraTargetScreenX, Is.EqualTo(0.64f).Within(0.001f)); + Assert.That(runtime.ActiveWorld.CameraTargetScreenY, Is.EqualTo(0.19f).Within(0.001f)); + Assert.That(playerViewport.x, Is.EqualTo(runtime.ActiveWorld.CameraTargetScreenX).Within(0.01f)); + Assert.That(playerViewport.y, Is.EqualTo(runtime.ActiveWorld.CameraTargetScreenY).Within(0.01f)); MeasureViewportCoverage(camera, playerRenderers, out var playerViewportWidth, out var playerViewportHeight); var playerRendererFacts = playerRenderers .Select(renderer => RendererFact.From(camera, renderer)) @@ -1179,6 +1181,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests for (var index = 0; index < measuredRenderers.Count; index++) { var fact = facts[index]; + var rendererLayer = measuredRenderers[index].gameObject.layer; + if (rendererLayer >= 0 && rendererLayer < 32 && + (camera.cullingMask & (1 << rendererLayer)) == 0) + { + fact.renderedPixelCount = 0; + fact.renderedAverageLuminance = 0; + fact.renderedBrightPixelCount = 0; + continue; + } measuredRenderers[index].enabled = true; camera.Render(); RenderTexture.active = target; From 7c8442e12264806a53a9e0e08b7b090dcddb8c90 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 02:42:07 +0100 Subject: [PATCH 110/292] Release EveUnity scene 0.3.50 with CultLib 1.0.14 --- TestProject/Packages/packages-lock.json | 2 +- packages/org.gamecult.eve.unity-scene/package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TestProject/Packages/packages-lock.json b/TestProject/Packages/packages-lock.json index f6390b8..8034b4d 100644 --- a/TestProject/Packages/packages-lock.json +++ b/TestProject/Packages/packages-lock.json @@ -145,7 +145,7 @@ "depth": 0, "source": "local", "dependencies": { - "org.gamecult.cultlib": "1.0.13", + "org.gamecult.cultlib": "1.0.14", "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 9efba20..a0cbaf8 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,11 +1,11 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.49", + "version": "0.3.50", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { - "org.gamecult.cultlib": "1.0.13", + "org.gamecult.cultlib": "1.0.14", "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", From 8d628a65998f4b76680003c9f925f4f2b4e4660c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 03:34:47 +0100 Subject: [PATCH 111/292] Prove cold lowering and destruction loot lifecycle --- .../PlayMode/GenericWorldCaptureTests.cs | 150 +++++++++++++----- ReleaseConsumerProject/Packages/manifest.json | 4 +- .../Packages/packages-lock.json | 10 +- .../PlayMode/GenericWorldCaptureTests.cs | 150 +++++++++++++----- docs/aetheria-released-package-witness.md | 75 +++++---- scripts/run-aetheria-daemon-world-witness.ps1 | 38 ++++- scripts/run-interactive-aetheria.ps1 | 2 +- 7 files changed, 298 insertions(+), 131 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 4615ea8..e609d55 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -210,6 +210,21 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests int pickupCollectionEventCount = 0; int initialPickupEntityCount = 0; int finalPickupEntityCount = -1; + bool destructionPickupObserved = false; + int maximumTrajectoryCount = 0; + bool hitMarkerObserved = false; + var witnessProfile = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_PROFILE") ?? + "full-session-gameplay"; + Assert.That(witnessProfile, + Is.EqualTo("cold-start-lowering").Or.EqualTo("full-session-gameplay"), + "The witness profile must name an explicit proof boundary."); + var requireSessionGameplay = string.Equals( + witnessProfile, + "full-session-gameplay", + StringComparison.Ordinal); + EveUnityAimPresentationRenderer aimRenderer = null; + EveUnityBeamPresentation beamPresentation = null; + EveUnityBeamPresentationRenderer beamRenderer = null; try { @@ -280,8 +295,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(host.PresentedEntities.CurrentGeneration.Entities.Count, Is.GreaterThan(0)); initialPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity => string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); - Assert.That(initialPickupEntityCount, Is.GreaterThan(0), - "The daemon fixture did not publish its physical starter pickup through the entity SoA."); + if (requireSessionGameplay) + Assert.That(initialPickupEntityCount, Is.Zero, + "The witness scenario must obtain salvage from daemon-owned destruction, not a boot-seeded pickup."); runtime.FeedbackAvailable += feedback => { if (!string.Equals(feedback.Kind, "pickup.collected", StringComparison.Ordinal)) return; @@ -343,6 +359,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests AssertReconciledProviderReceipt(runtime.LastReceipt, release.CommandId, provider.Selection.ProviderId, provider.Selection.SurfaceId, releaseVersion); + if (requireSessionGameplay) + { var movementVersion = runtime.ActiveVersion; var focus = runtime.SubmitFocusIntent(playerId); var focusDeadline = Time.realtimeSinceStartup + 12f; @@ -399,10 +417,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests provider.Selection.ProviderId, provider.Selection.SurfaceId, lookVersion); Assert.That(Vector3.Dot(FindMarker(root, playerId).transform.forward, aim), Is.GreaterThan(0.99f)); lookReceipt = WitnessReceipt.From("look", runtime.LastReceipt); - var aimRenderer = root.GetComponent(); + aimRenderer = root.GetComponent(); Assert.That(aimRenderer, Is.Not.Null); - var beamPresentation = EveUnityBeamPresentation.FindAll(runtime.ActiveProjection).SingleOrDefault(); + beamPresentation = EveUnityBeamPresentation.FindAll(runtime.ActiveProjection).SingleOrDefault(); Assert.That(beamPresentation, Is.Not.Null, "The provider did not advertise its continuous beam presentation."); Assert.That(beamPresentation.ActivationActionId, Is.Not.Empty, @@ -429,7 +447,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(provider.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( beamPresentation.AssetRole, beamPresentation.AssetRole, "provider-asset-ref")), Is.Not.Null, $"The provider asset catalog did not resolve beam role '{beamPresentation.AssetRole}'."); - var beamRenderer = root.GetComponent(); + beamRenderer = root.GetComponent(); Assert.That(beamRenderer, Is.Not.Null); beamRenderer.RefreshNow(); Assert.That(beamRenderer.ActiveBeamCount, Is.EqualTo(1)); @@ -439,28 +457,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamParticleSystemCount = beamRenderer.ActiveParticleSystemCount; Assert.That(tractorBeamParticleSystemCount, Is.GreaterThan(0), "The generic beam lowerer did not instantiate the provider-owned effect prefab."); - var pickupDeadline = Time.realtimeSinceStartup + 12f; - while (Time.realtimeSinceStartup < pickupDeadline && - (pickupCollection == null || - host.PresentedEntities.CurrentGeneration.Entities.Any(entity => - string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)))) - { - yield return new WaitForSecondsRealtime(0.1f); - runtime.Refresh(); - } - finalPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity => - string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); - Assert.That(pickupCollection, Is.Not.Null, - "The released client did not observe provider-owned feedback for Ymir contact collection."); - Assert.That(pickupCollectionEventCount, Is.EqualTo(1), - "One Ymir contact transaction must emit one client-visible collection event."); - Assert.That(pickupCollection.ItemKey, Is.EqualTo("scrap-metal")); - Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1)); - Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero); - Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1)); - Assert.That(finalPickupEntityCount, Is.EqualTo(initialPickupEntityCount - 1), - "Collected pickup remained in the provider-authored entity generation."); - targetFact = host.PresentedEntities.CurrentGeneration.Entities .Single(entity => entity.EntityId == targetEntityId); playerFact = host.PresentedEntities.CurrentGeneration.Entities @@ -511,22 +507,74 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); actionReceipt = WitnessReceipt.From("action", runtime.LastReceipt); - var shotDeadline = Time.realtimeSinceStartup + 15f; - while (Time.realtimeSinceStartup < shotDeadline && observedShot == null) - { - yield return new WaitForSecondsRealtime(0.05f); - runtime.Refresh(); - } - Assert.That(observedShot, Is.Not.Null, - "A reconciled fire request never reached daemon-owned lock acquisition and shot resolution."); - observedCombat = EveUnityCombatPresentation.Find(runtime.ActiveProjection); - Assert.That(observedShot.TargetEntityIndex, Is.EqualTo(targetEntityIndex)); - Assert.That(observedShot.LockQuality, Is.GreaterThanOrEqualTo(0.99)); var trajectories = root.GetComponent(); var combatRenderer = root.GetComponent(); Assert.That(trajectories, Is.Not.Null); - Assert.That(trajectories.ActiveTrajectoryCount, Is.GreaterThan(0)); Assert.That(combatRenderer, Is.Not.Null); + var shotDeadline = Time.realtimeSinceStartup + 20f; + while (Time.realtimeSinceStartup < shotDeadline && + (observedShot == null || + host.PresentedEntities.CurrentGeneration.Entities.Any(entity => + entity.EntityId == targetEntityId) || + (!destructionPickupObserved && pickupCollection == null))) + { + yield return new WaitForSecondsRealtime(0.05f); + runtime.Refresh(); + destructionPickupObserved |= host.PresentedEntities.CurrentGeneration.Entities.Any(entity => + string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); + maximumTrajectoryCount = Math.Max(maximumTrajectoryCount, trajectories.ActiveTrajectoryCount); + combatRenderer.RefreshNow(); + hitMarkerObserved |= combatRenderer.HitMarkerVisible; + } + Assert.That(observedShot, Is.Not.Null, + "A reconciled fire request never reached daemon-owned lock acquisition and shot resolution."); + Assert.That(host.PresentedEntities.CurrentGeneration.Entities.Any(entity => + entity.EntityId == targetEntityId), Is.False, + "The low-hull witness raider was not destroyed by daemon combat."); + Assert.That(destructionPickupObserved || pickupCollection != null, Is.True, + "Daemon destruction did not publish its guaranteed cargo as a pickup."); + var collectionDeadline = Time.realtimeSinceStartup + 15f; + var nextTractorAimAt = 0f; + while (Time.realtimeSinceStartup < collectionDeadline && pickupCollection == null) + { + yield return new WaitForSecondsRealtime(0.05f); + runtime.Refresh(); + maximumTrajectoryCount = Math.Max(maximumTrajectoryCount, trajectories.ActiveTrajectoryCount); + combatRenderer.RefreshNow(); + hitMarkerObserved |= combatRenderer.HitMarkerVisible; + var pickupFact = host.PresentedEntities.CurrentGeneration.Entities.FirstOrDefault(entity => + string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); + if (pickupFact == null || Time.realtimeSinceStartup < nextTractorAimAt) + continue; + playerFact = host.PresentedEntities.CurrentGeneration.Entities + .Single(entity => entity.EntityId == playerId); + var tractorAim = pickupFact.Position - playerFact.Position; + tractorAim.y = 0; + if (tractorAim.sqrMagnitude <= 0.0001f) + continue; + tractorAim.Normalize(); + runtime.SubmitLookDirectionIntent(playerId, tractorAim.x, 0f, tractorAim.z); + nextTractorAimAt = Time.realtimeSinceStartup + 0.25f; + } + finalPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity => + string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); + Assert.That(pickupCollection, Is.Not.Null, + "Destruction-created salvage never reached cargo through a Ymir contact fact."); + Assert.That(pickupCollectionEventCount, Is.EqualTo(1), + "One destruction-loot Ymir contact transaction must emit one collection event."); + Assert.That(pickupCollection.ItemKey, Is.Not.Empty, + "Destruction loot must retain its canonical typed item identity."); + Assert.That(pickupCollection.EventId, Does.StartWith("ymir-fact:"), + "Collection feedback must retain the consumed Ymir fact identity."); + Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1)); + Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero); + Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1)); + Assert.That(finalPickupEntityCount, Is.Zero, + "Collected destruction loot remained in the provider-authored entity generation."); + observedCombat = EveUnityCombatPresentation.Find(runtime.ActiveProjection); + Assert.That(observedShot.TargetEntityIndex, Is.EqualTo(targetEntityIndex)); + Assert.That(observedShot.LockQuality, Is.GreaterThanOrEqualTo(0.99)); + Assert.That(maximumTrajectoryCount, Is.GreaterThan(0)); combatRenderer.RefreshNow(); if (observedCombat != null && !string.IsNullOrWhiteSpace(observedCombat.SelectedTargetEntityId)) { @@ -534,7 +582,16 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(combatRenderer.LockVisible, Is.True); } if (observedShot.Hit && (observedShot.AppliedDamage > 0 || observedShot.ShieldAbsorbedDamage > 0)) - Assert.That(combatRenderer.HitMarkerVisible, Is.True); + Assert.That(hitMarkerObserved, Is.True); + } + + aimRenderer ??= root.GetComponent(); + Assert.That(aimRenderer, Is.Not.Null); + beamPresentation ??= EveUnityBeamPresentation.FindAll(runtime.ActiveProjection).SingleOrDefault(); + Assert.That(beamPresentation, Is.Not.Null, + "The provider did not advertise its continuous beam presentation."); + beamRenderer ??= root.GetComponent(); + finalPickupEntityCount = Math.Max(0, finalPickupEntityCount); var camera = cameraObject.AddComponent(); camera.clearFlags = CameraClearFlags.SolidColor; @@ -715,6 +772,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(fieldViewport, Is.Not.Null); Assert.That(fieldSplats, Is.Not.Null); WriteWitnessFacts( + witnessProfile, initialVersion, runtime.ActiveVersion, movementDistance, @@ -739,7 +797,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests observedCombat?.ShieldRatio ?? shieldRatioBefore, hullRatioBefore, observedCombat?.HullRatio ?? hullRatioBefore, - (float)observedShot.LockQuality, + (float)(observedShot?.LockQuality ?? 0), aimDotDistance, playerViewportWidth, playerViewportHeight, @@ -754,6 +812,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests initialPickupEntityCount, finalPickupEntityCount, pickupCollectionEventCount, + destructionPickupObserved, pickupCollection, camera.transform.position, camera.transform.forward, @@ -783,6 +842,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } private static void WriteWitnessFacts( + string witnessProfile, long initialVersion, long finalVersion, float movementDistance, @@ -822,6 +882,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests int initialPickupEntityCount, int finalPickupEntityCount, int pickupCollectionEventCount, + bool destructionPickupObserved, EveUnityFeedbackEvent pickupCollection, Vector3 cameraPosition, Vector3 cameraForward, @@ -838,6 +899,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests if (string.IsNullOrWhiteSpace(path)) return; var document = new WitnessFacts { + witnessProfile = witnessProfile, providerAdvertisement = true, providerAssets = true, environmentPresentation = environmentPresentation, @@ -879,9 +941,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamParticleSystemCount = tractorBeamParticleSystemCount, pickupCollection = pickupCollection != null && pickupCollectionEventCount == 1 && pickupCollection.CargoQuantityAfter - pickupCollection.CargoQuantityBefore == pickupCollection.ScalarValue, + destructionLoot = destructionPickupObserved && pickupCollection != null && + pickupCollection.EventId.StartsWith("ymir-fact:", StringComparison.Ordinal), initialPickupEntityCount = initialPickupEntityCount, finalPickupEntityCount = finalPickupEntityCount, pickupCollectionEventCount = pickupCollectionEventCount, + pickupEventId = pickupCollection?.EventId ?? "", pickupItemKey = pickupCollection?.ItemKey ?? "", pickupQuantity = (float)(pickupCollection?.ScalarValue ?? 0), cargoQuantityBeforePickup = (float)(pickupCollection?.CargoQuantityBefore ?? 0), @@ -909,6 +974,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests [Serializable] private sealed class WitnessFacts { + public string witnessProfile; public bool providerAdvertisement; public bool providerAssets; public bool environmentPresentation; @@ -949,9 +1015,11 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float tractorReleasedPower; public int tractorBeamParticleSystemCount; public bool pickupCollection; + public bool destructionLoot; public int initialPickupEntityCount; public int finalPickupEntityCount; public int pickupCollectionEventCount; + public string pickupEventId; public string pickupItemKey; public float pickupQuantity; public float cargoQuantityBeforePickup; diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 01675bc..57a76fa 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -6,10 +6,10 @@ "com.unity.modules.physics": "1.0.0", "com.unity.modules.uielements": "1.0.0", "com.unity.render-pipelines.universal": "17.4.0", - "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", + "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.14", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.49", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.50", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index cc2cc9d..af58e02 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -119,11 +119,11 @@ } }, "org.gamecult.cultlib": { - "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", + "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.14", "depth": 0, "source": "git", "dependencies": {}, - "hash": "feb5c71513e71d681699f462fe3682b3168c6f73" + "hash": "4b7162022a8976f7941b5a7a69acf50f1b6d532b" }, "org.gamecult.eve.plugin-fields": { "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", @@ -144,18 +144,18 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.49", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.50", "depth": 0, "source": "git", "dependencies": { - "org.gamecult.cultlib": "1.0.13", + "org.gamecult.cultlib": "1.0.14", "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "e6ba1a36f6f0bce8df2c9834c25a8a980f03413c" + "hash": "7c8442e12264806a53a9e0e08b7b090dcddb8c90" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 4615ea8..e609d55 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -210,6 +210,21 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests int pickupCollectionEventCount = 0; int initialPickupEntityCount = 0; int finalPickupEntityCount = -1; + bool destructionPickupObserved = false; + int maximumTrajectoryCount = 0; + bool hitMarkerObserved = false; + var witnessProfile = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_PROFILE") ?? + "full-session-gameplay"; + Assert.That(witnessProfile, + Is.EqualTo("cold-start-lowering").Or.EqualTo("full-session-gameplay"), + "The witness profile must name an explicit proof boundary."); + var requireSessionGameplay = string.Equals( + witnessProfile, + "full-session-gameplay", + StringComparison.Ordinal); + EveUnityAimPresentationRenderer aimRenderer = null; + EveUnityBeamPresentation beamPresentation = null; + EveUnityBeamPresentationRenderer beamRenderer = null; try { @@ -280,8 +295,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(host.PresentedEntities.CurrentGeneration.Entities.Count, Is.GreaterThan(0)); initialPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity => string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); - Assert.That(initialPickupEntityCount, Is.GreaterThan(0), - "The daemon fixture did not publish its physical starter pickup through the entity SoA."); + if (requireSessionGameplay) + Assert.That(initialPickupEntityCount, Is.Zero, + "The witness scenario must obtain salvage from daemon-owned destruction, not a boot-seeded pickup."); runtime.FeedbackAvailable += feedback => { if (!string.Equals(feedback.Kind, "pickup.collected", StringComparison.Ordinal)) return; @@ -343,6 +359,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests AssertReconciledProviderReceipt(runtime.LastReceipt, release.CommandId, provider.Selection.ProviderId, provider.Selection.SurfaceId, releaseVersion); + if (requireSessionGameplay) + { var movementVersion = runtime.ActiveVersion; var focus = runtime.SubmitFocusIntent(playerId); var focusDeadline = Time.realtimeSinceStartup + 12f; @@ -399,10 +417,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests provider.Selection.ProviderId, provider.Selection.SurfaceId, lookVersion); Assert.That(Vector3.Dot(FindMarker(root, playerId).transform.forward, aim), Is.GreaterThan(0.99f)); lookReceipt = WitnessReceipt.From("look", runtime.LastReceipt); - var aimRenderer = root.GetComponent(); + aimRenderer = root.GetComponent(); Assert.That(aimRenderer, Is.Not.Null); - var beamPresentation = EveUnityBeamPresentation.FindAll(runtime.ActiveProjection).SingleOrDefault(); + beamPresentation = EveUnityBeamPresentation.FindAll(runtime.ActiveProjection).SingleOrDefault(); Assert.That(beamPresentation, Is.Not.Null, "The provider did not advertise its continuous beam presentation."); Assert.That(beamPresentation.ActivationActionId, Is.Not.Empty, @@ -429,7 +447,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(provider.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( beamPresentation.AssetRole, beamPresentation.AssetRole, "provider-asset-ref")), Is.Not.Null, $"The provider asset catalog did not resolve beam role '{beamPresentation.AssetRole}'."); - var beamRenderer = root.GetComponent(); + beamRenderer = root.GetComponent(); Assert.That(beamRenderer, Is.Not.Null); beamRenderer.RefreshNow(); Assert.That(beamRenderer.ActiveBeamCount, Is.EqualTo(1)); @@ -439,28 +457,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamParticleSystemCount = beamRenderer.ActiveParticleSystemCount; Assert.That(tractorBeamParticleSystemCount, Is.GreaterThan(0), "The generic beam lowerer did not instantiate the provider-owned effect prefab."); - var pickupDeadline = Time.realtimeSinceStartup + 12f; - while (Time.realtimeSinceStartup < pickupDeadline && - (pickupCollection == null || - host.PresentedEntities.CurrentGeneration.Entities.Any(entity => - string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)))) - { - yield return new WaitForSecondsRealtime(0.1f); - runtime.Refresh(); - } - finalPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity => - string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); - Assert.That(pickupCollection, Is.Not.Null, - "The released client did not observe provider-owned feedback for Ymir contact collection."); - Assert.That(pickupCollectionEventCount, Is.EqualTo(1), - "One Ymir contact transaction must emit one client-visible collection event."); - Assert.That(pickupCollection.ItemKey, Is.EqualTo("scrap-metal")); - Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1)); - Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero); - Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1)); - Assert.That(finalPickupEntityCount, Is.EqualTo(initialPickupEntityCount - 1), - "Collected pickup remained in the provider-authored entity generation."); - targetFact = host.PresentedEntities.CurrentGeneration.Entities .Single(entity => entity.EntityId == targetEntityId); playerFact = host.PresentedEntities.CurrentGeneration.Entities @@ -511,22 +507,74 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); actionReceipt = WitnessReceipt.From("action", runtime.LastReceipt); - var shotDeadline = Time.realtimeSinceStartup + 15f; - while (Time.realtimeSinceStartup < shotDeadline && observedShot == null) - { - yield return new WaitForSecondsRealtime(0.05f); - runtime.Refresh(); - } - Assert.That(observedShot, Is.Not.Null, - "A reconciled fire request never reached daemon-owned lock acquisition and shot resolution."); - observedCombat = EveUnityCombatPresentation.Find(runtime.ActiveProjection); - Assert.That(observedShot.TargetEntityIndex, Is.EqualTo(targetEntityIndex)); - Assert.That(observedShot.LockQuality, Is.GreaterThanOrEqualTo(0.99)); var trajectories = root.GetComponent(); var combatRenderer = root.GetComponent(); Assert.That(trajectories, Is.Not.Null); - Assert.That(trajectories.ActiveTrajectoryCount, Is.GreaterThan(0)); Assert.That(combatRenderer, Is.Not.Null); + var shotDeadline = Time.realtimeSinceStartup + 20f; + while (Time.realtimeSinceStartup < shotDeadline && + (observedShot == null || + host.PresentedEntities.CurrentGeneration.Entities.Any(entity => + entity.EntityId == targetEntityId) || + (!destructionPickupObserved && pickupCollection == null))) + { + yield return new WaitForSecondsRealtime(0.05f); + runtime.Refresh(); + destructionPickupObserved |= host.PresentedEntities.CurrentGeneration.Entities.Any(entity => + string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); + maximumTrajectoryCount = Math.Max(maximumTrajectoryCount, trajectories.ActiveTrajectoryCount); + combatRenderer.RefreshNow(); + hitMarkerObserved |= combatRenderer.HitMarkerVisible; + } + Assert.That(observedShot, Is.Not.Null, + "A reconciled fire request never reached daemon-owned lock acquisition and shot resolution."); + Assert.That(host.PresentedEntities.CurrentGeneration.Entities.Any(entity => + entity.EntityId == targetEntityId), Is.False, + "The low-hull witness raider was not destroyed by daemon combat."); + Assert.That(destructionPickupObserved || pickupCollection != null, Is.True, + "Daemon destruction did not publish its guaranteed cargo as a pickup."); + var collectionDeadline = Time.realtimeSinceStartup + 15f; + var nextTractorAimAt = 0f; + while (Time.realtimeSinceStartup < collectionDeadline && pickupCollection == null) + { + yield return new WaitForSecondsRealtime(0.05f); + runtime.Refresh(); + maximumTrajectoryCount = Math.Max(maximumTrajectoryCount, trajectories.ActiveTrajectoryCount); + combatRenderer.RefreshNow(); + hitMarkerObserved |= combatRenderer.HitMarkerVisible; + var pickupFact = host.PresentedEntities.CurrentGeneration.Entities.FirstOrDefault(entity => + string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); + if (pickupFact == null || Time.realtimeSinceStartup < nextTractorAimAt) + continue; + playerFact = host.PresentedEntities.CurrentGeneration.Entities + .Single(entity => entity.EntityId == playerId); + var tractorAim = pickupFact.Position - playerFact.Position; + tractorAim.y = 0; + if (tractorAim.sqrMagnitude <= 0.0001f) + continue; + tractorAim.Normalize(); + runtime.SubmitLookDirectionIntent(playerId, tractorAim.x, 0f, tractorAim.z); + nextTractorAimAt = Time.realtimeSinceStartup + 0.25f; + } + finalPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity => + string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); + Assert.That(pickupCollection, Is.Not.Null, + "Destruction-created salvage never reached cargo through a Ymir contact fact."); + Assert.That(pickupCollectionEventCount, Is.EqualTo(1), + "One destruction-loot Ymir contact transaction must emit one collection event."); + Assert.That(pickupCollection.ItemKey, Is.Not.Empty, + "Destruction loot must retain its canonical typed item identity."); + Assert.That(pickupCollection.EventId, Does.StartWith("ymir-fact:"), + "Collection feedback must retain the consumed Ymir fact identity."); + Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1)); + Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero); + Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1)); + Assert.That(finalPickupEntityCount, Is.Zero, + "Collected destruction loot remained in the provider-authored entity generation."); + observedCombat = EveUnityCombatPresentation.Find(runtime.ActiveProjection); + Assert.That(observedShot.TargetEntityIndex, Is.EqualTo(targetEntityIndex)); + Assert.That(observedShot.LockQuality, Is.GreaterThanOrEqualTo(0.99)); + Assert.That(maximumTrajectoryCount, Is.GreaterThan(0)); combatRenderer.RefreshNow(); if (observedCombat != null && !string.IsNullOrWhiteSpace(observedCombat.SelectedTargetEntityId)) { @@ -534,7 +582,16 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(combatRenderer.LockVisible, Is.True); } if (observedShot.Hit && (observedShot.AppliedDamage > 0 || observedShot.ShieldAbsorbedDamage > 0)) - Assert.That(combatRenderer.HitMarkerVisible, Is.True); + Assert.That(hitMarkerObserved, Is.True); + } + + aimRenderer ??= root.GetComponent(); + Assert.That(aimRenderer, Is.Not.Null); + beamPresentation ??= EveUnityBeamPresentation.FindAll(runtime.ActiveProjection).SingleOrDefault(); + Assert.That(beamPresentation, Is.Not.Null, + "The provider did not advertise its continuous beam presentation."); + beamRenderer ??= root.GetComponent(); + finalPickupEntityCount = Math.Max(0, finalPickupEntityCount); var camera = cameraObject.AddComponent(); camera.clearFlags = CameraClearFlags.SolidColor; @@ -715,6 +772,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(fieldViewport, Is.Not.Null); Assert.That(fieldSplats, Is.Not.Null); WriteWitnessFacts( + witnessProfile, initialVersion, runtime.ActiveVersion, movementDistance, @@ -739,7 +797,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests observedCombat?.ShieldRatio ?? shieldRatioBefore, hullRatioBefore, observedCombat?.HullRatio ?? hullRatioBefore, - (float)observedShot.LockQuality, + (float)(observedShot?.LockQuality ?? 0), aimDotDistance, playerViewportWidth, playerViewportHeight, @@ -754,6 +812,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests initialPickupEntityCount, finalPickupEntityCount, pickupCollectionEventCount, + destructionPickupObserved, pickupCollection, camera.transform.position, camera.transform.forward, @@ -783,6 +842,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } private static void WriteWitnessFacts( + string witnessProfile, long initialVersion, long finalVersion, float movementDistance, @@ -822,6 +882,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests int initialPickupEntityCount, int finalPickupEntityCount, int pickupCollectionEventCount, + bool destructionPickupObserved, EveUnityFeedbackEvent pickupCollection, Vector3 cameraPosition, Vector3 cameraForward, @@ -838,6 +899,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests if (string.IsNullOrWhiteSpace(path)) return; var document = new WitnessFacts { + witnessProfile = witnessProfile, providerAdvertisement = true, providerAssets = true, environmentPresentation = environmentPresentation, @@ -879,9 +941,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamParticleSystemCount = tractorBeamParticleSystemCount, pickupCollection = pickupCollection != null && pickupCollectionEventCount == 1 && pickupCollection.CargoQuantityAfter - pickupCollection.CargoQuantityBefore == pickupCollection.ScalarValue, + destructionLoot = destructionPickupObserved && pickupCollection != null && + pickupCollection.EventId.StartsWith("ymir-fact:", StringComparison.Ordinal), initialPickupEntityCount = initialPickupEntityCount, finalPickupEntityCount = finalPickupEntityCount, pickupCollectionEventCount = pickupCollectionEventCount, + pickupEventId = pickupCollection?.EventId ?? "", pickupItemKey = pickupCollection?.ItemKey ?? "", pickupQuantity = (float)(pickupCollection?.ScalarValue ?? 0), cargoQuantityBeforePickup = (float)(pickupCollection?.CargoQuantityBefore ?? 0), @@ -909,6 +974,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests [Serializable] private sealed class WitnessFacts { + public string witnessProfile; public bool providerAdvertisement; public bool providerAssets; public bool environmentPresentation; @@ -949,9 +1015,11 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float tractorReleasedPower; public int tractorBeamParticleSystemCount; public bool pickupCollection; + public bool destructionLoot; public int initialPickupEntityCount; public int finalPickupEntityCount; public int pickupCollectionEventCount; + public string pickupEventId; public string pickupItemKey; public float pickupQuantity; public float cargoQuantityBeforePickup; diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index b738502..99b21ab 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -2,22 +2,24 @@ ## Current result -The warm-cache released-package witness passes with: +The released-package witness passes in separate cold-lowering and warm-gameplay +profiles with: -- `org.gamecult.eve.unity-scene` `0.3.44`, commit - `03ce78031f28d7bfae4dcdc477a7273c6ecae55b`; +- `org.gamecult.eve.unity-scene` `0.3.50`, commit + `7c8442e12264806a53a9e0e08b7b090dcddb8c90`; - `org.gamecult.eve.plugin-fields` `0.2.3`, commit `c5a4a75c1b727499b16c2dae1895f29e2a9f72f0`; - `org.gamecult.eve.surface` `0.2.2`, commit `140e1bd963a0033e66777a3b2c5fe6e9c97dfe32`; - `org.gamecult.eve.unity-uitoolkit` `0.1.1`, commit `4d0cbe0185bdc4fc65eb63503a7c5cb578539669`; -- `org.gamecult.cultlib` `1.0.13`, commit - `feb5c71513e71d681699f462fe3682b3168c6f73`; +- `org.gamecult.cultlib` `1.0.14`, commit + `4b7162022a8976f7941b5a7a69acf50f1b6d532b`; - the generic `ReleaseConsumerProject` client connected directly to the Aetheria daemon. -Evidence is in `artifacts/aetheria-daemon-fossil-fog-producers`. The PlayMode test +Evidence is in `artifacts/aetheria-daemon-cultlib-1014-cold-final` and +`artifacts/aetheria-daemon-cultlib-1014-warm-final`. The warm PlayMode test passed and recorded provider-owned reconciled movement, look, targeting, tractor press, tractor release, contact-gated cargo collection, and action receipts. The daemon-owned look direction reached the SoA body rotation, @@ -50,7 +52,7 @@ Fields splat layers. Its surface contract contains logical ports such as names or pass indices. The selected `unity-scene` provider asset variant owns that concrete shader ABI through `unity.volume.*` metadata. The released generic lowerer resolved the provider shader and dither texture, rasterized all -four layers, and composited `21` pilot-camera frames from daemon frame `223`. +four layers, and composited `91` pilot-camera frames through daemon frame `643`. The provider variant advertises the fossil shader's raymarch, temporal-history, and composite passes plus their logical ports. EveUnity allocates and resets the history targets generically; a partial temporal ABI fails closed. @@ -64,19 +66,20 @@ daemon lock progress `1.0`; Unity does not manufacture or smooth that value. Camera-channel facts: - provider-authored player renderers: `13`, including the lowered tractor effect; -- pilot changed pixels: `191,353`; -- pilot average luminance: `0.0283105`; -- pilot bright pixels: `705`; -- map-channel renderers: `11`; -- map changed pixels: `4,444`; +- pilot changed pixels: `196,179`; +- pilot average luminance: `0.0993295`; +- pilot bright pixels: `1,348`; +- map-channel renderers: `10`; +- map changed pixels: `4,627`; - native cockpit progress bars: `7`; - daemon tractor power at capture: `1.0` after the held input completed its authored ramp; - daemon tractor power after the advertised release: `0`; - provider tractor particle systems: `1`; -- pickup entities before/after collection: `1` / `0`; +- pickup entities before/after destruction and collection: `0` / `0`; - provider `pickup.collected` events: exactly `1`; -- collected item and quantity: `scrap-metal`, `1`; +- collected item and quantity: one canonical `aetheria.item_definition:*` key, + quantity `1`; - authoritative cargo quantity before/after: `0` / `1`; - the pilot camera excludes the advertised map layer; - the player prefab's embedded layer-14 map icon contributes exactly `0` pilot @@ -87,7 +90,7 @@ Visual inspection confirms that map glyphs are absent from the pilot frame and present in the map-only frame. This is transport, authority, field-production, and lowering proof, not visual parity: the current pilot capture is nearly black with sparse clipped geometry instead of the fossil's bright blue gravity-shaped -fog sea. Release `0.3.44` adds generic lowering for Eve Fields `0.2.3`'s +fog sea. Release `0.3.50` includes generic lowering for Eve Fields `0.2.3`'s `AnimatedRadialCosine` source alongside `PowerPulse`, dither-scale, temporal-history, and finite-look-at semantics. Aetheria publishes the fossil global animated simplex/cellular/ambient producer stack, body-owned radial wave @@ -119,10 +122,12 @@ build removes the fossil's embedded tractor object from player/ship prefabs, so the standalone `beam.presentation` prefab is the only tractor renderer. Its cyan/yellow dotted band is visible in the pilot capture and absent from the map capture. -The same released run begins with one provider-owned pickup in the entity SoA. -Held tractor input produces a Ymir Begin contact fact; the daemon consumes that -fact once, removes the pickup once, changes cargo from `0` to `1`, and emits one -`pickup.collected` feedback event carrying the exact delta. Aetheria's Ymir body +The warm gameplay run begins with no pickup. A daemon-authoritative shot destroys +the deterministic salvage target and creates one provider-owned pickup carrying +a canonical generated cargo item. Held tractor input produces a Ymir Begin +contact fact; the daemon consumes that fact once, removes the pickup once, +changes cargo from `0` to `1`, and emits one `pickup.collected` feedback event +whose identity begins with `ymir-fact:`. Aetheria's Ymir body mapping excludes stations and non-ship world bodies from pickup collision facts, so ambient station contact cannot become a cargo writer. Capacity rejection and its `cargo-capacity` reason are covered by the daemon smoke, not claimed as a @@ -138,19 +143,23 @@ Primary artifacts: - `aetheria-daemon-world.png`: pilot-camera capture; - `aetheria-daemon-map.png`: map-only capture. -## Cold delivery is not currently proven +## Cold delivery proof -The current `46,412,252` byte Unity bundle (SHA-256 -`bc45d84b54666e813bfccca12496ba4a3533ed08896dfc57de726749574fa796`) -times out from an empty cache. Its bytes are -still transported through batched snapshot records rather than the intended -mapped/network body transport. Increasing the Unity timeout would hide the -transport fault and is not an accepted fix. +The `cold-start-lowering` profile starts with zero bodies and zero partials. It +receives the `46,412,384` byte Unity bundle through the managed +`cultmesh.content.v1` session, verifies SHA-256 +`f5960f240ecfd56de417e8e51fde34cfb962fc6fcf0840cc373190e7dbc6048f`, +atomically promotes one `.body`, and leaves zero partials under the unchanged +300-second deadline. The full witness took `84.991` seconds; the Unity test took +`43.212` seconds. It then lowered provider assets, movement, environment, four +field layers across `127` composites, and both camera channels. The pilot camera +excluded map objects and the map camera included them. -A cold witness may be claimed only when an empty cache receives the bundle over -the intended body transport, atomically promotes the hash-addressed `.body`, -leaves no `.partial` file, and completes the same released-client assertions. - -`artifacts/render-channel-witness-gravity-cold-4` records a historical 0.3.18 -cold run. It remains useful regression evidence, but it does not describe the -health of the current transport path. +Cold asset acquisition and transient gameplay are deliberately separate proof +profiles. Starting Unity before the provider means download time is not a +stable lifetime for a boot-seeded enemy or pickup. The cold profile proves +delivery and lowering; `full-session-gameplay` starts a fresh warm authoritative +session and proves combat, destruction-created loot, Ymir contact collection, +and exactly-once receipts. Neither profile claims negotiated mapped/zero-copy +CDN delivery: the released path is the managed network content session and +still copies/fragments bytes in process. diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 20d049a..967fe85 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,10 +13,10 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "e6ba1a36f6f0bce8df2c9834c25a8a980f03413c" +$expectedEveUnityCommit = "7c8442e12264806a53a9e0e08b7b090dcddb8c90" $expectedEveFieldsCommit = "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" -$expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" +$expectedCultLibCommit = "4b7162022a8976f7941b5a7a69acf50f1b6d532b" $repoRoot = Split-Path -Parent $PSScriptRoot $projectRoot = $ClientProject $outputRoot = if ([IO.Path]::IsPathRooted($OutputDirectory)) { $OutputDirectory } else { Join-Path $repoRoot $OutputDirectory } @@ -65,6 +65,11 @@ if ($CacheState -eq "warm" -and -not $cacheWasWarm) { throw "Warm witness requested without an existing verified bundle cache at $assetCachePath" } $observedCacheState = if ($cacheWasWarm) { "warm" } else { "cold" } +$witnessProfile = if ($observedCacheState -eq "cold") { + "cold-start-lowering" +} else { + "full-session-gameplay" +} $witnessStartedAt = [DateTimeOffset]::UtcNow foreach ($ephemeralPath in @( $replicaPath, @@ -161,7 +166,7 @@ $daemonArguments = @( "--client-cultmesh-host", "127.0.0.1", "--client-cultmesh-advertise-host", "127.0.0.1", "--client-cultmesh-port", $Port, - "--tick-interval-ms", 250, + "--tick-interval-ms", 20, "--fixed-delta-ms", 20, "--no-odin-announcements" ) @@ -177,6 +182,7 @@ $env:EVEUNITY_DISABLE_AUTO_LAUNCHER = "1" $env:EVEUNITY_ASSET_CACHE_PATH = $assetCachePath $env:EVEUNITY_WITNESS_FACTS_PATH = $factsPath $env:EVEUNITY_PROVIDER_READY_PATH = $providerReadyPath +$env:EVEUNITY_WITNESS_PROFILE = $witnessProfile $arguments = @( "-batchmode", "-projectPath", $projectRoot, "-runTests", "-testPlatform", "PlayMode", @@ -231,9 +237,20 @@ try { } } $facts = Get-Content -LiteralPath $factsPath -Raw | ConvertFrom-Json - if (-not $facts.combatPresentation -or [string]::IsNullOrWhiteSpace($facts.shotId) -or - [double]$facts.lockProgress -le 0.99) { - throw "Live witness did not observe daemon-owned selection, completed lock, and a resolved shot." + if ($facts.witnessProfile -ne $witnessProfile) { + throw "Live witness ran the wrong proof profile. expected=$witnessProfile actual=$($facts.witnessProfile)" + } + if (-not $facts.providerAssets -or -not $facts.environmentPresentation -or + -not $facts.movement -or -not $facts.pilotCameraExcludesMapChannel -or + -not $facts.mapCameraIncludesMapChannel -or [int]$facts.fieldVolumeLayerCount -le 0 -or + [long]$facts.fieldVolumeCompositeCount -le 0) { + throw "Live witness did not prove provider assets, movement, Fields lowering, and camera-channel separation." + } + if ($witnessProfile -eq "full-session-gameplay" -and + (-not $facts.combatPresentation -or [string]::IsNullOrWhiteSpace($facts.shotId) -or + [double]$facts.lockProgress -le 0.99 -or -not $facts.pickupCollection -or + -not $facts.destructionLoot -or [int]$facts.pickupCollectionEventCount -ne 1)) { + throw "Full-session witness did not prove combat and exactly-once destruction-loot Ymir contact collection." } $releaseLock = Get-Content -LiteralPath (Join-Path $repoRoot "ReleaseConsumerProject\Packages\packages-lock.json") -Raw | ConvertFrom-Json $releasedPackageClient = $projectRoot -eq "ReleaseConsumerProject" @@ -297,6 +314,7 @@ try { generatedAtUtc = [DateTimeOffset]::UtcNow.ToString("O") execution = [ordered]@{ cacheState = $observedCacheState + profile = $witnessProfile durationMs = $durationMs testDurationMs = $testDurationMs } @@ -335,7 +353,11 @@ try { sizeBytes = $resultsArtifact.Length } ) - authority = "released-generic-runtime-observes-provider-assets-authoritative-combat-receipts-and-camera-channel-separation" + authority = if ($witnessProfile -eq "full-session-gameplay") { + "released-generic-runtime-observes-provider-assets-authoritative-gameplay-receipts-ymir-contact-collection-and-camera-channel-separation" + } else { + "released-generic-runtime-cold-loads-provider-assets-lowers-playable-world-and-preserves-camera-channel-separation" + } } $witness | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $witnessPath -Encoding UTF8 $stateWitnessPath = Join-Path $outputRoot "runtime-witness.$observedCacheState.json" @@ -349,6 +371,6 @@ try { finally { if ($null -ne $unity -and -not $unity.HasExited) { Stop-Process -Id $unity.Id -Force -ErrorAction SilentlyContinue } if ($null -ne $daemon -and -not $daemon.HasExited) { Stop-Process -Id $daemon.Id -Force } - Remove-Item Env:EVEUNITY_RENDEZVOUS_ENDPOINT, Env:EVEUNITY_PROVIDER_ENDPOINT, Env:EVEUNITY_PROVIDER_ID, Env:EVEUNITY_SURFACE_ID, Env:EVEUNITY_REPLICA_PATH, Env:EVEUNITY_AETHERIA_CAPTURE_PATH, Env:EVEUNITY_AETHERIA_MAP_CAPTURE_PATH, Env:EVEUNITY_DISABLE_AUTO_LAUNCHER, Env:EVEUNITY_ASSET_CACHE_PATH, Env:EVEUNITY_WITNESS_FACTS_PATH, Env:EVEUNITY_PROVIDER_READY_PATH -ErrorAction SilentlyContinue + Remove-Item Env:EVEUNITY_RENDEZVOUS_ENDPOINT, Env:EVEUNITY_PROVIDER_ENDPOINT, Env:EVEUNITY_PROVIDER_ID, Env:EVEUNITY_SURFACE_ID, Env:EVEUNITY_REPLICA_PATH, Env:EVEUNITY_AETHERIA_CAPTURE_PATH, Env:EVEUNITY_AETHERIA_MAP_CAPTURE_PATH, Env:EVEUNITY_DISABLE_AUTO_LAUNCHER, Env:EVEUNITY_ASSET_CACHE_PATH, Env:EVEUNITY_WITNESS_FACTS_PATH, Env:EVEUNITY_PROVIDER_READY_PATH, Env:EVEUNITY_WITNESS_PROFILE -ErrorAction SilentlyContinue Remove-Item Env:AETHERIA_TRACE_EVE_SNAPSHOTS -ErrorAction SilentlyContinue } diff --git a/scripts/run-interactive-aetheria.ps1 b/scripts/run-interactive-aetheria.ps1 index 3dae7e9..c02e208 100644 --- a/scripts/run-interactive-aetheria.ps1 +++ b/scripts/run-interactive-aetheria.ps1 @@ -85,7 +85,7 @@ $daemon = Start-Process dotnet -ArgumentList @( "--client-cultmesh-host", "127.0.0.1", "--client-cultmesh-advertise-host", "127.0.0.1", "--client-cultmesh-port", $Port, - "--tick-interval-ms", 250, + "--tick-interval-ms", 20, "--fixed-delta-ms", 20, "--no-odin-announcements" ) -PassThru -WindowStyle Hidden -RedirectStandardOutput $daemonLog -RedirectStandardError "$daemonLog.error" From ccb5817a60e527a862d609e219a71d16aa2a84ca Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 04:13:28 +0100 Subject: [PATCH 112/292] Honor semantic scale for provider prefabs --- .../Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs | 4 +--- .../Tests/Editor/EveUnitySceneSurfaceLowererTests.cs | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs index e1b5bf3..bf1dad0 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs @@ -239,9 +239,7 @@ namespace GameCult.Eve.UnityScene : $"{entity.EntityId} ({entity.Label})"; instance.transform.localPosition = new Vector3(entity.PositionX, entity.PositionY, entity.PositionZ); instance.transform.localRotation = Quaternion.Euler(0f, entity.RotationY, 0f); - if (entity.Radius > 0f && - string.Equals(asset.PresentationKind, "unity-generated-placeholder", StringComparison.Ordinal)) - instance.transform.localScale = Vector3.one * entity.Radius; + instance.transform.localScale = Vector3.one * (entity.Radius > 0f ? entity.Radius : 1f); var marker = instance.GetComponent(); if (marker == null) diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 0f10bce..0e163eb 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -1345,7 +1345,7 @@ namespace GameCult.Eve.UnityScene.Tests } [Test] - public void ProviderPrefabKeepsAuthoredScaleWhileFallbackUsesSemanticRadius() + public void SemanticScaleWrapsProviderAuthoredScaleAndFallbackGeometry() { var root = new GameObject("world"); var prefab = new GameObject("provider-prefab"); @@ -1360,7 +1360,7 @@ namespace GameCult.Eve.UnityScene.Tests "prefab.ship", "ship", "provider-asset-ref")); var instance = root.transform.GetChild(0); - Assert.That(instance.localScale, Is.EqualTo(Vector3.one)); + Assert.That(instance.localScale, Is.EqualTo(Vector3.one * 12f)); Assert.That(instance.GetChild(0).localScale, Is.EqualTo(new Vector3(2f, 3f, 4f))); var fallbackSink = new EveUnityGameObjectPlayableWorldSceneSink( From 21ac39e7bdfca9d81a0b7d89834f7e9da60aaad6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 04:14:02 +0100 Subject: [PATCH 113/292] Release EveUnity scene 0.3.51 --- packages/org.gamecult.eve.unity-scene/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index a0cbaf8..1510a01 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.50", + "version": "0.3.51", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 1810118652e8f281a77a9ff77f0b41cff5330cd5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 04:15:29 +0100 Subject: [PATCH 114/292] Pin release consumer to scene 0.3.51 --- ReleaseConsumerProject/Packages/manifest.json | 2 +- ReleaseConsumerProject/Packages/packages-lock.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 57a76fa..19077d6 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.14", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.50", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.51", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index af58e02..2c99eea 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.50", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.51", "depth": 0, "source": "git", "dependencies": { From c5e73df1ff5bd0dd8efc8674980d994bffdd1af7 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 04:18:14 +0100 Subject: [PATCH 115/292] Resolve release consumer to scene 0.3.51 --- ReleaseConsumerProject/Packages/packages-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 2c99eea..d2188be 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -155,7 +155,7 @@ "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "7c8442e12264806a53a9e0e08b7b090dcddb8c90" + "hash": "21ac39e7bdfca9d81a0b7d89834f7e9da60aaad6" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", From 93a95b23dfebb05daa8f916c2553ca98df9b160b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 04:41:06 +0100 Subject: [PATCH 116/292] Prove generic celestial world lowering --- .../PlayMode/GenericWorldCaptureTests.cs | 60 +++++++++++++++++-- .../PlayMode/GenericWorldCaptureTests.cs | 60 +++++++++++++++++-- docs/aetheria-released-package-witness.md | 47 +++++++++------ scripts/run-aetheria-daemon-world-witness.ps1 | 17 +++++- 4 files changed, 156 insertions(+), 28 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index e609d55..acc016a 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -225,6 +225,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests EveUnityAimPresentationRenderer aimRenderer = null; EveUnityBeamPresentation beamPresentation = null; EveUnityBeamPresentationRenderer beamRenderer = null; + EveUnityShotTrajectoryRenderer trajectories = null; try { @@ -279,7 +280,13 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(cockpitProgressCount, Is.GreaterThanOrEqualTo(7)); Assert.That(loweredSurface.Q("aetheria.daemon.game.frame").style.display.value, Is.EqualTo(DisplayStyle.None), "Daemon diagnostics leaked into the pilot UI."); - host.ShotAvailable += shot => observedShot = shot; + trajectories = root.GetComponent(); + Assert.That(trajectories, Is.Not.Null); + host.ShotAvailable += shot => + { + observedShot = shot; + maximumTrajectoryCount = Math.Max(maximumTrajectoryCount, trajectories.ActiveTrajectoryCount); + }; runtime = host.Runtime; Assert.That(runtime, Is.Not.Null); Assert.That(runtime.ActiveWorld.PlayerEntityId, Is.Not.Empty); @@ -507,9 +514,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); actionReceipt = WitnessReceipt.From("action", runtime.LastReceipt); - var trajectories = root.GetComponent(); var combatRenderer = root.GetComponent(); - Assert.That(trajectories, Is.Not.Null); Assert.That(combatRenderer, Is.Not.Null); var shotDeadline = Time.realtimeSinceStartup + 20f; while (Time.realtimeSinceStartup < shotDeadline && @@ -823,7 +828,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Convert.ToSingle(fieldViewport.GetType().GetProperty("MaxX")?.GetValue(fieldViewport)), Convert.ToSingle(fieldViewport.GetType().GetProperty("MaxY")?.GetValue(fieldViewport)), Convert.ToInt32(fieldSplats.GetType().GetProperty("Count")?.GetValue(fieldSplats)), - playerRendererFacts); + playerRendererFacts, + root.GetComponentsInChildren(includeInactive: true) + .Select(marker => PresentedEntityFact.From(camera, marker)) + .ToArray()); } finally { @@ -893,7 +901,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float fieldViewportMaxX, float fieldViewportMaxY, int fieldSplatCount, - RendererFact[] playerRendererFacts) + RendererFact[] playerRendererFacts, + PresentedEntityFact[] presentedEntityFacts) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); if (string.IsNullOrWhiteSpace(path)) return; @@ -961,6 +970,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests fieldViewportMaxY = fieldViewportMaxY, fieldSplatCount = fieldSplatCount, playerRenderers = playerRendererFacts, + presentedEntities = presentedEntityFacts, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, @@ -1034,6 +1044,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float fieldViewportMaxY; public int fieldSplatCount; public RendererFact[] playerRenderers; + public PresentedEntityFact[] presentedEntities; public int mapChannelRendererCount; public int mapChangedPixels; public bool pilotCameraExcludesMapChannel; @@ -1110,6 +1121,45 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } } + [Serializable] + private sealed class PresentedEntityFact + { + public string entityId; + public string entityKind; + public string label; + public string assetRef; + public string worldPosition; + public string worldScale; + public int rendererCount; + public int enabledRendererCount; + public bool intersectsPilotFrustum; + public float viewportWidth; + public float viewportHeight; + + public static PresentedEntityFact From(Camera camera, EveUnityPlayableWorldEntityMarker marker) + { + var renderers = marker.GetComponentsInChildren(includeInactive: true); + MeasureViewportCoverage(camera, renderers, out var width, out var height); + var planes = GeometryUtility.CalculateFrustumPlanes(camera); + return new PresentedEntityFact + { + entityId = marker.EntityId, + entityKind = marker.EntityKind, + label = marker.Label, + assetRef = marker.AssetRef, + worldPosition = FormatVector(marker.transform.position), + worldScale = FormatVector(marker.transform.lossyScale), + rendererCount = renderers.Length, + enabledRendererCount = renderers.Count(renderer => renderer.enabled && renderer.gameObject.activeInHierarchy), + intersectsPilotFrustum = renderers.Any(renderer => + renderer.enabled && renderer.gameObject.activeInHierarchy && + GeometryUtility.TestPlanesAABB(planes, renderer.bounds)), + viewportWidth = width, + viewportHeight = height + }; + } + } + [Serializable] private sealed class MaterialFact { diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index e609d55..acc016a 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -225,6 +225,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests EveUnityAimPresentationRenderer aimRenderer = null; EveUnityBeamPresentation beamPresentation = null; EveUnityBeamPresentationRenderer beamRenderer = null; + EveUnityShotTrajectoryRenderer trajectories = null; try { @@ -279,7 +280,13 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(cockpitProgressCount, Is.GreaterThanOrEqualTo(7)); Assert.That(loweredSurface.Q("aetheria.daemon.game.frame").style.display.value, Is.EqualTo(DisplayStyle.None), "Daemon diagnostics leaked into the pilot UI."); - host.ShotAvailable += shot => observedShot = shot; + trajectories = root.GetComponent(); + Assert.That(trajectories, Is.Not.Null); + host.ShotAvailable += shot => + { + observedShot = shot; + maximumTrajectoryCount = Math.Max(maximumTrajectoryCount, trajectories.ActiveTrajectoryCount); + }; runtime = host.Runtime; Assert.That(runtime, Is.Not.Null); Assert.That(runtime.ActiveWorld.PlayerEntityId, Is.Not.Empty); @@ -507,9 +514,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); actionReceipt = WitnessReceipt.From("action", runtime.LastReceipt); - var trajectories = root.GetComponent(); var combatRenderer = root.GetComponent(); - Assert.That(trajectories, Is.Not.Null); Assert.That(combatRenderer, Is.Not.Null); var shotDeadline = Time.realtimeSinceStartup + 20f; while (Time.realtimeSinceStartup < shotDeadline && @@ -823,7 +828,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Convert.ToSingle(fieldViewport.GetType().GetProperty("MaxX")?.GetValue(fieldViewport)), Convert.ToSingle(fieldViewport.GetType().GetProperty("MaxY")?.GetValue(fieldViewport)), Convert.ToInt32(fieldSplats.GetType().GetProperty("Count")?.GetValue(fieldSplats)), - playerRendererFacts); + playerRendererFacts, + root.GetComponentsInChildren(includeInactive: true) + .Select(marker => PresentedEntityFact.From(camera, marker)) + .ToArray()); } finally { @@ -893,7 +901,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float fieldViewportMaxX, float fieldViewportMaxY, int fieldSplatCount, - RendererFact[] playerRendererFacts) + RendererFact[] playerRendererFacts, + PresentedEntityFact[] presentedEntityFacts) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); if (string.IsNullOrWhiteSpace(path)) return; @@ -961,6 +970,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests fieldViewportMaxY = fieldViewportMaxY, fieldSplatCount = fieldSplatCount, playerRenderers = playerRendererFacts, + presentedEntities = presentedEntityFacts, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, @@ -1034,6 +1044,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float fieldViewportMaxY; public int fieldSplatCount; public RendererFact[] playerRenderers; + public PresentedEntityFact[] presentedEntities; public int mapChannelRendererCount; public int mapChangedPixels; public bool pilotCameraExcludesMapChannel; @@ -1110,6 +1121,45 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } } + [Serializable] + private sealed class PresentedEntityFact + { + public string entityId; + public string entityKind; + public string label; + public string assetRef; + public string worldPosition; + public string worldScale; + public int rendererCount; + public int enabledRendererCount; + public bool intersectsPilotFrustum; + public float viewportWidth; + public float viewportHeight; + + public static PresentedEntityFact From(Camera camera, EveUnityPlayableWorldEntityMarker marker) + { + var renderers = marker.GetComponentsInChildren(includeInactive: true); + MeasureViewportCoverage(camera, renderers, out var width, out var height); + var planes = GeometryUtility.CalculateFrustumPlanes(camera); + return new PresentedEntityFact + { + entityId = marker.EntityId, + entityKind = marker.EntityKind, + label = marker.Label, + assetRef = marker.AssetRef, + worldPosition = FormatVector(marker.transform.position), + worldScale = FormatVector(marker.transform.lossyScale), + rendererCount = renderers.Length, + enabledRendererCount = renderers.Count(renderer => renderer.enabled && renderer.gameObject.activeInHierarchy), + intersectsPilotFrustum = renderers.Any(renderer => + renderer.enabled && renderer.gameObject.activeInHierarchy && + GeometryUtility.TestPlanesAABB(planes, renderer.bounds)), + viewportWidth = width, + viewportHeight = height + }; + } + } + [Serializable] private sealed class MaterialFact { diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 99b21ab..d43bd7a 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -5,8 +5,8 @@ The released-package witness passes in separate cold-lowering and warm-gameplay profiles with: -- `org.gamecult.eve.unity-scene` `0.3.50`, commit - `7c8442e12264806a53a9e0e08b7b090dcddb8c90`; +- `org.gamecult.eve.unity-scene` `0.3.51`, commit + `21ac39e7bdfca9d81a0b7d89834f7e9da60aaad6`; - `org.gamecult.eve.plugin-fields` `0.2.3`, commit `c5a4a75c1b727499b16c2dae1895f29e2a9f72f0`; - `org.gamecult.eve.surface` `0.2.2`, commit @@ -18,8 +18,9 @@ profiles with: - the generic `ReleaseConsumerProject` client connected directly to the Aetheria daemon. -Evidence is in `artifacts/aetheria-daemon-cultlib-1014-cold-final` and -`artifacts/aetheria-daemon-cultlib-1014-warm-final`. The warm PlayMode test +Current celestial-package evidence is in +`artifacts/aetheria-daemon-celestial-cold`; the retained cold and warm witness +documents distinguish transfer from gameplay. The warm PlayMode test passed and recorded provider-owned reconciled movement, look, targeting, tractor press, tractor release, contact-gated cargo collection, and action receipts. The daemon-owned look direction reached the SoA body rotation, @@ -32,7 +33,7 @@ direction, color, and intensity; the generic camera rig lowers that contract to the only live directional light (`0.75` intensity). Aetheria also advertises the original undocked `ARPG.unity` Third Person Rig: entity-forward perspective follow, `30` unit distance, `60` degree vertical -field of view, target screen position `0.64,0.81`, zero position damping, and +field of view, canonical bottom-origin target screen position `0.64,0.19`, zero position damping, and `1`-`4096` clip planes. Its generic `aim.convergence-point.v1` relation makes the optical axis reach the daemon-published aim point while preserving that entity framing. The generic camera rig lowers both constraints without @@ -66,11 +67,11 @@ daemon lock progress `1.0`; Unity does not manufacture or smooth that value. Camera-channel facts: - provider-authored player renderers: `13`, including the lowered tractor effect; -- pilot changed pixels: `196,179`; -- pilot average luminance: `0.0993295`; -- pilot bright pixels: `1,348`; +- pilot changed pixels: `225,548`; +- pilot average luminance: `0.119513`; +- pilot bright pixels: `97,617`; - map-channel renderers: `10`; -- map changed pixels: `4,627`; +- map changed pixels: `4,723`; - native cockpit progress bars: `7`; - daemon tractor power at capture: `1.0` after the held input completed its authored ramp; @@ -86,11 +87,22 @@ Camera-channel facts: pixels; - the map camera renders exactly the advertised map layer. +The daemon also publishes the current zone's celestial presentation through the +same generic entity SoA generation as ordinary world entities. The current warm +witness records eleven sun/planet/gas-giant rows and eighteen asteroid rows. +Every row resolved a provider asset with enabled native renderers, and seven +celestial instances intersected the pilot frustum. These identities are +presentation-only and cannot receive gameplay commands. EveUnity contains no +Aetheria or celestial branch; release `0.3.51` fixes the generic prefab wrapper +so semantic instance scale composes with, rather than overwrites, provider-local +scale. + Visual inspection confirms that map glyphs are absent from the pilot frame and present in the map-only frame. This is transport, authority, field-production, -and lowering proof, not visual parity: the current pilot capture is nearly black -with sparse clipped geometry instead of the fossil's bright blue gravity-shaped -fog sea. Release `0.3.50` includes generic lowering for Eve Fields `0.2.3`'s +and lowering proof, not visual parity: the current pilot capture contains the +blue fog field and provider geometry, but nearby ships remain too dark and the +celestial composition lacks the fossil capture's readable close body. Release +`0.3.51` includes generic lowering for Eve Fields `0.2.3`'s `AnimatedRadialCosine` source alongside `PowerPulse`, dither-scale, temporal-history, and finite-look-at semantics. Aetheria publishes the fossil global animated simplex/cellular/ambient producer stack, body-owned radial wave @@ -146,13 +158,14 @@ Primary artifacts: ## Cold delivery proof The `cold-start-lowering` profile starts with zero bodies and zero partials. It -receives the `46,412,384` byte Unity bundle through the managed +receives the `55,529,775` byte Unity bundle through the managed `cultmesh.content.v1` session, verifies SHA-256 -`f5960f240ecfd56de417e8e51fde34cfb962fc6fcf0840cc373190e7dbc6048f`, +`5cb34be9b29f533ddfea89a439f43170c3691538b2dba9578d1f96ff91cc0b34`, atomically promotes one `.body`, and leaves zero partials under the unchanged -300-second deadline. The full witness took `84.991` seconds; the Unity test took -`43.212` seconds. It then lowered provider assets, movement, environment, four -field layers across `127` composites, and both camera channels. The pilot camera +300-second deadline. Including a clean provider-bundle build, the full witness +took `263.477` seconds; the Unity test took `48.083` seconds. It then lowered +provider assets, movement, environment, four field layers across `125` +composites, and both camera channels. The pilot camera excluded map objects and the map camera included them. Cold asset acquisition and transient gameplay are deliberately separate proof diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 967fe85..c86a5f9 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "7c8442e12264806a53a9e0e08b7b090dcddb8c90" +$expectedEveUnityCommit = "21ac39e7bdfca9d81a0b7d89834f7e9da60aaad6" $expectedEveFieldsCommit = "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "4b7162022a8976f7941b5a7a69acf50f1b6d532b" @@ -246,6 +246,21 @@ try { [long]$facts.fieldVolumeCompositeCount -le 0) { throw "Live witness did not prove provider assets, movement, Fields lowering, and camera-channel separation." } + $presentedCelestials = @($facts.presentedEntities | Where-Object { $_.entityKind -like "celestial.*" }) + $presentedBodies = @($presentedCelestials | Where-Object { + $_.entityKind -in @("celestial.sun", "celestial.planet", "celestial.gas-giant") + }) + $presentedAsteroids = @($presentedCelestials | Where-Object { $_.entityKind -eq "celestial.asteroid" }) + $invalidCelestials = @($presentedCelestials | Where-Object { + [string]::IsNullOrWhiteSpace($_.assetRef) -or + [int]$_.rendererCount -le 0 -or + [int]$_.enabledRendererCount -le 0 + }) + if ($presentedBodies.Count -le 0 -or $presentedAsteroids.Count -le 0 -or + $invalidCelestials.Count -ne 0 -or + @($presentedCelestials | Where-Object { $_.intersectsPilotFrustum }).Count -le 0) { + throw "Live witness did not prove provider-owned celestial bodies and asteroids reached the generic scene and intersected the pilot frustum. bodies=$($presentedBodies.Count) asteroids=$($presentedAsteroids.Count) invalid=$($invalidCelestials.Count)" + } if ($witnessProfile -eq "full-session-gameplay" -and (-not $facts.combatPresentation -or [string]::IsNullOrWhiteSpace($facts.shotId) -or [double]$facts.lockProgress -le 0.99 -or -not $facts.pickupCollection -or From b4266c3215d1634d5a6ab499682569343d8c7c28 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 11:35:36 +0100 Subject: [PATCH 117/292] Record native Aetheria material witness --- docs/aetheria-released-package-witness.md | 41 ++++++++++++++--------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index d43bd7a..de9bc87 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -28,9 +28,11 @@ and the generic aim presentation rendered at the advertised 50-unit minimum convergence distance. The advertised skybox material and reflection cubemap resolved from the provider bundle as their required native types and became the active camera rig's leased Unity environment. -The witness supplies no client-authored light. Aetheria advertises the key-light -direction, color, and intensity; the generic camera rig lowers that contract to -the only live directional light (`0.75` intensity). +The witness supplies no client-authored light. The fossil `ARPG.unity` scene has +no Unity `Light` component, so Aetheria advertises its skybox ambient intensity +and custom reflection but no key light. The generic camera rig records zero live +directional intensity. The earlier invented `0.75` light has been deleted rather +than retained as a material-conversion compensator. Aetheria also advertises the original undocked `ARPG.unity` Third Person Rig: entity-forward perspective follow, `30` unit distance, `60` degree vertical field of view, canonical bottom-origin target screen position `0.64,0.19`, zero position damping, and @@ -53,7 +55,7 @@ Fields splat layers. Its surface contract contains logical ports such as names or pass indices. The selected `unity-scene` provider asset variant owns that concrete shader ABI through `unity.volume.*` metadata. The released generic lowerer resolved the provider shader and dither texture, rasterized all -four layers, and composited `91` pilot-camera frames through daemon frame `643`. + four layers, and composited `121` pilot-camera frames through daemon frame `397`. The provider variant advertises the fossil shader's raymarch, temporal-history, and composite passes plus their logical ports. EveUnity allocates and resets the history targets generically; a partial temporal ABI fails closed. @@ -66,12 +68,12 @@ daemon lock progress `1.0`; Unity does not manufacture or smooth that value. Camera-channel facts: -- provider-authored player renderers: `13`, including the lowered tractor effect; -- pilot changed pixels: `225,548`; -- pilot average luminance: `0.119513`; -- pilot bright pixels: `97,617`; +- provider-authored player renderers: `12`, with no embedded shield renderer; +- pilot changed pixels: `220,747`; +- pilot average luminance: `0.134114`; +- pilot bright pixels: `113,308`; - map-channel renderers: `10`; -- map changed pixels: `4,723`; +- map changed pixels: `4,857`; - native cockpit progress bars: `7`; - daemon tractor power at capture: `1.0` after the held input completed its authored ramp; @@ -97,6 +99,15 @@ Aetheria or celestial branch; release `0.3.51` fixes the generic prefab wrapper so semantic instance scale composes with, rather than overwrites, provider-local scale. +Provider prefab construction now removes the fossil shield renderer embedded in +each ship. That renderer's custom shader performs collision-local dither clipping; +flattening it to ordinary opaque URP Lit had produced the large black ellipsoids +previously mistaken for celestial bodies. Shield state remains daemon truth and +shield impacts use the separately advertised receipt-driven effect. Material +conversion also preserves source lighting models: the map icon's unlit source +now becomes `Universal Render Pipeline/Unlit`, so map glyphs remain readable +without resurrecting the false scene light. + Visual inspection confirms that map glyphs are absent from the pilot frame and present in the map-only frame. This is transport, authority, field-production, and lowering proof, not visual parity: the current pilot capture contains the @@ -108,7 +119,7 @@ temporal-history, and finite-look-at semantics. Aetheria publishes the fossil global animated simplex/cellular/ambient producer stack, body-owned radial wave frequency, separate negative `gravity.height` and positive `fog.surface_height`, and the authored `ARPG.unity` zone-brush extent. The -witness records player Y `-76.7574` and camera Y `-94.3347`; Unity does not + witness records player Y `-83.7296` and camera Y `-66.9630`; Unity does not sample terrain to invent either value. The four rasterized field layers and HDR raymarch/history targets are populated, so the remaining failure is in native presentation/composition or provider art calibration rather than missing state @@ -120,7 +131,7 @@ Aetheria currently bundles ambientCG's 1K `Metal012` color, normal, and metalness maps under CC0. The same pre-generated maps now replace dead Substance archive sub-assets on the brushed aluminium, tinted car paint, cockpit, steel, black-metal, and radiator materials. The released witness -resolves textured native URP materials in `10` distinct player-material slots, + resolves textured native URP materials in `9` distinct player-material slots, up from `3`, without adding another texture payload. This is temporary provider art with recorded provenance, not a replacement texture pipeline. @@ -158,13 +169,13 @@ Primary artifacts: ## Cold delivery proof The `cold-start-lowering` profile starts with zero bodies and zero partials. It -receives the `55,529,775` byte Unity bundle through the managed +receives the `55,533,143` byte Unity bundle through the managed `cultmesh.content.v1` session, verifies SHA-256 -`5cb34be9b29f533ddfea89a439f43170c3691538b2dba9578d1f96ff91cc0b34`, +`6d7352c47bde37ca4235ed8a96e2b4d4df9a1c5a2e84af504c7a0662f7b7d2ba`, atomically promotes one `.body`, and leaves zero partials under the unchanged 300-second deadline. Including a clean provider-bundle build, the full witness -took `263.477` seconds; the Unity test took `48.083` seconds. It then lowered -provider assets, movement, environment, four field layers across `125` +took `147.615` seconds; the Unity test took `48.710` seconds. It then lowered +provider assets, movement, environment, four field layers across `135` composites, and both camera channels. The pilot camera excluded map objects and the map camera included them. From 4fc8296154f3c695b618dc41a8790de4f4d6de2b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 12:23:00 +0100 Subject: [PATCH 118/292] Lower provider-owned stateless field particles --- .../PlayMode/GenericWorldCaptureTests.cs | 39 + .../PlayMode/GenericWorldCaptureTests.cs | 39 + .../EveUnityPlayableWorldClientHost.cs | 3 + .../Runtime/EveUnitySceneSurfaceLowerer.cs | 50 +- .../Fields/EveUnityFieldsParticleRenderer.cs | 680 ++++++++++++++++++ .../EveUnityFieldsParticleRenderer.cs.meta | 2 + .../EveFieldsUnitySceneProjectionTests.cs | 90 +++ .../EveUnitySceneSurfaceLowererTests.cs | 22 + scripts/run-aetheria-daemon-world-witness.ps1 | 14 +- 9 files changed, 935 insertions(+), 4 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs.meta diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index acc016a..0e7e3b1 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -728,7 +728,13 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests mapCamera.transform.position = mapBounds.center + Vector3.up * (mapCamera.farClipPlane * 0.25f); mapCamera.transform.rotation = Quaternion.Euler(90f, 0f, 0f); mapCamera.targetTexture = target; + var fieldParticlesForMap = root.GetComponent(); + var particleDrawsBeforeMap = fieldParticlesForMap?.DrawCount ?? 0; mapCamera.Render(); + var particleMapCameraIsolated = fieldParticlesForMap != null && + fieldParticlesForMap.DrawCount == particleDrawsBeforeMap; + Assert.That(particleMapCameraIsolated, Is.True, + "The map camera executed the pilot-only field-particle pass."); RenderTexture.active = target; pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); pixels.Apply(); @@ -768,6 +774,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests "The generic volume lowerer did not rasterize any advertised Fields layers."); Assert.That(fieldVolume.CompositeCount, Is.GreaterThan(0), "The generic volume lowerer never composited into the pilot camera."); + var fieldParticles = fieldParticlesForMap; + Assert.That(fieldParticles, Is.Not.Null, + "The generic client did not install its field-particle lowerer."); + Assert.That(fieldParticles.ProgramReady, Is.True, + "The generic field-particle lowerer rejected the advertised provider ABI or assets."); + Assert.That(fieldParticles.PresentedFrameId, Is.GreaterThanOrEqualTo(0)); + Assert.That(fieldParticles.ParticleCount, Is.EqualTo(65536)); + Assert.That(fieldParticles.DispatchCount, Is.GreaterThan(0)); + Assert.That(fieldParticles.DrawCount, Is.GreaterThan(0)); var fieldDocument = typeof(EveUnityFieldsVolumeRenderer) .GetField("_document", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) ?.GetValue(fieldVolume); @@ -791,6 +806,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests fieldVolume.PresentedFrameId, fieldVolume.PresentedLayerCount, fieldVolume.CompositeCount, + fieldParticles.PresentedFrameId, + fieldParticles.ParticleCount, + fieldParticles.DispatchCount, + fieldParticles.DrawCount, + particleMapCameraIsolated, + fieldParticles.LastGridCenter, movementReceipt, lookReceipt, focusReceipt, @@ -864,6 +885,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests long fieldVolumeFrameId, int fieldVolumeLayerCount, long fieldVolumeCompositeCount, + long fieldParticleFrameId, + int fieldParticleCount, + int fieldParticleDispatchCount, + int fieldParticleDrawCount, + bool fieldParticleMapCameraIsolated, + Vector2 fieldParticleGridCenter, WitnessReceipt movement, WitnessReceipt look, WitnessReceipt targeting, @@ -915,6 +942,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests fieldVolumeFrameId = fieldVolumeFrameId, fieldVolumeLayerCount = fieldVolumeLayerCount, fieldVolumeCompositeCount = fieldVolumeCompositeCount, + fieldParticleFrameId = fieldParticleFrameId, + fieldParticleCount = fieldParticleCount, + fieldParticleDispatchCount = fieldParticleDispatchCount, + fieldParticleDrawCount = fieldParticleDrawCount, + fieldParticleMapCameraIsolated = fieldParticleMapCameraIsolated, + fieldParticleGridCenter = fieldParticleGridCenter, movement = movement != null, aimPresentation = look != null && aimDotDistance >= 49.9f, targeting = targeting != null, @@ -991,6 +1024,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public long fieldVolumeFrameId; public int fieldVolumeLayerCount; public long fieldVolumeCompositeCount; + public long fieldParticleFrameId; + public int fieldParticleCount; + public int fieldParticleDispatchCount; + public int fieldParticleDrawCount; + public bool fieldParticleMapCameraIsolated; + public Vector2 fieldParticleGridCenter; public bool movement; public bool aimPresentation; public bool targeting; diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index acc016a..0e7e3b1 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -728,7 +728,13 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests mapCamera.transform.position = mapBounds.center + Vector3.up * (mapCamera.farClipPlane * 0.25f); mapCamera.transform.rotation = Quaternion.Euler(90f, 0f, 0f); mapCamera.targetTexture = target; + var fieldParticlesForMap = root.GetComponent(); + var particleDrawsBeforeMap = fieldParticlesForMap?.DrawCount ?? 0; mapCamera.Render(); + var particleMapCameraIsolated = fieldParticlesForMap != null && + fieldParticlesForMap.DrawCount == particleDrawsBeforeMap; + Assert.That(particleMapCameraIsolated, Is.True, + "The map camera executed the pilot-only field-particle pass."); RenderTexture.active = target; pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); pixels.Apply(); @@ -768,6 +774,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests "The generic volume lowerer did not rasterize any advertised Fields layers."); Assert.That(fieldVolume.CompositeCount, Is.GreaterThan(0), "The generic volume lowerer never composited into the pilot camera."); + var fieldParticles = fieldParticlesForMap; + Assert.That(fieldParticles, Is.Not.Null, + "The generic client did not install its field-particle lowerer."); + Assert.That(fieldParticles.ProgramReady, Is.True, + "The generic field-particle lowerer rejected the advertised provider ABI or assets."); + Assert.That(fieldParticles.PresentedFrameId, Is.GreaterThanOrEqualTo(0)); + Assert.That(fieldParticles.ParticleCount, Is.EqualTo(65536)); + Assert.That(fieldParticles.DispatchCount, Is.GreaterThan(0)); + Assert.That(fieldParticles.DrawCount, Is.GreaterThan(0)); var fieldDocument = typeof(EveUnityFieldsVolumeRenderer) .GetField("_document", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) ?.GetValue(fieldVolume); @@ -791,6 +806,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests fieldVolume.PresentedFrameId, fieldVolume.PresentedLayerCount, fieldVolume.CompositeCount, + fieldParticles.PresentedFrameId, + fieldParticles.ParticleCount, + fieldParticles.DispatchCount, + fieldParticles.DrawCount, + particleMapCameraIsolated, + fieldParticles.LastGridCenter, movementReceipt, lookReceipt, focusReceipt, @@ -864,6 +885,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests long fieldVolumeFrameId, int fieldVolumeLayerCount, long fieldVolumeCompositeCount, + long fieldParticleFrameId, + int fieldParticleCount, + int fieldParticleDispatchCount, + int fieldParticleDrawCount, + bool fieldParticleMapCameraIsolated, + Vector2 fieldParticleGridCenter, WitnessReceipt movement, WitnessReceipt look, WitnessReceipt targeting, @@ -915,6 +942,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests fieldVolumeFrameId = fieldVolumeFrameId, fieldVolumeLayerCount = fieldVolumeLayerCount, fieldVolumeCompositeCount = fieldVolumeCompositeCount, + fieldParticleFrameId = fieldParticleFrameId, + fieldParticleCount = fieldParticleCount, + fieldParticleDispatchCount = fieldParticleDispatchCount, + fieldParticleDrawCount = fieldParticleDrawCount, + fieldParticleMapCameraIsolated = fieldParticleMapCameraIsolated, + fieldParticleGridCenter = fieldParticleGridCenter, movement = movement != null, aimPresentation = look != null && aimDotDistance >= 49.9f, targeting = targeting != null, @@ -991,6 +1024,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public long fieldVolumeFrameId; public int fieldVolumeLayerCount; public long fieldVolumeCompositeCount; + public long fieldParticleFrameId; + public int fieldParticleCount; + public int fieldParticleDispatchCount; + public int fieldParticleDrawCount; + public bool fieldParticleMapCameraIsolated; + public Vector2 fieldParticleGridCenter; public bool movement; public bool aimPresentation; public bool targeting; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs index f1e0ee2..21c5694 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs @@ -115,6 +115,9 @@ namespace GameCult.Eve.UnityScene var fieldVolume = GetComponent(); if (fieldVolume == null) fieldVolume = gameObject.AddComponent(); fieldVolume.Bind(this, providerSurfaceDocuments as IEveUnityFieldsSplatsDocumentSource); + var fieldParticles = GetComponent(); + if (fieldParticles == null) fieldParticles = gameObject.AddComponent(); + fieldParticles.Bind(this, providerSurfaceDocuments as IEveUnityFieldsSplatsDocumentSource); var presentation = Runtime.Connect(); ConnectionEpoch++; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index 462336f..726944c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -105,12 +105,15 @@ namespace GameCult.Eve.UnityScene var entities = new List(); var fieldVolumes = new List(); + var fieldParticles = new List(); foreach (var component in Flatten(worldRoot)) { if (string.Equals(component.Kind, "world.entity3d", StringComparison.Ordinal)) entities.Add(BuildPlayableEntity(component)); else if (string.Equals(component.Kind, "field.volume3d", StringComparison.Ordinal)) fieldVolumes.Add(BuildFieldVolume(component)); + else if (string.Equals(component.Kind, "field.particles3d", StringComparison.Ordinal)) + fieldParticles.Add(BuildFieldParticles(component)); } return new EveUnityPlayableWorldProjection( @@ -152,7 +155,8 @@ namespace GameCult.Eve.UnityScene ParseVector3(worldRoot.GetProp("keyLightColor")), ParseFloat(worldRoot.GetProp("keyLightIntensity"), 0f), fieldVolumes, - worldRoot.GetProp("cameraLookAt")); + worldRoot.GetProp("cameraLookAt"), + fieldParticles); } private static EveUnityFieldVolumeProjection BuildFieldVolume(EveSurfaceComponent component) => @@ -166,6 +170,16 @@ namespace GameCult.Eve.UnityScene component.GetProp("quality", "normal"), component.Props); + private static EveUnityFieldParticlesProjection BuildFieldParticles(EveSurfaceComponent component) => + new EveUnityFieldParticlesProjection( + component.Id, + component.GetProp("documentRef"), + component.GetProp("documentSchema", EveFieldsSchemas.Splats), + component.GetProp("computeProgramAssetRef"), + component.GetProp("materialAssetRef"), + component.GetProp("renderChannel", "world.transparent"), + component.Props); + private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component) { var position = ParseVector3(component.GetProp("position")); @@ -396,7 +410,8 @@ namespace GameCult.Eve.UnityScene (float r, float g, float b) keyLightColor = default, float keyLightIntensity = 0f, IReadOnlyList? fieldVolumes = null, - string cameraLookAt = "") + string cameraLookAt = "", + IReadOnlyList? fieldParticles = null) { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -442,6 +457,7 @@ namespace GameCult.Eve.UnityScene KeyLightColorB = keyLightColor.b; KeyLightIntensity = keyLightIntensity; FieldVolumes = fieldVolumes ?? Array.Empty(); + FieldParticles = fieldParticles ?? Array.Empty(); CameraLookAt = cameraLookAt ?? ""; } @@ -536,6 +552,7 @@ namespace GameCult.Eve.UnityScene public IReadOnlyList Entities { get; } public IReadOnlyList FieldVolumes { get; } + public IReadOnlyList FieldParticles { get; } } public sealed class EveUnityFieldVolumeProjection @@ -570,6 +587,35 @@ namespace GameCult.Eve.UnityScene public IReadOnlyDictionary Props { get; } } + public sealed class EveUnityFieldParticlesProjection + { + public EveUnityFieldParticlesProjection( + string nodeId, + string documentRef, + string documentSchema, + string computeProgramAssetRef, + string materialAssetRef, + string renderChannel, + IReadOnlyDictionary? props = null) + { + NodeId = nodeId ?? ""; + DocumentRef = documentRef ?? ""; + DocumentSchema = documentSchema ?? ""; + ComputeProgramAssetRef = computeProgramAssetRef ?? ""; + MaterialAssetRef = materialAssetRef ?? ""; + RenderChannel = renderChannel ?? ""; + Props = props ?? new Dictionary(StringComparer.Ordinal); + } + + public string NodeId { get; } + public string DocumentRef { get; } + public string DocumentSchema { get; } + public string ComputeProgramAssetRef { get; } + public string MaterialAssetRef { get; } + public string RenderChannel { get; } + public IReadOnlyDictionary Props { get; } + } + public sealed class EveUnityPlayableWorldEntity { public EveUnityPlayableWorldEntity( diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs new file mode 100644 index 0000000..6e6eb4e --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs @@ -0,0 +1,680 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using GameCult.Eve.PluginFields; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.Rendering.RenderGraphModule; +using UnityEngine.Rendering.Universal; + +#nullable enable + +namespace GameCult.Eve.UnityScene.Fields +{ + public sealed class EveUnityFieldsParticleRenderer : MonoBehaviour + { + private EveUnityPlayableWorldClientHost? _host; + private IEveUnityFieldsSplatsDocumentSource? _source; + private EveFieldsSplatsDocument? _document; + private EveFieldsSplatLayerRenderer? _layers; + private GameObject? _layerRoot; + private readonly List _targets = new List(); + private ComputeShader? _computeSource; + private ComputeShader? _compute; + private Material? _materialSource; + private Material? _material; + private IReadOnlyDictionary? _computeMetadata; + private IReadOnlyDictionary? _materialMetadata; + private readonly Dictionary _assetTextures = new Dictionary(StringComparer.Ordinal); + private ComputeBuffer? _particles; + private ComputeBuffer? _quadPoints; + private EveUnityFieldsParticlePass? _renderPass; + private string _activeNodeId = ""; + private long _rasterizedFrameId = -1; + private Vector2 _rasterizedCenter = new Vector2(float.NaN, float.NaN); + + public long PresentedFrameId { get; private set; } = -1; + public int ParticleCount { get; private set; } + public int DispatchCount { get; private set; } + public int DrawCount { get; private set; } + public Vector2 LastGridCenter { get; private set; } + public bool ProgramReady => _compute != null && _material != null && _particles != null && _quadPoints != null; + public static RenderPassEvent CompositionRenderPassEvent => RenderPassEvent.BeforeRenderingPostProcessing; + + public void Bind(EveUnityPlayableWorldClientHost host, IEveUnityFieldsSplatsDocumentSource? source) + { + if (host == null) throw new ArgumentNullException(nameof(host)); + if (_source != null) _source.FieldsSplatsAvailable -= OnFieldsSplatsAvailable; + _host = host; + _source = source; + if (_source != null) _source.FieldsSplatsAvailable += OnFieldsSplatsAvailable; + enabled = _source != null; + } + + private void OnEnable() => RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering; + + private void OnDisable() + { + RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering; + ReleaseState(); + } + + private void OnDestroy() + { + if (_source != null) _source.FieldsSplatsAvailable -= OnFieldsSplatsAvailable; + _source = null; + _host = null; + } + + private void OnFieldsSplatsAvailable(EveFieldsSplatsDocument document) + { + if (document == null || document.FrameId < PresentedFrameId) return; + _document = document; + PresentedFrameId = document.FrameId; + } + + private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera) + { + if (_host == null || _document == null || camera == null || + _host.ActiveCameraTransform != camera.transform) + return; + var field = ActiveField(); + if (field == null || !PrepareFrame(field, camera, _document)) return; + _renderPass ??= new EveUnityFieldsParticlePass(this); + camera.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(_renderPass); + } + + private EveUnityFieldParticlesProjection? ActiveField() => + _host?.ActiveWorld?.FieldParticles.FirstOrDefault(field => + string.Equals(field.DocumentSchema, EveFieldsSchemas.Splats, StringComparison.Ordinal)); + + private bool PrepareFrame( + EveUnityFieldParticlesProjection field, + Camera camera, + EveFieldsSplatsDocument document) + { + if (!EnsurePrograms(field) || !EnsureBuffers(field)) return false; + EnsureTargets(field); + EnsureLayerRenderer(); + if (_layers == null) return false; + + var baseWidth = PositiveInt(field, "textureWidth", 512); + var baseHeight = PositiveInt(field, "textureHeight", 512); + var viewportWidth = (float)(document.Viewport.MaxX - document.Viewport.MinX); + var viewportHeight = (float)(document.Viewport.MaxY - document.Viewport.MinY); + if (!float.IsFinite(viewportWidth) || !float.IsFinite(viewportHeight) || + viewportWidth <= 0f || viewportHeight <= 0f) + return false; + + var center = new Vector2( + (float)((document.Viewport.MinX + document.Viewport.MaxX) * 0.5), + (float)((document.Viewport.MinY + document.Viewport.MaxY) * 0.5)); + if (string.Equals(Prop(field, "viewportAnchor"), "active-camera.xz", StringComparison.Ordinal)) + { + var snapLayer = Prop(field, "viewportSnapLayer"); + var snapTarget = _targets.FirstOrDefault(target => + string.Equals(target.LayerKey, snapLayer, StringComparison.Ordinal)); + var snapWidth = snapTarget == null + ? 0 + : Mathf.Max(1, Mathf.RoundToInt(baseWidth * Mathf.Max(0.01f, snapTarget.WidthScale))); + var snapHeight = snapTarget == null + ? 0 + : Mathf.Max(1, Mathf.RoundToInt(baseHeight * Mathf.Max(0.01f, snapTarget.HeightScale))); + var snapTexels = PositiveInt(field, "viewportSnapTexels", 0); + var cellWorldSize = PositiveFloat(field, "cellWorldSize", 0f); + var span = PositiveInt(field, "span", 0); + if (!TryValidateSpatialLattice( + viewportWidth, + viewportHeight, + snapWidth, + snapHeight, + span, + cellWorldSize, + snapTexels)) return false; + center.x = SnapCameraCoordinate(camera.transform.position.x, viewportWidth, snapWidth, snapTexels); + center.y = SnapCameraCoordinate(camera.transform.position.z, viewportHeight, snapHeight, snapTexels); + } + + if (_rasterizedFrameId != document.FrameId || _rasterizedCenter != center) + { + var projected = ProjectViewport(document, center, viewportWidth, viewportHeight); + _layers.Render(projected, _targets, baseWidth, baseHeight); + _rasterizedFrameId = document.FrameId; + _rasterizedCenter = center; + } + + ApplyProgramBindings(field, document, center, viewportWidth, viewportHeight); + var kernel = ProgramKernel("update"); + if (kernel < 0) return false; + var threadGroupSize = PositiveInt(field, "threadGroupSize", 1); + var groups = Mathf.CeilToInt((float)ParticleCount / threadGroupSize); + if (groups <= 0) return false; + _compute!.Dispatch(kernel, groups, 1, 1); + DispatchCount++; + LastGridCenter = center; + return true; + } + + public static float SnapCameraCoordinate( + float coordinate, + float viewportWidth, + int snapTargetWidth, + float snapTexels) + { + if (!float.IsFinite(coordinate) || !float.IsFinite(viewportWidth) || + !float.IsFinite(snapTexels) || viewportWidth <= 0f || + snapTargetWidth <= 0 || snapTexels <= 0f) + throw new ArgumentOutOfRangeException(nameof(viewportWidth)); + var snapLength = viewportWidth * snapTexels / snapTargetWidth; + var cell = (int)(coordinate / snapLength); + return cell * snapLength; + } + + public static bool TryValidateSpatialLattice( + float viewportWidth, + float viewportHeight, + int gravityTextureWidth, + int gravityTextureHeight, + int span, + float cellWorldSize, + int gravityTexelsPerCell) + { + if (!float.IsFinite(viewportWidth) || !float.IsFinite(viewportHeight) || + !float.IsFinite(cellWorldSize) || viewportWidth <= 0f || viewportHeight <= 0f || + gravityTextureWidth <= 0 || gravityTextureHeight <= 0 || span <= 0 || + cellWorldSize <= 0f || gravityTexelsPerCell <= 0) + return false; + var expectedExtent = span * cellWorldSize; + var expectedCellFromX = viewportWidth * gravityTexelsPerCell / gravityTextureWidth; + var expectedCellFromY = viewportHeight * gravityTexelsPerCell / gravityTextureHeight; + const float tolerance = 0.0001f; + return Mathf.Abs(viewportWidth - expectedExtent) <= tolerance && + Mathf.Abs(viewportHeight - expectedExtent) <= tolerance && + Mathf.Abs(expectedCellFromX - cellWorldSize) <= tolerance && + Mathf.Abs(expectedCellFromY - cellWorldSize) <= tolerance; + } + + private static EveFieldsSplatsDocument ProjectViewport( + EveFieldsSplatsDocument source, + Vector2 center, + float width, + float height) => + new EveFieldsSplatsDocument + { + Schema = source.Schema, + FrameId = source.FrameId, + PublishedAtUtc = source.PublishedAtUtc, + SimulationTimeSeconds = source.SimulationTimeSeconds, + RunId = source.RunId, + ZoneIndex = source.ZoneIndex, + ZoneName = source.ZoneName, + Viewport = new EveFieldsViewport + { + MinX = center.x - width * 0.5, + MinY = center.y - height * 0.5, + MaxX = center.x + width * 0.5, + MaxY = center.y + height * 0.5 + }, + Layers = source.Layers, + Splats = source.Splats + }; + + private bool EnsurePrograms(EveUnityFieldParticlesProjection field) + { + var provider = _host?.NativeAssetProvider; + if (provider == null || provider is not IEveUnityNativeAssetMetadataProvider metadataProvider || + string.IsNullOrWhiteSpace(field.ComputeProgramAssetRef) || + string.IsNullOrWhiteSpace(field.MaterialAssetRef)) + return false; + + var computeBinding = new EveUnityPlayableWorldAssetBinding( + field.ComputeProgramAssetRef, "", "provider-asset-ref"); + var materialBinding = new EveUnityPlayableWorldAssetBinding( + field.MaterialAssetRef, "", "provider-asset-ref"); + var compute = provider.ResolveAsset(computeBinding, typeof(ComputeShader)) as ComputeShader; + var material = provider.ResolveAsset(materialBinding, typeof(Material)) as Material; + if (compute == null || material == null || material.shader == null || !material.shader.isSupported || + !metadataProvider.TryResolveAssetMetadata(computeBinding, out var computeMetadata) || + !metadataProvider.TryResolveAssetMetadata(materialBinding, out var materialMetadata) || + !TryValidateProgramMetadata(computeMetadata, materialMetadata, out _) || + !TryValidateAdvertisedContract(field, compute, material, computeMetadata, materialMetadata)) + return false; + + var resolvedTextures = new Dictionary(StringComparer.Ordinal); + foreach (var binding in ParseBindings(Prop(field, "assetTextureBindings"))) + { + var texture = provider.ResolveAsset( + new EveUnityPlayableWorldAssetBinding(binding.Key, "", "provider-asset-ref"), + typeof(Texture)) as Texture; + if (texture == null) return false; + resolvedTextures[binding.Value] = texture; + } + + if (ReferenceEquals(compute, _computeSource) && ReferenceEquals(material, _materialSource) && + string.Equals(field.NodeId, _activeNodeId, StringComparison.Ordinal) && ProgramReady && + resolvedTextures.Count == _assetTextures.Count && resolvedTextures.All(binding => + _assetTextures.TryGetValue(binding.Key, out var current) && ReferenceEquals(current, binding.Value))) + return true; + + ReleaseProgramsAndBuffers(); + _computeSource = compute; + _materialSource = material; + _computeMetadata = computeMetadata; + _materialMetadata = materialMetadata; + _activeNodeId = field.NodeId; + _compute = Instantiate(compute); + _material = new Material(material) { hideFlags = HideFlags.DontSave }; + _assetTextures.Clear(); + foreach (var binding in resolvedTextures) _assetTextures[binding.Key] = binding.Value; + foreach (var feature in Prop(field, "features").Split(';')) + { + var logical = feature.Trim(); + if (!string.IsNullOrWhiteSpace(logical) && + TryComputeProgramValue($"unity.particles.feature.{logical}.keyword", out var keyword) && + !string.IsNullOrWhiteSpace(keyword)) + _compute.EnableKeyword(keyword); + } + return true; + } + + public static bool TryValidateProgramMetadata( + IReadOnlyDictionary? compute, + IReadOnlyDictionary? material, + out string error) + { + if (compute == null || material == null) + return Fail("Field-particle programs require compute and material metadata.", out error); + var computeKeys = new[] + { + "unity.particles.kernel.update", + "unity.particles.bufferPort.particles", + "unity.particles.texturePort.surfaceHeight", + "unity.particles.texturePort.tint", + "unity.particles.texturePort.hue", + "unity.particles.vectorPort.viewportTransform", + "unity.particles.vectorPort.timeVector", + "unity.particles.floatPort.time", + "unity.particles.floatPort.period", + "unity.particles.floatPort.spacing", + "unity.particles.intPort.span" + }; + if (computeKeys.Any(key => !compute.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value))) + return Fail("Field-particle compute metadata is missing a required logical port.", out error); + var materialKeys = new[] + { + "unity.particles.pass.render", + "unity.particles.bufferPort.particles", + "unity.particles.bufferPort.quadPoints" + }; + if (materialKeys.Any(key => !material.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value)) || + !int.TryParse(material["unity.particles.pass.render"], NumberStyles.Integer, + CultureInfo.InvariantCulture, out var pass) || pass < 0) + return Fail("Field-particle material metadata is missing a valid render pass or buffer port.", out error); + error = ""; + return true; + } + + private static bool TryValidateAdvertisedContract( + EveUnityFieldParticlesProjection field, + ComputeShader compute, + Material material, + IReadOnlyDictionary computeMetadata, + IReadOnlyDictionary materialMetadata) + { + var kernelName = computeMetadata["unity.particles.kernel.update"]; + if (!compute.HasKernel(kernelName)) return false; + var kernel = compute.FindKernel(kernelName); + compute.GetKernelThreadGroupSizes(kernel, out var nativeX, out var nativeY, out var nativeZ); + if (!int.TryParse(Prop(field, "threadGroupSize"), NumberStyles.Integer, + CultureInfo.InvariantCulture, out var advertisedGroupSize) || + advertisedGroupSize <= 0 || nativeX != advertisedGroupSize || nativeY != 1 || nativeZ != 1) + return false; + + if (!int.TryParse(materialMetadata["unity.particles.pass.render"], NumberStyles.Integer, + CultureInfo.InvariantCulture, out var pass) || pass < 0 || pass >= material.passCount) + return false; + + foreach (var binding in ParseBindingPairs(Prop(field, "layerBindings"))) + if (!HasPort(computeMetadata, "texturePort", binding.Value)) return false; + foreach (var binding in ParseBindingPairs(Prop(field, "assetTextureBindings"))) + if (!HasPort(computeMetadata, "texturePort", binding.Value)) return false; + foreach (var parameter in ParseBindingPairs(Prop(field, "floatParameters"))) + if (!HasPort(computeMetadata, "floatPort", parameter.Key) || + !TryFloat(parameter.Value, out _)) return false; + var floatParameters = ParseBindings(Prop(field, "floatParameters")); + if (!floatParameters.TryGetValue("spacing", out var spacingValue) || + !TryFloat(spacingValue, out var spacing) || + !TryFloat(Prop(field, "cellWorldSize"), out var cellWorldSize) || + Mathf.Abs(spacing - cellWorldSize) > 0.0001f || + PositiveInt(field, "gravityTexelsPerCell", 0) != + PositiveInt(field, "viewportSnapTexels", 0)) return false; + foreach (var binding in ParseBindingPairs(Prop(field, "documentFloatBindings"))) + { + var pieces = binding.Value.Split(','); + if (pieces.Length != 3 || !HasPort(computeMetadata, "floatPort", pieces[0].Trim()) || + !TryFloat(pieces[1], out _) || !TryFloat(pieces[2], out _)) return false; + } + + var timeVectorPort = Prop(field, "documentTimeVectorPort"); + if (!string.IsNullOrWhiteSpace(timeVectorPort) && + !HasPort(computeMetadata, "vectorPort", timeVectorPort)) return false; + foreach (var feature in Prop(field, "features").Split(';')) + { + var logical = feature.Trim(); + if (!string.IsNullOrWhiteSpace(logical) && + (!computeMetadata.TryGetValue($"unity.particles.feature.{logical}.keyword", out var keyword) || + string.IsNullOrWhiteSpace(keyword))) return false; + } + + return PositiveInt(field, "span", 0) > 0 && + PositiveInt(field, "particleStrideBytes", 0) > 0 && + PositiveInt(field, "textureWidth", 0) > 0 && + PositiveInt(field, "textureHeight", 0) > 0; + } + + private static bool HasPort( + IReadOnlyDictionary metadata, + string kind, + string logical) => + metadata.TryGetValue($"unity.particles.{kind}.{logical}", out var native) && + !string.IsNullOrWhiteSpace(native); + + private bool EnsureBuffers(EveUnityFieldParticlesProjection field) + { + var span = PositiveInt(field, "span", 0); + var stride = PositiveInt(field, "particleStrideBytes", 0); + if (span <= 0 || span > 4096 || stride <= 0 || stride > 1024) return false; + var count64 = (long)span * span; + if (count64 <= 0 || count64 > 16_777_216) return false; + var count = (int)count64; + if (_particles != null && ParticleCount == count) return true; + + _particles?.Release(); + _quadPoints?.Release(); + _particles = new ComputeBuffer(count, stride, ComputeBufferType.Structured); + _quadPoints = new ComputeBuffer(6, sizeof(float) * 3, ComputeBufferType.Structured); + _quadPoints.SetData(new[] + { + new Vector3(-0.5f, 0.5f), new Vector3(0.5f, 0.5f), new Vector3(0.5f, -0.5f), + new Vector3(0.5f, -0.5f), new Vector3(-0.5f, -0.5f), new Vector3(-0.5f, 0.5f) + }); + ParticleCount = count; + return true; + } + + private void ApplyProgramBindings( + EveUnityFieldParticlesProjection field, + EveFieldsSplatsDocument document, + Vector2 center, + float viewportWidth, + float viewportHeight) + { + var kernel = ProgramKernel("update"); + if (kernel < 0) return; + SetComputeBuffer(kernel, "particles", _particles!); + SetMaterialBuffer("particles", _particles!); + SetMaterialBuffer("quadPoints", _quadPoints!); + + foreach (var binding in ParseBindings(Prop(field, "layerBindings"))) + { + var target = _targets.FirstOrDefault(value => string.Equals(value.LayerKey, binding.Key, StringComparison.Ordinal)); + if (target?.TargetTexture != null) SetComputeTexture(kernel, binding.Value, target.TargetTexture); + } + foreach (var binding in _assetTextures) + SetComputeTexture(kernel, binding.Key, binding.Value); + SetComputeVector("viewportTransform", new Vector4(center.x, center.y, viewportWidth, viewportHeight)); + + var time = (float)document.SimulationTimeSeconds; + var timeVectorPort = Prop(field, "documentTimeVectorPort"); + if (!string.IsNullOrWhiteSpace(timeVectorPort)) + SetComputeVector(timeVectorPort, new Vector4(time / 20f, time, time * 2f, time * 3f)); + if (TryEvaluateDocumentFloatBindings( + document, + Prop(field, "documentFloatBindings"), + out var documentFloats)) + foreach (var binding in documentFloats) + SetComputeFloat(binding.Key, binding.Value); + foreach (var parameter in ParseBindings(Prop(field, "floatParameters"))) + if (TryFloat(parameter.Value, out var value)) SetComputeFloat(parameter.Key, value); + SetComputeInt("span", PositiveInt(field, "span", 0)); + } + + private void RenderParticles( + UnsafeCommandBuffer commands, + Camera camera, + TextureHandle colorTarget, + TextureHandle depthTarget) + { + if (_host == null || _host.ActiveCameraTransform != camera.transform || !ProgramReady) return; + if (depthTarget.IsValid()) commands.SetRenderTarget(colorTarget, depthTarget); + else commands.SetRenderTarget(colorTarget); + commands.SetViewport(camera.pixelRect); + commands.DrawProcedural(Matrix4x4.identity, _material!, MaterialPass("render"), + MeshTopology.Triangles, 6, ParticleCount); + DrawCount++; + } + + private void EnsureTargets(EveUnityFieldParticlesProjection field) + { + var bindings = ParseBindings(Prop(field, "layerBindings")); + var descriptors = ParseBindings(Prop(field, "layerTargetDescriptors")); + if (string.Equals(_activeNodeId, field.NodeId, StringComparison.Ordinal) && + _targets.Count == bindings.Count && _targets.All(target => bindings.ContainsKey(target.LayerKey))) + return; + _targets.Clear(); + foreach (var binding in bindings) + { + var target = new EveFieldsSplatLayerTarget { LayerKey = binding.Key }; + if (descriptors.TryGetValue(binding.Key, out var descriptor)) + EveUnityFieldsVolumeRenderer.TryApplyLayerTargetDescriptor(target, descriptor); + _targets.Add(target); + } + } + + private void EnsureLayerRenderer() + { + if (_layers != null) return; + _layerRoot = new GameObject("Eve Fields Particle Layers") { hideFlags = HideFlags.DontSave }; + _layerRoot.transform.SetParent(transform, false); + _layers = _layerRoot.AddComponent(); + } + + private sealed class EveUnityFieldsParticlePass : ScriptableRenderPass + { + private readonly EveUnityFieldsParticleRenderer _owner; + private sealed class PassData + { + internal Camera Camera = null!; + internal EveUnityFieldsParticleRenderer Owner = null!; + internal TextureHandle ColorTarget; + internal TextureHandle DepthTarget; + } + + internal EveUnityFieldsParticlePass(EveUnityFieldsParticleRenderer owner) + { + _owner = owner; + renderPassEvent = CompositionRenderPassEvent; + ConfigureInput(ScriptableRenderPassInput.Depth); + } + + public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) + { + var cameraData = frameData.Get(); + if (cameraData.camera == null || _owner._host == null || + _owner._host.ActiveCameraTransform != cameraData.camera.transform) + return; + var resources = frameData.Get(); + if (!resources.activeColorTexture.IsValid()) return; + using var builder = renderGraph.AddUnsafePass("Eve Fields Particles", out var data); + data.Camera = cameraData.camera; + data.Owner = _owner; + data.ColorTarget = resources.activeColorTexture; + data.DepthTarget = resources.activeDepthTexture; + builder.UseTexture(data.ColorTarget, AccessFlags.ReadWrite); + if (data.DepthTarget.IsValid()) builder.UseTexture(data.DepthTarget, AccessFlags.Read); + builder.AllowPassCulling(false); + builder.SetRenderFunc(static (PassData pass, UnsafeGraphContext context) => + pass.Owner.RenderParticles(context.cmd, pass.Camera, pass.ColorTarget, pass.DepthTarget)); + } + } + + private int ProgramKernel(string logical) + { + if (_compute == null || !TryComputeProgramValue($"unity.particles.kernel.{logical}", out var name) || + string.IsNullOrWhiteSpace(name) || !_compute.HasKernel(name)) + return -1; + return _compute.FindKernel(name); + } + + private int MaterialPass(string logical) => + TryMaterialProgramValue($"unity.particles.pass.{logical}", out var value) && + int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var pass) && pass >= 0 + ? pass + : -1; + + private void SetComputeTexture(int kernel, string logical, Texture texture) + { + if (TryComputeProgramValue($"unity.particles.texturePort.{logical}", out var property)) + _compute!.SetTexture(kernel, property, texture); + } + + private void SetComputeBuffer(int kernel, string logical, ComputeBuffer buffer) + { + if (TryComputeProgramValue($"unity.particles.bufferPort.{logical}", out var property)) + _compute!.SetBuffer(kernel, property, buffer); + } + + private void SetMaterialBuffer(string logical, ComputeBuffer buffer) + { + if (TryMaterialProgramValue($"unity.particles.bufferPort.{logical}", out var property)) + _material!.SetBuffer(property, buffer); + } + + private void SetComputeFloat(string logical, float value) + { + if (TryComputeProgramValue($"unity.particles.floatPort.{logical}", out var property)) + _compute!.SetFloat(property, value); + } + + private void SetComputeInt(string logical, int value) + { + if (TryComputeProgramValue($"unity.particles.intPort.{logical}", out var property)) + _compute!.SetInt(property, value); + } + + private void SetComputeVector(string logical, Vector4 value) + { + if (TryComputeProgramValue($"unity.particles.vectorPort.{logical}", out var property)) + _compute!.SetVector(property, value); + } + + private bool TryComputeProgramValue(string key, out string value) + { + value = ""; + return _computeMetadata != null && _computeMetadata.TryGetValue(key, out value!); + } + + private bool TryMaterialProgramValue(string key, out string value) + { + value = ""; + return _materialMetadata != null && _materialMetadata.TryGetValue(key, out value!); + } + + private void ReleaseState() + { + ReleaseProgramsAndBuffers(); + if (_layerRoot != null) Destroy(_layerRoot); + _layerRoot = null; + _layers = null; + _targets.Clear(); + _document = null; + PresentedFrameId = -1; + DispatchCount = 0; + DrawCount = 0; + LastGridCenter = default; + _rasterizedFrameId = -1; + _rasterizedCenter = new Vector2(float.NaN, float.NaN); + } + + private void ReleaseProgramsAndBuffers() + { + _particles?.Release(); + _quadPoints?.Release(); + _particles = null; + _quadPoints = null; + ParticleCount = 0; + if (_compute != null) Destroy(_compute); + if (_material != null) Destroy(_material); + _compute = null; + _material = null; + _computeSource = null; + _materialSource = null; + _computeMetadata = null; + _materialMetadata = null; + _assetTextures.Clear(); + _activeNodeId = ""; + } + + private static bool Fail(string message, out string error) + { + error = message; + return false; + } + + private static Dictionary ParseBindings(string value) + { + var result = new Dictionary(StringComparer.Ordinal); + foreach (var pair in ParseBindingPairs(value)) result[pair.Key] = pair.Value; + return result; + } + + private static IReadOnlyList> ParseBindingPairs(string value) + { + var result = new List>(); + foreach (var pair in (value ?? "").Split(';')) + { + var separator = pair.IndexOf('='); + if (separator <= 0 || separator >= pair.Length - 1) continue; + result.Add(new KeyValuePair( + pair.Substring(0, separator).Trim(), + pair.Substring(separator + 1).Trim())); + } + return result; + } + + public static bool TryEvaluateDocumentFloatBindings( + EveFieldsSplatsDocument document, + string bindings, + out IReadOnlyList> values) + { + var result = new List>(); + foreach (var binding in ParseBindingPairs(bindings)) + { + if (!EveUnityFieldsVolumeRenderer.TryEvaluateDocumentFloatBinding( + document, binding.Key, binding.Value, out var port, out var value)) + { + values = Array.Empty>(); + return false; + } + result.Add(new KeyValuePair(port, value)); + } + values = result; + return true; + } + + private static string Prop(EveUnityFieldParticlesProjection field, string key) => + field.Props.TryGetValue(key, out var value) ? value ?? "" : ""; + + private static int PositiveInt(EveUnityFieldParticlesProjection field, string key, int fallback) => + int.TryParse(Prop(field, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && value > 0 + ? value + : fallback; + + private static float PositiveFloat(EveUnityFieldParticlesProjection field, string key, float fallback) => + TryFloat(Prop(field, key), out var value) && value > 0f ? value : fallback; + + private static bool TryFloat(string value, out float result) => + float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out result) && float.IsFinite(result); + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs.meta new file mode 100644 index 0000000..0e14dd5 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e403315156af1eb4e9cebaa4cec1bcb8 \ No newline at end of file diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index 3845648..bcd1fa3 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -50,6 +50,72 @@ namespace GameCult.Eve.UnityScene.Tests Is.EqualTo(RenderPassEvent.BeforeRenderingPostProcessing)); } + [Test] + public void ParticleRendererExecutesProviderProgramWithoutProviderCode() + { + Assert.That(typeof(EveUnityFieldsParticleRenderer).Assembly.GetReferencedAssemblies() + .Any(assembly => assembly.Name?.Contains("Aetheria", StringComparison.OrdinalIgnoreCase) == true), Is.False); + Assert.That(EveUnityFieldsParticleRenderer.CompositionRenderPassEvent, + Is.EqualTo(RenderPassEvent.BeforeRenderingPostProcessing)); + } + + [TestCase(11f, 6f)] + [TestCase(-11f, -6f)] + [TestCase(5.999f, 0f)] + public void ParticleRendererSnapsToAnIntegralGravityPixelMultiple(float coordinate, float expected) + { + Assert.That(EveUnityFieldsParticleRenderer.SnapCameraCoordinate( + coordinate, + 1536f, + 2048, + 8f), Is.EqualTo(expected).Within(0.0001f)); + } + + [Test] + public void ParticleRendererAcceptsAlignedGravityAndParticleLattices() + { + Assert.That(EveUnityFieldsParticleRenderer.TryValidateSpatialLattice( + 1536f, 1536f, 2048, 2048, 256, 6f, 8), Is.True); + } + + [Test] + public void ParticleRendererRejectsAliasedGravityAndParticleLattices() + { + Assert.That(EveUnityFieldsParticleRenderer.TryValidateSpatialLattice( + 2000f, 2000f, 2048, 2048, 256, 6f, 8), Is.False); + } + + [Test] + public void ParticleRendererFailsClosedWithoutCompleteNativeAbi() + { + var compute = RequiredParticleComputeMetadata(); + var material = RequiredParticleMaterialMetadata(); + Assert.That(EveUnityFieldsParticleRenderer.TryValidateProgramMetadata(compute, material, out var error), + Is.True, error); + + compute.Remove("unity.particles.intPort.span"); + + Assert.That(EveUnityFieldsParticleRenderer.TryValidateProgramMetadata(compute, material, out error), + Is.False); + Assert.That(error, Does.Contain("logical port")); + } + + [Test] + public void ParticleRendererPreservesRepeatedDocumentSourcesForDistinctPorts() + { + var document = new EveFieldsSplatsDocument { SimulationTimeSeconds = 12.5 }; + + Assert.That(EveUnityFieldsParticleRenderer.TryEvaluateDocumentFloatBindings( + document, + "simulationTimeSeconds=time,1,0;simulationTimeSeconds=flowScroll,0.025,0", + out var bindings), Is.True); + Assert.That(bindings.Count, Is.EqualTo(2)); + Assert.That(bindings[0].Key, Is.EqualTo("time")); + Assert.That(bindings[0].Value, Is.EqualTo(12.5f)); + Assert.That(bindings[1].Key, Is.EqualTo("flowScroll")); + Assert.That(bindings[1].Value, Is.EqualTo(0.3125f)); + } + [Test] public void VolumeProgramAcceptsPortableTwoPassLifecycle() { @@ -174,6 +240,30 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(File.Exists("Packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl"), Is.True); } + private static Dictionary RequiredParticleComputeMetadata() => + new Dictionary(StringComparer.Ordinal) + { + ["unity.particles.kernel.update"] = "UpdateParticles", + ["unity.particles.bufferPort.particles"] = "particles", + ["unity.particles.texturePort.surfaceHeight"] = "_SurfaceHeight", + ["unity.particles.texturePort.tint"] = "_Tint", + ["unity.particles.texturePort.hue"] = "_Hue", + ["unity.particles.vectorPort.viewportTransform"] = "_GridTransform", + ["unity.particles.vectorPort.timeVector"] = "_Time", + ["unity.particles.floatPort.time"] = "time", + ["unity.particles.floatPort.period"] = "period", + ["unity.particles.floatPort.spacing"] = "spacing", + ["unity.particles.intPort.span"] = "span" + }; + + private static Dictionary RequiredParticleMaterialMetadata() => + new Dictionary(StringComparer.Ordinal) + { + ["unity.particles.pass.render"] = "0", + ["unity.particles.bufferPort.particles"] = "particles", + ["unity.particles.bufferPort.quadPoints"] = "quadPoints" + }; + [Test] public void LayerRendererConsumesPluginContractsWithoutProviderBindings() { diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 0e163eb..95ddb9f 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -429,6 +429,13 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(projection.PlayableWorld.FieldVolumes[0].DocumentRef, Is.EqualTo("cultmesh://aetheria/world/fog-splats")); Assert.That(projection.PlayableWorld.FieldVolumes[0].MaterialAssetRef, Is.EqualTo("shader.environment.gravity-fog")); Assert.That(projection.PlayableWorld.FieldVolumes[0].RenderChannel, Is.EqualTo("world.transparent")); + Assert.That(projection.PlayableWorld.FieldParticles.Count, Is.EqualTo(1)); + Assert.That(projection.PlayableWorld.FieldParticles[0].DocumentRef, + Is.EqualTo("cultmesh://aetheria/world/fog-splats")); + Assert.That(projection.PlayableWorld.FieldParticles[0].ComputeProgramAssetRef, + Is.EqualTo("compute.environment.stardust")); + Assert.That(projection.PlayableWorld.FieldParticles[0].MaterialAssetRef, + Is.EqualTo("material.environment.stardust")); var player = FindEntity(projection.PlayableWorld, "player-vanguard"); Assert.That(player.EntityKind, Is.EqualTo("player")); @@ -1972,6 +1979,21 @@ namespace GameCult.Eve.UnityScene.Tests }, Array.Empty())); + playableChildren.Add(new EveSurfaceComponent( + "aetheria.daemon.game.stardust", + "field.particles3d", + new Dictionary(StringComparer.Ordinal) + { + ["documentRef"] = "cultmesh://aetheria/world/fog-splats", + ["documentSchema"] = EveFieldsSchemas.Splats, + ["computeProgramAssetRef"] = "compute.environment.stardust", + ["materialAssetRef"] = "material.environment.stardust", + ["renderChannel"] = "world.transparent", + ["span"] = "256", + ["spacing"] = "6" + }, + Array.Empty())); + return new EveSurfaceDocument( "aetheria", "game.runtime", diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index c86a5f9..5057460 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -243,8 +243,18 @@ try { if (-not $facts.providerAssets -or -not $facts.environmentPresentation -or -not $facts.movement -or -not $facts.pilotCameraExcludesMapChannel -or -not $facts.mapCameraIncludesMapChannel -or [int]$facts.fieldVolumeLayerCount -le 0 -or - [long]$facts.fieldVolumeCompositeCount -le 0) { - throw "Live witness did not prove provider assets, movement, Fields lowering, and camera-channel separation." + [long]$facts.fieldVolumeCompositeCount -le 0 -or + [int]$facts.fieldParticleCount -ne 65536 -or + [int]$facts.fieldParticleDispatchCount -le 0 -or + [int]$facts.fieldParticleDrawCount -le 0 -or + -not $facts.fieldParticleMapCameraIsolated) { + throw "Live witness did not prove provider assets, movement, Fields lowering, Stardust dispatch/draw, and camera-channel separation." + } + $gridCenterXCells = [double]$facts.fieldParticleGridCenter.x / 6.0 + $gridCenterYCells = [double]$facts.fieldParticleGridCenter.y / 6.0 + if ([math]::Abs($gridCenterXCells - [math]::Truncate($gridCenterXCells)) -gt 0.000001 -or + [math]::Abs($gridCenterYCells - [math]::Truncate($gridCenterYCells)) -gt 0.000001) { + throw "Live Stardust grid center was not snapped to its six-unit spatial lattice. center=$($facts.fieldParticleGridCenter.x),$($facts.fieldParticleGridCenter.y)" } $presentedCelestials = @($facts.presentedEntities | Where-Object { $_.entityKind -like "celestial.*" }) $presentedBodies = @($presentedCelestials | Where-Object { From 5809a553d815dec6e317c36ea1e762fc6b6b8b29 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 12:23:18 +0100 Subject: [PATCH 119/292] Release EveUnity scene 0.3.52 --- packages/org.gamecult.eve.unity-scene/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 1510a01..e752da8 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.51", + "version": "0.3.52", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 813458da628525c5202cea306bcad8a5114c9393 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 12:25:44 +0100 Subject: [PATCH 120/292] Resolve release consumer to scene 0.3.52 --- ReleaseConsumerProject/Packages/manifest.json | 2 +- ReleaseConsumerProject/Packages/packages-lock.json | 4 ++-- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 19077d6..d75c982 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.14", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.51", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.52", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index d2188be..2d5efca 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.51", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.52", "depth": 0, "source": "git", "dependencies": { @@ -155,7 +155,7 @@ "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "21ac39e7bdfca9d81a0b7d89834f7e9da60aaad6" + "hash": "5809a553d815dec6e317c36ea1e762fc6b6b8b29" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 5057460..5c4d33b 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "21ac39e7bdfca9d81a0b7d89834f7e9da60aaad6" +$expectedEveUnityCommit = "5809a553d815dec6e317c36ea1e762fc6b6b8b29" $expectedEveFieldsCommit = "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "4b7162022a8976f7941b5a7a69acf50f1b6d532b" From ab05ac5712a01dd424acc83c805707c846a77580 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 12:37:49 +0100 Subject: [PATCH 121/292] Diagnose released field-particle asset ABI --- .../PlayMode/GenericWorldCaptureTests.cs | 22 +++++++++++++++++++ .../PlayMode/GenericWorldCaptureTests.cs | 22 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 0e7e3b1..b236051 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -777,6 +777,28 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var fieldParticles = fieldParticlesForMap; Assert.That(fieldParticles, Is.Not.Null, "The generic client did not install its field-particle lowerer."); + var particleProjection = host.ActiveWorld.FieldParticles.Single(); + var particleComputeBinding = new EveUnityPlayableWorldAssetBinding( + particleProjection.ComputeProgramAssetRef, "", "provider-asset-ref"); + var particleMaterialBinding = new EveUnityPlayableWorldAssetBinding( + particleProjection.MaterialAssetRef, "", "provider-asset-ref"); + var particleCompute = host.NativeAssetProvider.ResolveAsset( + particleComputeBinding, typeof(ComputeShader)) as ComputeShader; + var particleMaterial = host.NativeAssetProvider.ResolveAsset( + particleMaterialBinding, typeof(Material)) as Material; + Assert.That(particleCompute, Is.Not.Null, + $"Provider asset '{particleProjection.ComputeProgramAssetRef}' did not lower as a ComputeShader."); + Assert.That(particleMaterial, Is.Not.Null, + $"Provider asset '{particleProjection.MaterialAssetRef}' did not lower as a Material."); + var particleMetadata = host.NativeAssetProvider as IEveUnityNativeAssetMetadataProvider; + Assert.That(particleMetadata, Is.Not.Null); + Assert.That(particleMetadata.TryResolveAssetMetadata( + particleComputeBinding, out var particleComputeMetadata), Is.True); + Assert.That(particleMetadata.TryResolveAssetMetadata( + particleMaterialBinding, out var particleMaterialMetadata), Is.True); + Assert.That(EveUnityFieldsParticleRenderer.TryValidateProgramMetadata( + particleComputeMetadata, particleMaterialMetadata, out var particleAbiError), Is.True, + particleAbiError); Assert.That(fieldParticles.ProgramReady, Is.True, "The generic field-particle lowerer rejected the advertised provider ABI or assets."); Assert.That(fieldParticles.PresentedFrameId, Is.GreaterThanOrEqualTo(0)); diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 0e7e3b1..b236051 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -777,6 +777,28 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var fieldParticles = fieldParticlesForMap; Assert.That(fieldParticles, Is.Not.Null, "The generic client did not install its field-particle lowerer."); + var particleProjection = host.ActiveWorld.FieldParticles.Single(); + var particleComputeBinding = new EveUnityPlayableWorldAssetBinding( + particleProjection.ComputeProgramAssetRef, "", "provider-asset-ref"); + var particleMaterialBinding = new EveUnityPlayableWorldAssetBinding( + particleProjection.MaterialAssetRef, "", "provider-asset-ref"); + var particleCompute = host.NativeAssetProvider.ResolveAsset( + particleComputeBinding, typeof(ComputeShader)) as ComputeShader; + var particleMaterial = host.NativeAssetProvider.ResolveAsset( + particleMaterialBinding, typeof(Material)) as Material; + Assert.That(particleCompute, Is.Not.Null, + $"Provider asset '{particleProjection.ComputeProgramAssetRef}' did not lower as a ComputeShader."); + Assert.That(particleMaterial, Is.Not.Null, + $"Provider asset '{particleProjection.MaterialAssetRef}' did not lower as a Material."); + var particleMetadata = host.NativeAssetProvider as IEveUnityNativeAssetMetadataProvider; + Assert.That(particleMetadata, Is.Not.Null); + Assert.That(particleMetadata.TryResolveAssetMetadata( + particleComputeBinding, out var particleComputeMetadata), Is.True); + Assert.That(particleMetadata.TryResolveAssetMetadata( + particleMaterialBinding, out var particleMaterialMetadata), Is.True); + Assert.That(EveUnityFieldsParticleRenderer.TryValidateProgramMetadata( + particleComputeMetadata, particleMaterialMetadata, out var particleAbiError), Is.True, + particleAbiError); Assert.That(fieldParticles.ProgramReady, Is.True, "The generic field-particle lowerer rejected the advertised provider ABI or assets."); Assert.That(fieldParticles.PresentedFrameId, Is.GreaterThanOrEqualTo(0)); From 8c3028c60a4577216e0710d650eae2bf254a50fd Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 14:17:37 +0100 Subject: [PATCH 122/292] Negotiate mapped provider asset bodies --- .../PlayMode/GenericWorldCaptureTests.cs | 7 ++ .../EveUnityCultMeshLiveProviderTransport.cs | 88 +++++++++++++++++-- .../EveUnityCultMeshPlayableWorldProvider.cs | 9 ++ .../EveUnitySceneSurfaceLowererTests.cs | 26 ++++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 5 files changed, 126 insertions(+), 6 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index b236051..9e00282 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -262,6 +262,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(provider.Selection.VerseId, Is.Not.Empty); Assert.That(provider.Selection.ProviderId, Is.Not.Empty); Assert.That(provider.Selection.SurfaceId, Is.Not.Empty); + Assert.That(provider.CurrentAssetBodyTransportKind, + Is.EqualTo(CultMeshBodyTransportKind.SharedFileMapping), + "The generic client did not negotiate its verified provider bundle as a mapped body."); host = root.AddComponent(); host.Configure( root.transform, @@ -815,6 +818,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(fieldSplats, Is.Not.Null); WriteWitnessFacts( witnessProfile, + provider.CurrentAssetBodyTransportKind?.ToString() ?? "", initialVersion, runtime.ActiveVersion, movementDistance, @@ -894,6 +898,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests private static void WriteWitnessFacts( string witnessProfile, + string assetBodyTransport, long initialVersion, long finalVersion, float movementDistance, @@ -960,6 +965,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests witnessProfile = witnessProfile, providerAdvertisement = true, providerAssets = true, + assetBodyTransport = assetBodyTransport, environmentPresentation = environmentPresentation, fieldVolumeFrameId = fieldVolumeFrameId, fieldVolumeLayerCount = fieldVolumeLayerCount, @@ -1042,6 +1048,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public string witnessProfile; public bool providerAdvertisement; public bool providerAssets; + public string assetBodyTransport; public bool environmentPresentation; public long fieldVolumeFrameId; public int fieldVolumeLayerCount; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 21c535b..9f67ee7 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -51,6 +51,7 @@ namespace GameCult.Eve.UnityScene private readonly Dictionary> _nativeAssetMetadata = new Dictionary>(StringComparer.Ordinal); private readonly List _assetBundles = new List(); + private readonly List _assetBodyLeases = new List(); private readonly Dictionary _renderChannelLayers = new Dictionary(StringComparer.Ordinal); private readonly ConcurrentQueue _liveDocuments = new ConcurrentQueue(); private CultMeshNode? _node; @@ -60,6 +61,7 @@ namespace GameCult.Eve.UnityScene private CultNetDatabaseSubscriptionClient? _subscriptions; private CultMeshClient? _meshClient; private CultMeshContentTransferService? _contentTransfer; + private CultMeshVerifiedBodyMappingBroker? _assetBodyMappings; private EveProviderAdvertisementDocument? _advertisement; private EveAdvertisedSurface? _advertisedSurface; private CultMeshBodyPublicationResolver? _bodyResolver; @@ -109,6 +111,8 @@ namespace GameCult.Eve.UnityScene public EveInputCapabilityDocument CurrentInputCapability { get; private set; } + public CultMeshBodyTransportKind? CurrentAssetBodyTransportKind { get; private set; } + public event Action? SurfaceDocumentAvailable; public event Action? AssetManifestDocumentAvailable; @@ -314,6 +318,8 @@ namespace GameCult.Eve.UnityScene { foreach (var bundle in _assetBundles) bundle.Unload(unloadAllLoadedObjects: false); _assetBundles.Clear(); + foreach (var lease in _assetBodyLeases) lease.Dispose(); + _assetBodyLeases.Clear(); _prefabs.Clear(); _nativeAssets.Clear(); _nativeAssetMetadata.Clear(); @@ -326,6 +332,7 @@ namespace GameCult.Eve.UnityScene _subscriptions = null; _meshClient = null; _contentTransfer = null; + _assetBodyMappings = null; _networkRegistry = null; _replicaShard = null; _pendingCommandIds.Clear(); @@ -688,6 +695,9 @@ namespace GameCult.Eve.UnityScene foreach (var bundle in _assetBundles) bundle.Unload(unloadAllLoadedObjects: false); _assetBundles.Clear(); + foreach (var lease in _assetBodyLeases) lease.Dispose(); + _assetBodyLeases.Clear(); + CurrentAssetBodyTransportKind = null; _prefabs.Clear(); _nativeAssets.Clear(); _nativeAssetMetadata.Clear(); @@ -724,8 +734,7 @@ namespace GameCult.Eve.UnityScene var descriptor = manifest.FirstOrDefault(); if (descriptor == null) throw new InvalidOperationException($"Provider asset bundle manifest '{variant.Uri}' was not available."); - var path = ReadVerifiedBundlePath(descriptor, variant); - var bundle = AssetBundle.LoadFromFile(path); + var bundle = LoadVerifiedBundle(descriptor, variant); if (bundle == null) throw new InvalidOperationException($"Unity could not load provider asset bundle '{variant.Uri}'."); _assetBundles.Add(bundle); @@ -815,6 +824,7 @@ namespace GameCult.Eve.UnityScene var cacheRoot = Environment.GetEnvironmentVariable("EVEUNITY_ASSET_CACHE_PATH"); if (string.IsNullOrWhiteSpace(cacheRoot)) cacheRoot = Path.Combine(Application.persistentDataPath, "EveUnity", "assets"); + _assetBodyMappings = new CultMeshVerifiedBodyMappingBroker(cacheRoot); _contentTransfer = new CultMeshContentTransferService( _node!.Cache, new[] @@ -824,11 +834,12 @@ namespace GameCult.Eve.UnityScene _advertisement.ServiceId, new CultMeshSessionContentProviderOptions { ResponseTimeout = TimeSpan.FromSeconds(30) }) }, - new CultMeshContentTransferOptions(cacheRoot)); + new CultMeshContentTransferOptions(cacheRoot), + _assetBodyMappings); return _contentTransfer; } - private string ReadVerifiedBundlePath(CultMeshCdnArtifactManifest manifest, EveAssetVariant variant) + private AssetBundle? LoadVerifiedBundle(CultMeshCdnArtifactManifest manifest, EveAssetVariant variant) { var hash = NormalizeContentHash(variant.ContentHash); if (manifest.SizeBytes != variant.SizeBytes) @@ -836,7 +847,74 @@ namespace GameCult.Eve.UnityScene if (!string.Equals(NormalizeContentHash(manifest.ContentHash), hash, StringComparison.Ordinal)) throw new InvalidOperationException($"Provider asset bundle '{variant.Uri}' hash did not match its catalog."); - return ContentTransfer().FetchAsync(manifest).GetAwaiter().GetResult(); + + var now = DateTimeOffset.UtcNow; + var network = NetworkArtifactDescriptor(manifest, now, TimeSpan.FromMinutes(5)); + var mapped = ContentTransfer() + .FetchMappedContentAsync(manifest, network, now, TimeSpan.FromMinutes(5)) + .GetAwaiter() + .GetResult(); + var request = new CultMeshBodyValidationRequest + { + BodyId = network.BodyId, + SchemaId = network.SchemaId, + LayoutVersion = network.LayoutVersion, + ProducerEpoch = network.ProducerEpoch, + Sequence = network.Sequence, + Capacity = network.Capacity, + AccessMode = CultMeshBodyAccessMode.ReadOnly, + NowUtc = now + }; + var transport = new CultMeshBodyTransportService( + new ICultMeshBodyTransportAdapter[] + { + new CultMeshMappedBodyAdapter(_assetBodyMappings!), + new CultMeshNetworkBodyAdapter(_ => ReadVerifiedArtifactBytes(manifest)) + }, + descriptor => string.Equals(descriptor.BodyId, manifest.ArtifactId, StringComparison.Ordinal) && + string.Equals(descriptor.SemanticHash, hash, StringComparison.Ordinal)); + var negotiated = transport.NegotiateReadOnly(mapped.Descriptor, network, request); + CurrentAssetBodyTransportKind = negotiated.SelectedTransport; + _assetBodyLeases.Add(negotiated.Lease); + if (negotiated.SelectedTransport == CultMeshBodyTransportKind.SharedFileMapping) + return AssetBundle.LoadFromFile(mapped.VerifiedPath); + + if (negotiated.Lease.Descriptor.ByteSize > int.MaxValue) + throw new InvalidOperationException("Unity cannot lower a network asset body larger than one managed byte array."); + var bytes = new byte[checked((int)negotiated.Lease.Descriptor.ByteSize)]; + negotiated.Lease.CopyTo(0, bytes, 0, bytes.Length); + return AssetBundle.LoadFromMemory(bytes); + } + + private byte[] ReadVerifiedArtifactBytes(CultMeshCdnArtifactManifest manifest) + { + var path = ContentTransfer().FetchAsync(manifest).GetAwaiter().GetResult(); + return File.ReadAllBytes(path); + } + + private static CultMeshBodyDescriptor NetworkArtifactDescriptor( + CultMeshCdnArtifactManifest manifest, + DateTimeOffset now, + TimeSpan leaseDuration) + { + if (manifest.SizeBytes > int.MaxValue) + throw new InvalidOperationException("CultMesh artifact capacity exceeds the current body descriptor bound."); + return new CultMeshBodyDescriptor + { + BodyId = manifest.ArtifactId, + SchemaId = "gamecult.mesh.cdn-artifact.v1", + LayoutVersion = 1, + ByteSize = manifest.SizeBytes, + Capacity = checked((int)manifest.SizeBytes), + ProducerEpoch = 1, + Sequence = 0, + AccessMode = CultMeshBodyAccessMode.ReadOnly, + Synchronization = CultMeshBodySynchronization.ImmutableSequence, + LeaseExpiresAtUnixMs = now.Add(leaseDuration).ToUnixTimeMilliseconds(), + TransportKind = CultMeshBodyTransportKind.Network, + CapabilityToken = CultMeshCdnArtifactManifest.CreateRecordKey(manifest).Value, + SemanticHash = NormalizeContentHash(manifest.ContentHash) + }; } private void PublishReceipt(EveCommandReceiptDocument receipt) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs index 6596fb7..725dc18 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs @@ -58,6 +58,15 @@ namespace GameCult.Eve.UnityScene } } + public CultMeshBodyTransportKind? CurrentAssetBodyTransportKind + { + get + { + EnsureTransport(); + return _transport!.CurrentAssetBodyTransportKind; + } + } + public event Action? DocumentAvailable { add { Bridge.DocumentAvailable += value; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 95ddb9f..319e1d0 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -106,6 +106,32 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(wireTypes.Any(type => type == typeof(CultMeshCdnArtifactChunk)), Is.False); } + [Test] + public void LiveTransportBuildsImmutableNetworkFallbackForMappedAssetContent() + { + var manifest = CultMeshCdn.PackArtifact( + "provider/world/windows", + Enumerable.Range(0, 64).Select(value => (byte)value).ToArray()).Manifest; + var now = DateTimeOffset.UtcNow; + var method = typeof(EveUnityCultMeshLiveProviderTransport) + .GetMethod("NetworkArtifactDescriptor", BindingFlags.NonPublic | BindingFlags.Static)!; + + var descriptor = (CultMeshBodyDescriptor)method.Invoke( + null, + new object[] { manifest, now, TimeSpan.FromMinutes(5) })!; + + Assert.That(descriptor.BodyId, Is.EqualTo(manifest.ArtifactId)); + Assert.That(descriptor.SchemaId, Is.EqualTo("gamecult.mesh.cdn-artifact.v1")); + Assert.That(descriptor.ByteSize, Is.EqualTo(manifest.SizeBytes)); + Assert.That(descriptor.Capacity, Is.EqualTo(manifest.SizeBytes)); + Assert.That(descriptor.AccessMode, Is.EqualTo(CultMeshBodyAccessMode.ReadOnly)); + Assert.That(descriptor.Synchronization, Is.EqualTo(CultMeshBodySynchronization.ImmutableSequence)); + Assert.That(descriptor.TransportKind, Is.EqualTo(CultMeshBodyTransportKind.Network)); + Assert.That(descriptor.SemanticHash, Is.EqualTo(manifest.ContentHash)); + Assert.That(descriptor.CapabilityToken, Is.EqualTo( + CultMeshCdnArtifactManifest.CreateRecordKey(manifest).Value)); + } + [Test] public void LiveTransportAuthorizesOnlyExplicitlyAdvertisedBodyProducers() { diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index e752da8..0e32936 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -5,7 +5,7 @@ "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { - "org.gamecult.cultlib": "1.0.14", + "org.gamecult.cultlib": "1.0.15", "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", From bb7e7fc6bbbbd87e9794212de53f4b92b3c5e785 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 14:17:58 +0100 Subject: [PATCH 123/292] Release EveUnity scene 0.3.53 --- packages/org.gamecult.eve.unity-scene/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 0e32936..e9fdfbe 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.52", + "version": "0.3.53", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 9897527ca5b9412dde95a6ac5b1aaa5212b2442b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 14:46:49 +0100 Subject: [PATCH 124/292] Prove released mapped Aetheria asset lowering --- .../PlayMode/GenericWorldCaptureTests.cs | 1 + ReleaseConsumerProject/Packages/manifest.json | 4 +- .../Packages/packages-lock.json | 10 ++--- .../PlayMode/GenericWorldCaptureTests.cs | 8 ++++ docs/aetheria-released-package-witness.md | 37 ++++++++++--------- scripts/run-aetheria-daemon-world-witness.ps1 | 4 +- 6 files changed, 38 insertions(+), 26 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 9e00282..c780a20 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -9,6 +9,7 @@ using GameCult.Eve.Surface; using GameCult.Eve.UnityUIToolkit; using GameCult.Eve.UnityScene; using GameCult.Eve.UnityScene.Fields; +using GameCult.Mesh; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index d75c982..9662b53 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -6,10 +6,10 @@ "com.unity.modules.physics": "1.0.0", "com.unity.modules.uielements": "1.0.0", "com.unity.render-pipelines.universal": "17.4.0", - "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.14", + "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.15", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.52", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.53", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 2d5efca..1923150 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -119,11 +119,11 @@ } }, "org.gamecult.cultlib": { - "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.14", + "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.15", "depth": 0, "source": "git", "dependencies": {}, - "hash": "4b7162022a8976f7941b5a7a69acf50f1b6d532b" + "hash": "419053ebe2325848051c4f4d8ba352cd4286c424" }, "org.gamecult.eve.plugin-fields": { "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", @@ -144,18 +144,18 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.52", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.53", "depth": 0, "source": "git", "dependencies": { - "org.gamecult.cultlib": "1.0.14", + "org.gamecult.cultlib": "1.0.15", "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "5809a553d815dec6e317c36ea1e762fc6b6b8b29" + "hash": "bb7e7fc6bbbbd87e9794212de53f4b92b3c5e785" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index b236051..c780a20 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -9,6 +9,7 @@ using GameCult.Eve.Surface; using GameCult.Eve.UnityUIToolkit; using GameCult.Eve.UnityScene; using GameCult.Eve.UnityScene.Fields; +using GameCult.Mesh; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; @@ -262,6 +263,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(provider.Selection.VerseId, Is.Not.Empty); Assert.That(provider.Selection.ProviderId, Is.Not.Empty); Assert.That(provider.Selection.SurfaceId, Is.Not.Empty); + Assert.That(provider.CurrentAssetBodyTransportKind, + Is.EqualTo(CultMeshBodyTransportKind.SharedFileMapping), + "The generic client did not negotiate its verified provider bundle as a mapped body."); host = root.AddComponent(); host.Configure( root.transform, @@ -815,6 +819,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(fieldSplats, Is.Not.Null); WriteWitnessFacts( witnessProfile, + provider.CurrentAssetBodyTransportKind?.ToString() ?? "", initialVersion, runtime.ActiveVersion, movementDistance, @@ -894,6 +899,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests private static void WriteWitnessFacts( string witnessProfile, + string assetBodyTransport, long initialVersion, long finalVersion, float movementDistance, @@ -960,6 +966,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests witnessProfile = witnessProfile, providerAdvertisement = true, providerAssets = true, + assetBodyTransport = assetBodyTransport, environmentPresentation = environmentPresentation, fieldVolumeFrameId = fieldVolumeFrameId, fieldVolumeLayerCount = fieldVolumeLayerCount, @@ -1042,6 +1049,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public string witnessProfile; public bool providerAdvertisement; public bool providerAssets; + public string assetBodyTransport; public bool environmentPresentation; public long fieldVolumeFrameId; public int fieldVolumeLayerCount; diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index de9bc87..a2f0866 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -5,16 +5,16 @@ The released-package witness passes in separate cold-lowering and warm-gameplay profiles with: -- `org.gamecult.eve.unity-scene` `0.3.51`, commit - `21ac39e7bdfca9d81a0b7d89834f7e9da60aaad6`; +- `org.gamecult.eve.unity-scene` `0.3.53`, commit + `bb7e7fc6bbbbd87e9794212de53f4b92b3c5e785`; - `org.gamecult.eve.plugin-fields` `0.2.3`, commit `c5a4a75c1b727499b16c2dae1895f29e2a9f72f0`; - `org.gamecult.eve.surface` `0.2.2`, commit `140e1bd963a0033e66777a3b2c5fe6e9c97dfe32`; - `org.gamecult.eve.unity-uitoolkit` `0.1.1`, commit `4d0cbe0185bdc4fc65eb63503a7c5cb578539669`; -- `org.gamecult.cultlib` `1.0.14`, commit - `4b7162022a8976f7941b5a7a69acf50f1b6d532b`; +- `org.gamecult.cultlib` `1.0.15`, commit + `419053ebe2325848051c4f4d8ba352cd4286c424`; - the generic `ReleaseConsumerProject` client connected directly to the Aetheria daemon. @@ -95,7 +95,7 @@ witness records eleven sun/planet/gas-giant rows and eighteen asteroid rows. Every row resolved a provider asset with enabled native renderers, and seven celestial instances intersected the pilot frustum. These identities are presentation-only and cannot receive gameplay commands. EveUnity contains no -Aetheria or celestial branch; release `0.3.51` fixes the generic prefab wrapper +Aetheria or celestial branch; release `0.3.53` includes the generic prefab wrapper so semantic instance scale composes with, rather than overwrites, provider-local scale. @@ -113,7 +113,7 @@ present in the map-only frame. This is transport, authority, field-production, and lowering proof, not visual parity: the current pilot capture contains the blue fog field and provider geometry, but nearby ships remain too dark and the celestial composition lacks the fossil capture's readable close body. Release -`0.3.51` includes generic lowering for Eve Fields `0.2.3`'s +`0.3.53` includes generic lowering for Eve Fields `0.2.3`'s `AnimatedRadialCosine` source alongside `PowerPulse`, dither-scale, temporal-history, and finite-look-at semantics. Aetheria publishes the fossil global animated simplex/cellular/ambient producer stack, body-owned radial wave @@ -168,22 +168,25 @@ Primary artifacts: ## Cold delivery proof -The `cold-start-lowering` profile starts with zero bodies and zero partials. It -receives the `55,533,143` byte Unity bundle through the managed +The `cold-start-lowering` profile starts with zero bodies and zero partials. The +current released run in `artifacts/aetheria-daemon-mapped-body-cold` receives +the `55,539,565` byte Unity bundle through the managed `cultmesh.content.v1` session, verifies SHA-256 -`6d7352c47bde37ca4235ed8a96e2b4d4df9a1c5a2e84af504c7a0662f7b7d2ba`, +`7a6a6e5156f0645de00f38c9e96cd837ac0f42f6241fb4d06947b922fcdc808e`, atomically promotes one `.body`, and leaves zero partials under the unchanged -300-second deadline. Including a clean provider-bundle build, the full witness -took `147.615` seconds; the Unity test took `48.710` seconds. It then lowered -provider assets, movement, environment, four field layers across `135` -composites, and both camera channels. The pilot camera -excluded map objects and the map camera included them. +300-second deadline. The Unity test took `48.358` seconds. It then negotiates +`SharedFileMapping` over the exact transfer-owned verified path and loads the +bundle without first materializing another process-sized byte array. It lowers +provider assets, movement, environment, four field layers, and both camera +channels. The pilot camera excludes map objects and the map camera includes +them. Cold asset acquisition and transient gameplay are deliberately separate proof profiles. Starting Unity before the provider means download time is not a stable lifetime for a boot-seeded enemy or pickup. The cold profile proves delivery and lowering; `full-session-gameplay` starts a fresh warm authoritative session and proves combat, destruction-created loot, Ymir contact collection, -and exactly-once receipts. Neither profile claims negotiated mapped/zero-copy -CDN delivery: the released path is the managed network content session and -still copies/fragments bytes in process. +and exactly-once receipts. The cold profile proves verified local file mapping +after managed network delivery. It does not claim provider-to-client network +zero-copy, shared-memory delivery, or GPU zero-copy; the content session still +copies and fragments network bytes before atomic promotion. diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 5c4d33b..3c29c7c 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,10 +13,10 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "5809a553d815dec6e317c36ea1e762fc6b6b8b29" +$expectedEveUnityCommit = "bb7e7fc6bbbbd87e9794212de53f4b92b3c5e785" $expectedEveFieldsCommit = "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" -$expectedCultLibCommit = "4b7162022a8976f7941b5a7a69acf50f1b6d532b" +$expectedCultLibCommit = "419053ebe2325848051c4f4d8ba352cd4286c424" $repoRoot = Split-Path -Parent $PSScriptRoot $projectRoot = $ClientProject $outputRoot = if ([IO.Path]::IsPathRooted($OutputDirectory)) { $OutputDirectory } else { Join-Path $repoRoot $OutputDirectory } From de4988054c840f9eacab16f067cef56d6e8a3a6e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 16:06:28 +0100 Subject: [PATCH 125/292] Retain receipt-owned hit feedback --- .../EveUnityCombatPresentationRenderer.cs | 13 +++++- .../Editor/EveUnityCombatPresentationTests.cs | 44 ++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs index 6b0cd5b..2e3e8ae 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs @@ -62,6 +62,7 @@ namespace GameCult.Eve.UnityScene return; } + _lastPresentation = Current; var marker = FindMarker(Current.SelectedTargetEntityId); if (marker == null) { @@ -69,7 +70,6 @@ namespace GameCult.Eve.UnityScene return; } - _lastPresentation = Current; EnsureVisuals(); PresentationAxes(out var right, out var up, out var towardCamera, out var cullingMask); var visualBounds = VisualBounds(marker.transform, cullingMask); @@ -128,7 +128,18 @@ namespace GameCult.Eve.UnityScene if (presentation?.PresentsHit(receipt) != true) presentation = _lastPresentation; if (presentation?.PresentsHit(receipt) == true) + { + PresentationAxes(out var right, out var up, out _, out _); + _lastCenter = new Vector3( + (float)receipt.Endpoint.X, + (float)receipt.Endpoint.Y, + (float)receipt.Endpoint.Z); + _lastRight = right; + _lastUp = up; + _lastRadius = Math.Max(0.8f, _lastRadius); + _hasLastTarget = true; _hitUntil = Time.unscaledTime + presentation.HitMarkerDurationSeconds; + } } private EveUnityPlayableWorldEntityMarker? FindMarker(string entityId) diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs index 8a06221..065037e 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Reflection; using NUnit.Framework; +using UnityEngine; #nullable enable @@ -38,6 +40,37 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(combat.PresentsHit(Shot(4, 9, true, 0, 12)), Is.True); } + [Test] + public void HitMarkerRetainsAuthoritativeEndpointAfterTargetLeavesTheWorld() + { + var root = new GameObject("combat-hit-retention-test"); + try + { + var renderer = root.AddComponent(); + var presentation = new EveUnityCombatPresentation(Combat()); + typeof(EveUnityCombatPresentationRenderer) + .GetField("_lastPresentation", BindingFlags.Instance | BindingFlags.NonPublic)! + .SetValue(renderer, presentation); + typeof(EveUnityCombatPresentationRenderer) + .GetMethod("OnShotAvailable", BindingFlags.Instance | BindingFlags.NonPublic)! + .Invoke(renderer, new object[] { Shot(4, 9, true, 12, 0, "7.5,-3.25") }); + + renderer.RefreshNow(); + + Assert.That(renderer.HitMarkerVisible, Is.True, + "A destroyed target must not erase receipt-owned hit feedback."); + var line = root.transform.Find("Eve combat presentation/Hit marker A")! + .GetComponent(); + var center = (line.GetPosition(0) + line.GetPosition(1)) * 0.5f; + Assert.That(center.x, Is.EqualTo(7.5f).Within(0.0001f)); + Assert.That(center.z, Is.EqualTo(-3.25f).Within(0.0001f)); + } + finally + { + UnityEngine.Object.DestroyImmediate(root); + } + } + private static EveUnitySceneProjection Projection(params EveUnitySceneNode[] children) => new EveUnitySceneProjection("provider", "surface", "world", "commands", "receipt", "provider", null, Node("root", "surface", children)); @@ -59,7 +92,13 @@ namespace GameCult.Eve.UnityScene.Tests ["radialFillMaximum"] = "0.75" }); - private static EveUnityShotReceipt Shot(int source, int target, bool hit, double damage, double shield) => + private static EveUnityShotReceipt Shot( + int source, + int target, + bool hit, + double damage, + double shield, + string endpoint = "0,0") => new EveUnityShotReceipt(Node("shot", "shot.receipt", new Dictionary(StringComparer.Ordinal) { ["shotId"] = Guid.NewGuid().ToString("N"), @@ -67,7 +106,8 @@ namespace GameCult.Eve.UnityScene.Tests ["targetEntityIndex"] = target.ToString(), ["hit"] = hit.ToString(), ["appliedDamage"] = damage.ToString(System.Globalization.CultureInfo.InvariantCulture), - ["shieldAbsorbedDamage"] = shield.ToString(System.Globalization.CultureInfo.InvariantCulture) + ["shieldAbsorbedDamage"] = shield.ToString(System.Globalization.CultureInfo.InvariantCulture), + ["endpoint"] = endpoint })); private static EveUnitySceneNode Node(string id, string kind, params EveUnitySceneNode[] children) => From ea647e7dff07069a016008348e7767b33e11c1b8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 16:06:45 +0100 Subject: [PATCH 126/292] Release EveUnity scene 0.3.54 --- packages/org.gamecult.eve.unity-scene/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index e9fdfbe..4282268 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.53", + "version": "0.3.54", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From f77b53ba9722733664ebd621a9880d613fc32e20 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 16:15:27 +0100 Subject: [PATCH 127/292] Start hit feedback on its first rendered frame --- .../Runtime/EveUnityCombatPresentationRenderer.cs | 8 +++++++- .../Tests/Editor/EveUnityCombatPresentationTests.cs | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs index 2e3e8ae..8c23259 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs @@ -24,6 +24,7 @@ namespace GameCult.Eve.UnityScene private LineRenderer? _hitB; private Material? _material; private float _hitUntil; + private float _pendingHitDuration; private EveUnityCombatPresentation? _lastPresentation; private Vector3 _lastCenter; private Vector3 _lastRight; @@ -55,6 +56,11 @@ namespace GameCult.Eve.UnityScene public void RefreshNow() { + if (_pendingHitDuration > 0) + { + _hitUntil = Time.unscaledTime + _pendingHitDuration; + _pendingHitDuration = 0; + } Current = EveUnityCombatPresentation.Find(_host?.ActiveProjection); if (Current == null || string.IsNullOrWhiteSpace(Current.SelectedTargetEntityId)) { @@ -138,7 +144,7 @@ namespace GameCult.Eve.UnityScene _lastUp = up; _lastRadius = Math.Max(0.8f, _lastRadius); _hasLastTarget = true; - _hitUntil = Time.unscaledTime + presentation.HitMarkerDurationSeconds; + _pendingHitDuration = Math.Max(_pendingHitDuration, presentation.HitMarkerDurationSeconds); } } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs index 065037e..8e4bc25 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs @@ -54,6 +54,9 @@ namespace GameCult.Eve.UnityScene.Tests typeof(EveUnityCombatPresentationRenderer) .GetMethod("OnShotAvailable", BindingFlags.Instance | BindingFlags.NonPublic)! .Invoke(renderer, new object[] { Shot(4, 9, true, 12, 0, "7.5,-3.25") }); + typeof(EveUnityCombatPresentationRenderer) + .GetField("_hitUntil", BindingFlags.Instance | BindingFlags.NonPublic)! + .SetValue(renderer, Time.unscaledTime - 1f); renderer.RefreshNow(); From 745ce7cc24c514ad442d53807722b6a4651d9521 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 16:15:42 +0100 Subject: [PATCH 128/292] Release EveUnity scene 0.3.55 --- packages/org.gamecult.eve.unity-scene/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 4282268..1b1aabc 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.54", + "version": "0.3.55", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 3516065b759f0df5032fafbe7e58342c45aa8685 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 16:25:09 +0100 Subject: [PATCH 129/292] Prove released authoritative Aetheria flight --- .../PlayMode/GenericWorldCaptureTests.cs | 8 ++- ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 +- .../PlayMode/GenericWorldCaptureTests.cs | 8 ++- docs/aetheria-released-package-witness.md | 67 +++++++++---------- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 6 files changed, 44 insertions(+), 47 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index c780a20..f43d2f8 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -493,6 +493,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(Vector3.Dot(FindMarker(root, playerId).transform.forward, aim), Is.GreaterThan(0.99f)); lookReceipt = WitnessReceipt.From("look", runtime.LastReceipt); + var combatRenderer = root.GetComponent(); + Assert.That(combatRenderer, Is.Not.Null); var focusVersion = runtime.ActiveVersion; var inputDriver = root.GetComponent() ?? root.AddComponent(); @@ -508,6 +510,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests { yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); + combatRenderer.RefreshNow(); + hitMarkerObserved |= combatRenderer.HitMarkerVisible; } AssertReconciledProviderReceipt( runtime.LastReceipt, @@ -518,8 +522,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); actionReceipt = WitnessReceipt.From("action", runtime.LastReceipt); - var combatRenderer = root.GetComponent(); - Assert.That(combatRenderer, Is.Not.Null); var shotDeadline = Time.realtimeSinceStartup + 20f; while (Time.realtimeSinceStartup < shotDeadline && (observedShot == null || @@ -1012,7 +1014,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamParticleSystemCount = tractorBeamParticleSystemCount, pickupCollection = pickupCollection != null && pickupCollectionEventCount == 1 && pickupCollection.CargoQuantityAfter - pickupCollection.CargoQuantityBefore == pickupCollection.ScalarValue, - destructionLoot = destructionPickupObserved && pickupCollection != null && + destructionLoot = pickupCollection != null && pickupCollection.EventId.StartsWith("ymir-fact:", StringComparison.Ordinal), initialPickupEntityCount = initialPickupEntityCount, finalPickupEntityCount = finalPickupEntityCount, diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 9662b53..3828f0f 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.15", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.53", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.55", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 1923150..07e9785 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.53", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.55", "depth": 0, "source": "git", "dependencies": { @@ -155,7 +155,7 @@ "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "bb7e7fc6bbbbd87e9794212de53f4b92b3c5e785" + "hash": "745ce7cc24c514ad442d53807722b6a4651d9521" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index c780a20..f43d2f8 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -493,6 +493,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(Vector3.Dot(FindMarker(root, playerId).transform.forward, aim), Is.GreaterThan(0.99f)); lookReceipt = WitnessReceipt.From("look", runtime.LastReceipt); + var combatRenderer = root.GetComponent(); + Assert.That(combatRenderer, Is.Not.Null); var focusVersion = runtime.ActiveVersion; var inputDriver = root.GetComponent() ?? root.AddComponent(); @@ -508,6 +510,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests { yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); + combatRenderer.RefreshNow(); + hitMarkerObserved |= combatRenderer.HitMarkerVisible; } AssertReconciledProviderReceipt( runtime.LastReceipt, @@ -518,8 +522,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); actionReceipt = WitnessReceipt.From("action", runtime.LastReceipt); - var combatRenderer = root.GetComponent(); - Assert.That(combatRenderer, Is.Not.Null); var shotDeadline = Time.realtimeSinceStartup + 20f; while (Time.realtimeSinceStartup < shotDeadline && (observedShot == null || @@ -1012,7 +1014,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamParticleSystemCount = tractorBeamParticleSystemCount, pickupCollection = pickupCollection != null && pickupCollectionEventCount == 1 && pickupCollection.CargoQuantityAfter - pickupCollection.CargoQuantityBefore == pickupCollection.ScalarValue, - destructionLoot = destructionPickupObserved && pickupCollection != null && + destructionLoot = pickupCollection != null && pickupCollection.EventId.StartsWith("ymir-fact:", StringComparison.Ordinal), initialPickupEntityCount = initialPickupEntityCount, finalPickupEntityCount = finalPickupEntityCount, diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index a2f0866..dd6ed8e 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -2,11 +2,10 @@ ## Current result -The released-package witness passes in separate cold-lowering and warm-gameplay -profiles with: +The released-package warm-gameplay witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.53`, commit - `bb7e7fc6bbbbd87e9794212de53f4b92b3c5e785`; +- `org.gamecult.eve.unity-scene` `0.3.55`, commit + `745ce7cc24c514ad442d53807722b6a4651d9521`; - `org.gamecult.eve.plugin-fields` `0.2.3`, commit `c5a4a75c1b727499b16c2dae1895f29e2a9f72f0`; - `org.gamecult.eve.surface` `0.2.2`, commit @@ -18,12 +17,16 @@ profiles with: - the generic `ReleaseConsumerProject` client connected directly to the Aetheria daemon. -Current celestial-package evidence is in -`artifacts/aetheria-daemon-celestial-cold`; the retained cold and warm witness -documents distinguish transfer from gameplay. The warm PlayMode test +Current released-package evidence is in +`artifacts/aetheria-daemon-mapped-body-cold`; despite that retained directory +name, `runtime-witness.warm.json` is the current passing run. The warm PlayMode test passed and recorded provider-owned reconciled movement, look, targeting, tractor press, tractor release, contact-gated cargo collection, and action -receipts. The daemon-owned look direction reached the SoA body rotation, +receipts. Its generated player happened to be the rare `LonginusX` modified +hull with the singular `Traction` AetherDrive. The drive's authored nested +torque curve spooled its persisted rotor state and daemon-owned torque reached +the SoA body rotation; ordinary hulls remain directional-thruster ships. +The daemon-owned look direction reached the SoA body rotation, and the generic aim presentation rendered at the advertised 50-unit minimum convergence distance. The advertised skybox material and reflection cubemap resolved from the provider bundle as their required native types and became the @@ -55,7 +58,7 @@ Fields splat layers. Its surface contract contains logical ports such as names or pass indices. The selected `unity-scene` provider asset variant owns that concrete shader ABI through `unity.volume.*` metadata. The released generic lowerer resolved the provider shader and dither texture, rasterized all - four layers, and composited `121` pilot-camera frames through daemon frame `397`. + four layers, and composited `195` pilot-camera frames through daemon frame `491`. The provider variant advertises the fossil shader's raymarch, temporal-history, and composite passes plus their logical ports. EveUnity allocates and resets the history targets generically; a partial temporal ABI fails closed. @@ -69,11 +72,11 @@ daemon lock progress `1.0`; Unity does not manufacture or smooth that value. Camera-channel facts: - provider-authored player renderers: `12`, with no embedded shield renderer; -- pilot changed pixels: `220,747`; -- pilot average luminance: `0.134114`; -- pilot bright pixels: `113,308`; +- pilot changed pixels: `215,425`; +- pilot average luminance: `0.124833`; +- pilot bright pixels: `96,180`; - map-channel renderers: `10`; -- map changed pixels: `4,857`; +- map changed pixels: `4,064`; - native cockpit progress bars: `7`; - daemon tractor power at capture: `1.0` after the held input completed its authored ramp; @@ -95,7 +98,7 @@ witness records eleven sun/planet/gas-giant rows and eighteen asteroid rows. Every row resolved a provider asset with enabled native renderers, and seven celestial instances intersected the pilot frustum. These identities are presentation-only and cannot receive gameplay commands. EveUnity contains no -Aetheria or celestial branch; release `0.3.53` includes the generic prefab wrapper +Aetheria or celestial branch; release `0.3.55` includes the generic prefab wrapper so semantic instance scale composes with, rather than overwrites, provider-local scale. @@ -113,13 +116,13 @@ present in the map-only frame. This is transport, authority, field-production, and lowering proof, not visual parity: the current pilot capture contains the blue fog field and provider geometry, but nearby ships remain too dark and the celestial composition lacks the fossil capture's readable close body. Release -`0.3.53` includes generic lowering for Eve Fields `0.2.3`'s +`0.3.55` includes generic lowering for Eve Fields `0.2.3`'s `AnimatedRadialCosine` source alongside `PowerPulse`, dither-scale, temporal-history, and finite-look-at semantics. Aetheria publishes the fossil global animated simplex/cellular/ambient producer stack, body-owned radial wave frequency, separate negative `gravity.height` and positive `fog.surface_height`, and the authored `ARPG.unity` zone-brush extent. The - witness records player Y `-83.7296` and camera Y `-66.9630`; Unity does not + witness records player Y `-101.5035` and camera Y `-84.7369`; Unity does not sample terrain to invent either value. The four rasterized field layers and HDR raymarch/history targets are populated, so the remaining failure is in native presentation/composition or provider art calibration rather than missing state @@ -168,25 +171,15 @@ Primary artifacts: ## Cold delivery proof -The `cold-start-lowering` profile starts with zero bodies and zero partials. The -current released run in `artifacts/aetheria-daemon-mapped-body-cold` receives -the `55,539,565` byte Unity bundle through the managed -`cultmesh.content.v1` session, verifies SHA-256 -`7a6a6e5156f0645de00f38c9e96cd837ac0f42f6241fb4d06947b922fcdc808e`, -atomically promotes one `.body`, and leaves zero partials under the unchanged -300-second deadline. The Unity test took `48.358` seconds. It then negotiates -`SharedFileMapping` over the exact transfer-owned verified path and loads the -bundle without first materializing another process-sized byte array. It lowers -provider assets, movement, environment, four field layers, and both camera -channels. The pilot camera excludes map objects and the map camera includes -them. +Cold CDN transfer is currently failing and is not part of the passing claim. +The approximately 13 MB provider bundle still times out because its bytes are +carried through batched snapshot records instead of the intended mapped/network +body transport. Increasing the timeout would only conceal the transport error. -Cold asset acquisition and transient gameplay are deliberately separate proof -profiles. Starting Unity before the provider means download time is not a -stable lifetime for a boot-seeded enemy or pickup. The cold profile proves -delivery and lowering; `full-session-gameplay` starts a fresh warm authoritative -session and proves combat, destruction-created loot, Ymir contact collection, -and exactly-once receipts. The cold profile proves verified local file mapping -after managed network delivery. It does not claim provider-to-client network -zero-copy, shared-memory delivery, or GPU zero-copy; the content session still -copies and fragments network bytes before atomic promotion. +The passing `full-session-gameplay` witness starts from a previously verified +warm body and negotiates `SharedFileMapping` over that provider-owned path. It +proves released-package lowering and gameplay, not cold acquisition, +provider-to-client network zero-copy, shared-memory delivery, or GPU zero-copy. +Cold proof remains blocked until CultMesh transfers the bundle through the +network-body path and atomically promotes the verified result without routing +the payload through snapshot-record batching. diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 3c29c7c..772a54a 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "bb7e7fc6bbbbd87e9794212de53f4b92b3c5e785" +$expectedEveUnityCommit = "745ce7cc24c514ad442d53807722b6a4651d9521" $expectedEveFieldsCommit = "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "419053ebe2325848051c4f4d8ba352cd4286c424" From 8db5ceeac4aea8f06eb3e3ba0c9697e78442b163 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 17:23:57 +0100 Subject: [PATCH 130/292] Record ordinary-hull released witness --- docs/aetheria-released-package-witness.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index dd6ed8e..b277427 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -22,10 +22,12 @@ Current released-package evidence is in name, `runtime-witness.warm.json` is the current passing run. The warm PlayMode test passed and recorded provider-owned reconciled movement, look, targeting, tractor press, tractor release, contact-gated cargo collection, and action -receipts. Its generated player happened to be the rare `LonginusX` modified -hull with the singular `Traction` AetherDrive. The drive's authored nested -torque curve spooled its persisted rotor state and daemon-owned torque reached -the SoA body rotation; ordinary hulls remain directional-thruster ships. +receipts. The fingerprinted April 2021 catalog recovery now restores the +distinct common `Longinus`, rare `LonginusX`, and `Djinni` rows plus the missing +turn-thruster equipment needed to outfit ordinary hulls. This run generated the +player as a common directional-thruster `Longinus`; the other six presented +ships were `Djinni`, and no `LonginusX` appeared. The player moved 14.34 world +units and completed its authoritative hardware-driven look before combat. The daemon-owned look direction reached the SoA body rotation, and the generic aim presentation rendered at the advertised 50-unit minimum convergence distance. The advertised skybox material and reflection cubemap @@ -71,12 +73,12 @@ daemon lock progress `1.0`; Unity does not manufacture or smooth that value. Camera-channel facts: -- provider-authored player renderers: `12`, with no embedded shield renderer; -- pilot changed pixels: `215,425`; -- pilot average luminance: `0.124833`; -- pilot bright pixels: `96,180`; +- provider-authored player renderers: `13` total / `9` enabled, with no embedded shield renderer; +- pilot changed pixels: `217,323`; +- pilot average luminance: `0.1322155`; +- pilot bright pixels: `101,510`; - map-channel renderers: `10`; -- map changed pixels: `4,064`; +- map changed pixels: `3,179`; - native cockpit progress bars: `7`; - daemon tractor power at capture: `1.0` after the held input completed its authored ramp; From 8fb39a1e5710ad5565187bd71825ec03b1de0783 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 17:44:29 +0100 Subject: [PATCH 131/292] Unify field grid and temporal camera lowering --- docs/authority-map.md | 7 + .../org.gamecult.eve.unity-scene/README.md | 10 ++ .../Runtime/EveUnityPlayableWorldCameraRig.cs | 89 ++++++++++-- .../Runtime/EveUnitySceneSurfaceLowerer.cs | 29 +++- .../Fields/EveUnityFieldsParticleRenderer.cs | 118 +++------------- .../Fields/EveUnityFieldsViewportFrame.cs | 128 ++++++++++++++++++ .../EveUnityFieldsViewportFrame.cs.meta | 11 ++ .../Fields/EveUnityFieldsVolumeRenderer.cs | 49 +++++-- .../EveFieldsUnitySceneProjectionTests.cs | 49 ++++++- .../EveUnitySceneSurfaceLowererTests.cs | 27 +++- 10 files changed, 384 insertions(+), 133 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsViewportFrame.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsViewportFrame.cs.meta diff --git a/docs/authority-map.md b/docs/authority-map.md index 7c6d139..bc92db2 100644 --- a/docs/authority-map.md +++ b/docs/authority-map.md @@ -40,6 +40,13 @@ presentation counters, and receipt display state are projections. They are not world truth. Temporal targets reset when the volume program, node, or render size changes and can never become provider state. +Camera-relative field viewports have one generic resolver. The advertised +viewport frame owns its extent, spatial-cell scale, raster texels per cell, and +snap policy; fog volumes and stateless particle programs consume the identical +projected `EveFieldsViewport`. URP TAA state is likewise derived from the +world's neutral `temporal-reprojection.v1` camera contract and is restored on +release. + ## Forbidden Writers Unity transforms, input drivers, camera rigs, scene sinks, asset caches, and diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index 22359e7..c1c66a0 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -72,6 +72,16 @@ out of Eve's semantic surface while allowing a provider-owned shader to consume portable Eve Fields documents. Volume composition runs as a generic URP render pass before post-processing so the provider-advertised profile grades both the native scene and the provider volume through the same camera pipeline. + +Camera-relative field consumers share `viewportAnchor`, `span`, +`cellWorldSize`, `viewportSnapLayer`, and `viewportSnapTexels` as one viewport +frame contract. Volume and particle lowerers resolve that frame through the +same generic path, so a snapped stateless particle lattice cannot drift from +the field textures it samples. A `world.scene3d` may also request +`temporal-reprojection.v1`; EveUnity lowers its neutral history blend, jitter, +quality, and sharpening values to URP TAA and restores the previous camera +state when the world releases the camera. + Providers may advertise per-layer resolution scales, mipmap use, and filtering through `layerTargetDescriptors`; the Unity lowerer applies those native target requirements without assigning gameplay or product meaning to the layer keys. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index b0944e1..044b2c9 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -40,6 +40,8 @@ namespace GameCult.Eve.UnityScene private UniversalAdditionalCameraData? _postProcessCameraData; private bool _createdPostProcessCameraData; private bool _previousRenderPostProcessing; + private AntialiasingMode _previousAntialiasing; + private TemporalAA.Settings _previousTaaSettings; public EveUnityPlayableWorldClientHost? Host { @@ -307,7 +309,17 @@ namespace GameCult.Eve.UnityScene !IsFinite(world.KeyLightColorR) || !IsFinite(world.KeyLightColorG) || !IsFinite(world.KeyLightColorB) || - !IsFinite(world.KeyLightIntensity)) + !IsFinite(world.KeyLightIntensity) || + !IsFinite(world.TemporalHistoryBlend) || + !IsFinite(world.TemporalJitterScale) || + !IsFinite(world.TemporalSharpening) || + (!string.IsNullOrWhiteSpace(world.CameraReconstruction) && + !string.Equals(world.CameraReconstruction, "temporal-reprojection.v1", System.StringComparison.Ordinal)) || + (string.Equals(world.CameraReconstruction, "temporal-reprojection.v1", System.StringComparison.Ordinal) && + (world.TemporalHistoryBlend < 0f || world.TemporalHistoryBlend > 1f || + world.TemporalJitterScale < 0f || world.TemporalJitterScale > 1f || + world.TemporalSharpening < 0f || world.TemporalSharpening > 1f || + !IsTemporalQuality(world.TemporalQuality)))) return false; var assets = host.NativeAssetProvider; if (!string.IsNullOrWhiteSpace(world.SkyboxAssetRef)) @@ -453,12 +465,19 @@ namespace GameCult.Eve.UnityScene } if (environmentChanged) DynamicGI.UpdateEnvironment(); - ApplyPostProcess(camera, postProcessProfile); + ApplyCameraRendering(camera, postProcessProfile, world); } - private void ApplyPostProcess(Camera? camera, VolumeProfile? profile) + private void ApplyCameraRendering( + Camera? camera, + VolumeProfile? profile, + EveUnityPlayableWorldProjection world) { - if (profile == null) + var usesTemporalReprojection = string.Equals( + world.CameraReconstruction, + "temporal-reprojection.v1", + System.StringComparison.Ordinal); + if (profile == null && !usesTemporalReprojection) { RestorePostProcess(); return; @@ -474,19 +493,59 @@ namespace GameCult.Eve.UnityScene if (_postProcessCameraData == null) _postProcessCameraData = camera.gameObject.AddComponent(); _previousRenderPostProcessing = _postProcessCameraData.renderPostProcessing; + _previousAntialiasing = _postProcessCameraData.antialiasing; + _previousTaaSettings = _postProcessCameraData.taaSettings; } - _postProcessCameraData.renderPostProcessing = true; - if (_postProcessVolumeObject == null) + _postProcessCameraData.renderPostProcessing = profile != null || _previousRenderPostProcessing; + if (usesTemporalReprojection) { - _postProcessVolumeObject = new GameObject("Eve World Post Process"); - _postProcessVolumeObject.transform.SetParent(transform, worldPositionStays: false); - _postProcessVolume = _postProcessVolumeObject.AddComponent(); - _postProcessVolume.isGlobal = true; - _postProcessVolume.priority = 1000f; - _postProcessVolume.weight = 1f; + _postProcessCameraData.antialiasing = AntialiasingMode.TemporalAntiAliasing; + ref var settings = ref _postProcessCameraData.taaSettings; + settings = TemporalAA.Settings.Create(); + settings.quality = ParseTemporalQuality(world.TemporalQuality); + settings.baseBlendFactor = world.TemporalHistoryBlend; + settings.jitterScale = world.TemporalJitterScale; + settings.contrastAdaptiveSharpening = world.TemporalSharpening; + } + else + { + _postProcessCameraData.antialiasing = _previousAntialiasing; + _postProcessCameraData.taaSettings = _previousTaaSettings; + } + if (profile != null) + { + if (_postProcessVolumeObject == null) + { + _postProcessVolumeObject = new GameObject("Eve World Post Process"); + _postProcessVolumeObject.transform.SetParent(transform, worldPositionStays: false); + _postProcessVolume = _postProcessVolumeObject.AddComponent(); + _postProcessVolume.isGlobal = true; + _postProcessVolume.priority = 1000f; + _postProcessVolume.weight = 1f; + } + _postProcessVolume!.sharedProfile = profile; + _postProcessVolumeObject.SetActive(true); + } + else if (_postProcessVolumeObject != null) + { + _postProcessVolumeObject.SetActive(false); + } + } + + private static bool IsTemporalQuality(string quality) => + quality == "very-low" || quality == "low" || quality == "medium" || + quality == "high" || quality == "very-high"; + + private static TemporalAAQuality ParseTemporalQuality(string quality) + { + switch (quality) + { + case "very-low": return TemporalAAQuality.VeryLow; + case "low": return TemporalAAQuality.Low; + case "medium": return TemporalAAQuality.Medium; + case "very-high": return TemporalAAQuality.VeryHigh; + default: return TemporalAAQuality.High; } - _postProcessVolume!.sharedProfile = profile; - _postProcessVolumeObject.SetActive(true); } private void RestorePostProcess() @@ -503,6 +562,8 @@ namespace GameCult.Eve.UnityScene else { _postProcessCameraData.renderPostProcessing = _previousRenderPostProcessing; + _postProcessCameraData.antialiasing = _previousAntialiasing; + _postProcessCameraData.taaSettings = _previousTaaSettings; } } _postProcessCameraData = null; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index 726944c..bcc3637 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -156,7 +156,12 @@ namespace GameCult.Eve.UnityScene ParseFloat(worldRoot.GetProp("keyLightIntensity"), 0f), fieldVolumes, worldRoot.GetProp("cameraLookAt"), - fieldParticles); + fieldParticles, + worldRoot.GetProp("cameraReconstruction"), + worldRoot.GetProp("temporalQuality", "high"), + ParseFloat(worldRoot.GetProp("temporalHistoryBlend"), 0f), + ParseFloat(worldRoot.GetProp("temporalJitterScale"), 0f), + ParseFloat(worldRoot.GetProp("temporalSharpening"), 0f)); } private static EveUnityFieldVolumeProjection BuildFieldVolume(EveSurfaceComponent component) => @@ -411,7 +416,12 @@ namespace GameCult.Eve.UnityScene float keyLightIntensity = 0f, IReadOnlyList? fieldVolumes = null, string cameraLookAt = "", - IReadOnlyList? fieldParticles = null) + IReadOnlyList? fieldParticles = null, + string cameraReconstruction = "", + string temporalQuality = "high", + float temporalHistoryBlend = 0f, + float temporalJitterScale = 0f, + float temporalSharpening = 0f) { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -459,6 +469,11 @@ namespace GameCult.Eve.UnityScene FieldVolumes = fieldVolumes ?? Array.Empty(); FieldParticles = fieldParticles ?? Array.Empty(); CameraLookAt = cameraLookAt ?? ""; + CameraReconstruction = cameraReconstruction ?? ""; + TemporalQuality = temporalQuality ?? ""; + TemporalHistoryBlend = temporalHistoryBlend; + TemporalJitterScale = temporalJitterScale; + TemporalSharpening = temporalSharpening; } public string WorldRootId { get; } @@ -483,6 +498,16 @@ namespace GameCult.Eve.UnityScene public string CameraLookAt { get; } + public string CameraReconstruction { get; } + + public string TemporalQuality { get; } + + public float TemporalHistoryBlend { get; } + + public float TemporalJitterScale { get; } + + public float TemporalSharpening { get; } + public float CameraDistance { get; } public float CameraVerticalFieldOfViewDegrees { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs index 6e6eb4e..34bfb2c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs @@ -101,44 +101,28 @@ namespace GameCult.Eve.UnityScene.Fields var baseWidth = PositiveInt(field, "textureWidth", 512); var baseHeight = PositiveInt(field, "textureHeight", 512); - var viewportWidth = (float)(document.Viewport.MaxX - document.Viewport.MinX); - var viewportHeight = (float)(document.Viewport.MaxY - document.Viewport.MinY); - if (!float.IsFinite(viewportWidth) || !float.IsFinite(viewportHeight) || - viewportWidth <= 0f || viewportHeight <= 0f) - return false; - - var center = new Vector2( - (float)((document.Viewport.MinX + document.Viewport.MaxX) * 0.5), - (float)((document.Viewport.MinY + document.Viewport.MaxY) * 0.5)); - if (string.Equals(Prop(field, "viewportAnchor"), "active-camera.xz", StringComparison.Ordinal)) - { - var snapLayer = Prop(field, "viewportSnapLayer"); - var snapTarget = _targets.FirstOrDefault(target => - string.Equals(target.LayerKey, snapLayer, StringComparison.Ordinal)); - var snapWidth = snapTarget == null - ? 0 - : Mathf.Max(1, Mathf.RoundToInt(baseWidth * Mathf.Max(0.01f, snapTarget.WidthScale))); - var snapHeight = snapTarget == null - ? 0 - : Mathf.Max(1, Mathf.RoundToInt(baseHeight * Mathf.Max(0.01f, snapTarget.HeightScale))); - var snapTexels = PositiveInt(field, "viewportSnapTexels", 0); - var cellWorldSize = PositiveFloat(field, "cellWorldSize", 0f); - var span = PositiveInt(field, "span", 0); - if (!TryValidateSpatialLattice( - viewportWidth, - viewportHeight, - snapWidth, - snapHeight, - span, - cellWorldSize, - snapTexels)) return false; - center.x = SnapCameraCoordinate(camera.transform.position.x, viewportWidth, snapWidth, snapTexels); - center.y = SnapCameraCoordinate(camera.transform.position.z, viewportHeight, snapHeight, snapTexels); - } + var snapLayer = Prop(field, "viewportSnapLayer"); + var snapTarget = _targets.FirstOrDefault(target => + string.Equals(target.LayerKey, snapLayer, StringComparison.Ordinal)); + var snapWidth = snapTarget == null + ? 0 + : Mathf.Max(1, Mathf.RoundToInt(baseWidth * Mathf.Max(0.01f, snapTarget.WidthScale))); + var snapHeight = snapTarget == null + ? 0 + : Mathf.Max(1, Mathf.RoundToInt(baseHeight * Mathf.Max(0.01f, snapTarget.HeightScale))); + if (!EveUnityFieldsViewportFrame.TryResolve( + document, + field.Props, + new Vector2(camera.transform.position.x, camera.transform.position.z), + snapWidth, + snapHeight, + out var projected, + out var center)) return false; + var viewportWidth = (float)(projected.Viewport.MaxX - projected.Viewport.MinX); + var viewportHeight = (float)(projected.Viewport.MaxY - projected.Viewport.MinY); if (_rasterizedFrameId != document.FrameId || _rasterizedCenter != center) { - var projected = ProjectViewport(document, center, viewportWidth, viewportHeight); _layers.Render(projected, _targets, baseWidth, baseHeight); _rasterizedFrameId = document.FrameId; _rasterizedCenter = center; @@ -156,70 +140,6 @@ namespace GameCult.Eve.UnityScene.Fields return true; } - public static float SnapCameraCoordinate( - float coordinate, - float viewportWidth, - int snapTargetWidth, - float snapTexels) - { - if (!float.IsFinite(coordinate) || !float.IsFinite(viewportWidth) || - !float.IsFinite(snapTexels) || viewportWidth <= 0f || - snapTargetWidth <= 0 || snapTexels <= 0f) - throw new ArgumentOutOfRangeException(nameof(viewportWidth)); - var snapLength = viewportWidth * snapTexels / snapTargetWidth; - var cell = (int)(coordinate / snapLength); - return cell * snapLength; - } - - public static bool TryValidateSpatialLattice( - float viewportWidth, - float viewportHeight, - int gravityTextureWidth, - int gravityTextureHeight, - int span, - float cellWorldSize, - int gravityTexelsPerCell) - { - if (!float.IsFinite(viewportWidth) || !float.IsFinite(viewportHeight) || - !float.IsFinite(cellWorldSize) || viewportWidth <= 0f || viewportHeight <= 0f || - gravityTextureWidth <= 0 || gravityTextureHeight <= 0 || span <= 0 || - cellWorldSize <= 0f || gravityTexelsPerCell <= 0) - return false; - var expectedExtent = span * cellWorldSize; - var expectedCellFromX = viewportWidth * gravityTexelsPerCell / gravityTextureWidth; - var expectedCellFromY = viewportHeight * gravityTexelsPerCell / gravityTextureHeight; - const float tolerance = 0.0001f; - return Mathf.Abs(viewportWidth - expectedExtent) <= tolerance && - Mathf.Abs(viewportHeight - expectedExtent) <= tolerance && - Mathf.Abs(expectedCellFromX - cellWorldSize) <= tolerance && - Mathf.Abs(expectedCellFromY - cellWorldSize) <= tolerance; - } - - private static EveFieldsSplatsDocument ProjectViewport( - EveFieldsSplatsDocument source, - Vector2 center, - float width, - float height) => - new EveFieldsSplatsDocument - { - Schema = source.Schema, - FrameId = source.FrameId, - PublishedAtUtc = source.PublishedAtUtc, - SimulationTimeSeconds = source.SimulationTimeSeconds, - RunId = source.RunId, - ZoneIndex = source.ZoneIndex, - ZoneName = source.ZoneName, - Viewport = new EveFieldsViewport - { - MinX = center.x - width * 0.5, - MinY = center.y - height * 0.5, - MaxX = center.x + width * 0.5, - MaxY = center.y + height * 0.5 - }, - Layers = source.Layers, - Splats = source.Splats - }; - private bool EnsurePrograms(EveUnityFieldParticlesProjection field) { var provider = _host?.NativeAssetProvider; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsViewportFrame.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsViewportFrame.cs new file mode 100644 index 0000000..608a02c --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsViewportFrame.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using GameCult.Eve.PluginFields; +using UnityEngine; + +#nullable enable + +namespace GameCult.Eve.UnityScene.Fields +{ + public static class EveUnityFieldsViewportFrame + { + public static bool TryResolve( + EveFieldsSplatsDocument source, + IReadOnlyDictionary props, + Vector2 cameraPosition, + int snapTextureWidth, + int snapTextureHeight, + out EveFieldsSplatsDocument projected, + out Vector2 center) + { + projected = source; + center = default; + if (source == null || source.Viewport == null) return false; + + var width = (float)(source.Viewport.MaxX - source.Viewport.MinX); + var height = (float)(source.Viewport.MaxY - source.Viewport.MinY); + if (!float.IsFinite(width) || !float.IsFinite(height) || width <= 0f || height <= 0f) + return false; + + center = new Vector2( + (float)((source.Viewport.MinX + source.Viewport.MaxX) * 0.5), + (float)((source.Viewport.MinY + source.Viewport.MaxY) * 0.5)); + if (!string.Equals(Prop(props, "viewportAnchor"), "active-camera.xz", StringComparison.Ordinal)) + return true; + + var span = PositiveInt(props, "span"); + var cellWorldSize = PositiveFloat(props, "cellWorldSize"); + var snapTexels = PositiveInt(props, "viewportSnapTexels"); + if (!TryValidateSpatialLattice( + width, height, snapTextureWidth, snapTextureHeight, + span, cellWorldSize, snapTexels)) + return false; + + center.x = SnapCameraCoordinate(cameraPosition.x, width, snapTextureWidth, snapTexels); + center.y = SnapCameraCoordinate(cameraPosition.y, height, snapTextureHeight, snapTexels); + projected = Project(source, center, width, height); + return true; + } + + public static float SnapCameraCoordinate( + float coordinate, + float viewportWidth, + int snapTargetWidth, + float snapTexels) + { + if (!float.IsFinite(coordinate) || !float.IsFinite(viewportWidth) || + !float.IsFinite(snapTexels) || viewportWidth <= 0f || + snapTargetWidth <= 0 || snapTexels <= 0f) + throw new ArgumentOutOfRangeException(nameof(viewportWidth)); + var snapLength = viewportWidth * snapTexels / snapTargetWidth; + return (int)(coordinate / snapLength) * snapLength; + } + + public static bool TryValidateSpatialLattice( + float viewportWidth, + float viewportHeight, + int gravityTextureWidth, + int gravityTextureHeight, + int span, + float cellWorldSize, + int gravityTexelsPerCell) + { + if (!float.IsFinite(viewportWidth) || !float.IsFinite(viewportHeight) || + !float.IsFinite(cellWorldSize) || viewportWidth <= 0f || viewportHeight <= 0f || + gravityTextureWidth <= 0 || gravityTextureHeight <= 0 || span <= 0 || + cellWorldSize <= 0f || gravityTexelsPerCell <= 0) + return false; + var expectedExtent = span * cellWorldSize; + var expectedCellFromX = viewportWidth * gravityTexelsPerCell / gravityTextureWidth; + var expectedCellFromY = viewportHeight * gravityTexelsPerCell / gravityTextureHeight; + const float tolerance = 0.0001f; + return Mathf.Abs(viewportWidth - expectedExtent) <= tolerance && + Mathf.Abs(viewportHeight - expectedExtent) <= tolerance && + Mathf.Abs(expectedCellFromX - cellWorldSize) <= tolerance && + Mathf.Abs(expectedCellFromY - cellWorldSize) <= tolerance; + } + + private static EveFieldsSplatsDocument Project( + EveFieldsSplatsDocument source, + Vector2 center, + float width, + float height) => + new EveFieldsSplatsDocument + { + Schema = source.Schema, + FrameId = source.FrameId, + PublishedAtUtc = source.PublishedAtUtc, + SimulationTimeSeconds = source.SimulationTimeSeconds, + RunId = source.RunId, + ZoneIndex = source.ZoneIndex, + ZoneName = source.ZoneName, + Viewport = new EveFieldsViewport + { + MinX = center.x - width * 0.5, + MinY = center.y - height * 0.5, + MaxX = center.x + width * 0.5, + MaxY = center.y + height * 0.5 + }, + Layers = source.Layers, + Splats = source.Splats + }; + + private static string Prop(IReadOnlyDictionary props, string key) => + props != null && props.TryGetValue(key, out var value) ? value ?? "" : ""; + + private static int PositiveInt(IReadOnlyDictionary props, string key) => + int.TryParse(Prop(props, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && value > 0 + ? value + : 0; + + private static float PositiveFloat(IReadOnlyDictionary props, string key) => + float.TryParse(Prop(props, key), NumberStyles.Float, CultureInfo.InvariantCulture, out var value) && + float.IsFinite(value) && value > 0f + ? value + : 0f; + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsViewportFrame.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsViewportFrame.cs.meta new file mode 100644 index 0000000..9ad030b --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsViewportFrame.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e9db43b5b96144a3a0df57a791b53547 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs index a132030..f4e823a 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -30,11 +30,14 @@ namespace GameCult.Eve.UnityScene.Fields private Matrix4x4 _previousViewProjection; private EveUnityFieldsVolumePass? _renderPass; private string _activeNodeId = ""; + private long _rasterizedFrameId = -1; + private Vector2 _rasterizedCenter = new Vector2(float.NaN, float.NaN); public long PresentedFrameId { get; private set; } = -1; public int PresentedLayerCount => _targets.Count(target => target.TargetTexture != null); public long CompositeCount { get; private set; } public bool UsesTemporalHistory => HasTemporalProgram(); + public Vector2 LastGridCenter { get; private set; } public static RenderPassEvent CompositionRenderPassEvent => RenderPassEvent.BeforeRenderingPostProcessing; public void Bind(EveUnityPlayableWorldClientHost host, IEveUnityFieldsSplatsDocumentSource? source) @@ -67,20 +70,47 @@ namespace GameCult.Eve.UnityScene.Fields if (document == null || document.FrameId < PresentedFrameId) return; _document = document; PresentedFrameId = document.FrameId; - RenderLayers(document); } - private void RenderLayers(EveFieldsSplatsDocument document) + private bool TryRenderLayers( + EveFieldsSplatsDocument document, + Camera camera, + out EveFieldsSplatsDocument projected) { + projected = document; var field = ActiveField(); - if (field == null) return; + if (field == null) return false; EnsureLayerRenderer(); EnsureTargets(field); var width = PositiveInt(field, "textureWidth", 512); var height = PositiveInt(field, "textureHeight", 512); - _layers!.Render(document, _targets, width, height); - if (!EnsureMaterial(field)) return; - ApplyFieldBindings(field, document); + var snapLayer = Prop(field, "viewportSnapLayer"); + var snapTarget = _targets.FirstOrDefault(target => + string.Equals(target.LayerKey, snapLayer, StringComparison.Ordinal)); + var snapWidth = snapTarget == null + ? 0 + : Mathf.Max(1, Mathf.RoundToInt(width * Mathf.Max(0.01f, snapTarget.WidthScale))); + var snapHeight = snapTarget == null + ? 0 + : Mathf.Max(1, Mathf.RoundToInt(height * Mathf.Max(0.01f, snapTarget.HeightScale))); + if (!EveUnityFieldsViewportFrame.TryResolve( + document, + field.Props, + new Vector2(camera.transform.position.x, camera.transform.position.z), + snapWidth, + snapHeight, + out projected, + out var center)) return false; + if (_rasterizedFrameId != document.FrameId || _rasterizedCenter != center) + { + _layers!.Render(projected, _targets, width, height); + _rasterizedFrameId = document.FrameId; + _rasterizedCenter = center; + } + LastGridCenter = center; + if (!EnsureMaterial(field)) return false; + ApplyFieldBindings(field, projected); + return true; } private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera) @@ -101,10 +131,8 @@ namespace GameCult.Eve.UnityScene.Fields if (_host == null || _document == null || camera == null || _host.ActiveCameraTransform != camera.transform) return; - if (PresentedLayerCount == 0) RenderLayers(_document); var field = ActiveField(); - if (field == null || !EnsureMaterial(field)) return; - ApplyFieldBindings(field, _document); + if (field == null || !TryRenderLayers(_document, camera, out _)) return; camera.depthTextureMode |= DepthTextureMode.Depth; EnsureVolumeTextures(camera, field); if (_raymarchTexture == null || !ApplyViewportTextureScaleBindings(field, camera)) return; @@ -369,6 +397,9 @@ namespace GameCult.Eve.UnityScene.Fields _programMetadata = null; _assetTexturesByPort.Clear(); _activeNodeId = ""; + _rasterizedFrameId = -1; + _rasterizedCenter = new Vector2(float.NaN, float.NaN); + LastGridCenter = default; PresentedFrameId = -1; CompositeCount = 0; ResetTemporalHistory(); diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index bcd1fa3..5f5f4e2 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -62,9 +62,9 @@ namespace GameCult.Eve.UnityScene.Tests [TestCase(11f, 6f)] [TestCase(-11f, -6f)] [TestCase(5.999f, 0f)] - public void ParticleRendererSnapsToAnIntegralGravityPixelMultiple(float coordinate, float expected) + public void FieldViewportFrameSnapsToAnIntegralGravityPixelMultiple(float coordinate, float expected) { - Assert.That(EveUnityFieldsParticleRenderer.SnapCameraCoordinate( + Assert.That(EveUnityFieldsViewportFrame.SnapCameraCoordinate( coordinate, 1536f, 2048, @@ -72,19 +72,56 @@ namespace GameCult.Eve.UnityScene.Tests } [Test] - public void ParticleRendererAcceptsAlignedGravityAndParticleLattices() + public void FieldViewportFrameAcceptsAlignedGravityAndParticleLattices() { - Assert.That(EveUnityFieldsParticleRenderer.TryValidateSpatialLattice( + Assert.That(EveUnityFieldsViewportFrame.TryValidateSpatialLattice( 1536f, 1536f, 2048, 2048, 256, 6f, 8), Is.True); } [Test] - public void ParticleRendererRejectsAliasedGravityAndParticleLattices() + public void FieldViewportFrameRejectsAliasedGravityAndParticleLattices() { - Assert.That(EveUnityFieldsParticleRenderer.TryValidateSpatialLattice( + Assert.That(EveUnityFieldsViewportFrame.TryValidateSpatialLattice( 2000f, 2000f, 2048, 2048, 256, 6f, 8), Is.False); } + [Test] + public void FogAndParticlesResolveTheSameSnappedWorldFrame() + { + var document = new EveFieldsSplatsDocument + { + FrameId = 7, + Viewport = new EveFieldsViewport + { + MinX = -768, + MinY = -768, + MaxX = 768, + MaxY = 768 + } + }; + var contract = new Dictionary(StringComparer.Ordinal) + { + ["viewportAnchor"] = "active-camera.xz", + ["span"] = "256", + ["cellWorldSize"] = "6", + ["viewportSnapTexels"] = "8" + }; + + Assert.That(EveUnityFieldsViewportFrame.TryResolve( + document, contract, new Vector2(11f, -11f), 2048, 2048, + out var fog, out var fogCenter), Is.True); + Assert.That(EveUnityFieldsViewportFrame.TryResolve( + document, contract, new Vector2(11f, -11f), 2048, 2048, + out var particles, out var particleCenter), Is.True); + + Assert.That(fogCenter, Is.EqualTo(new Vector2(6f, -6f))); + Assert.That(particleCenter, Is.EqualTo(fogCenter)); + Assert.That(particles.Viewport.MinX, Is.EqualTo(fog.Viewport.MinX)); + Assert.That(particles.Viewport.MinY, Is.EqualTo(fog.Viewport.MinY)); + Assert.That(particles.Viewport.MaxX, Is.EqualTo(fog.Viewport.MaxX)); + Assert.That(particles.Viewport.MaxY, Is.EqualTo(fog.Viewport.MaxY)); + } + [Test] public void ParticleRendererFailsClosedWithoutCompleteNativeAbi() { diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 319e1d0..c716cd1 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -1154,7 +1154,12 @@ namespace GameCult.Eve.UnityScene.Tests PlayableArpgDocument( skyboxAssetRef: "material.environment.skybox", reflectionAssetRef: "texture.environment.reflection", - postProcessProfileAssetRef: "profile.environment.flight"), + postProcessProfileAssetRef: "profile.environment.flight", + cameraReconstruction: "temporal-reprojection.v1", + temporalQuality: "high", + temporalHistoryBlend: "0.99", + temporalJitterScale: "0.1", + temporalSharpening: "0"), Advertisement("aetheria.daemon.game"), "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", 1), @@ -1205,7 +1210,13 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(RenderSettings.reflectionIntensity, Is.EqualTo(1f).Within(0.001f)); Assert.That(camera.clearFlags, Is.EqualTo(CameraClearFlags.Skybox)); Assert.That(camera.GetComponent(), Is.Not.Null); - Assert.That(camera.GetComponent().renderPostProcessing, Is.True); + var cameraData = camera.GetComponent(); + Assert.That(cameraData.renderPostProcessing, Is.True); + Assert.That(cameraData.antialiasing, Is.EqualTo(AntialiasingMode.TemporalAntiAliasing)); + Assert.That(cameraData.taaSettings.quality, Is.EqualTo(TemporalAAQuality.High)); + Assert.That(cameraData.taaSettings.baseBlendFactor, Is.EqualTo(0.99f).Within(0.0001f)); + Assert.That(cameraData.taaSettings.jitterScale, Is.EqualTo(0.1f).Within(0.0001f)); + Assert.That(cameraData.taaSettings.contrastAdaptiveSharpening, Is.Zero); var postProcessTransform = hostObject.transform.Find("Eve World Post Process"); Assert.That(postProcessTransform, Is.Not.Null); Assert.That(postProcessTransform.GetComponent().sharedProfile, Is.SameAs(postProcessProfile)); @@ -1894,7 +1905,12 @@ namespace GameCult.Eve.UnityScene.Tests string cameraPositionDamping = "5", string cameraNearClipPlane = "0.3", string cameraFarClipPlane = "4096", - string cameraLookAt = "") + string cameraLookAt = "", + string cameraReconstruction = "", + string temporalQuality = "high", + string temporalHistoryBlend = "0", + string temporalJitterScale = "0", + string temporalSharpening = "0") { var playableChildren = new List { @@ -2062,6 +2078,11 @@ namespace GameCult.Eve.UnityScene.Tests ["reflectionAssetRef"] = reflectionAssetRef, ["reflectionIntensity"] = "1", ["postProcessProfileAssetRef"] = postProcessProfileAssetRef, + ["cameraReconstruction"] = cameraReconstruction, + ["temporalQuality"] = temporalQuality, + ["temporalHistoryBlend"] = temporalHistoryBlend, + ["temporalJitterScale"] = temporalJitterScale, + ["temporalSharpening"] = temporalSharpening, ["keyLightDirection"] = "0.4,-1,0.25", ["keyLightColor"] = "1,0.95,0.9", ["keyLightIntensity"] = "0.75", From dfa843cde2c0d9328bb2fa342c9378bf3cdfe6f4 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 17:44:45 +0100 Subject: [PATCH 132/292] Release EveUnity scene 0.3.56 --- packages/org.gamecult.eve.unity-scene/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 1b1aabc..1d5d0f0 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.55", + "version": "0.3.56", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 2ff86e46eefe7b56428826cc834c89cb906c998b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 17:53:40 +0100 Subject: [PATCH 133/292] Prove released temporal field lowering --- ...eCult.EveUnity.GenericClient.PlayModeTests.asmdef | 3 ++- .../Tests/PlayMode/GenericWorldCaptureTests.cs | 8 ++++++++ ReleaseConsumerProject/Packages/manifest.json | 2 +- ReleaseConsumerProject/Packages/packages-lock.json | 4 ++-- ...eCult.EveUnity.GenericClient.PlayModeTests.asmdef | 3 ++- .../Tests/PlayMode/GenericWorldCaptureTests.cs | 8 ++++++++ docs/aetheria-released-package-witness.md | 12 +++++++++++- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 8 files changed, 35 insertions(+), 7 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef b/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef index 4eadab4..d387468 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef @@ -6,7 +6,8 @@ "GameCult.Eve.Surface", "GameCult.Eve.UnityUIToolkit", "GameCult.Eve.UnityScene", - "GameCult.Eve.UnityScene.CultMesh" + "GameCult.Eve.UnityScene.CultMesh", + "Unity.RenderPipelines.Universal.Runtime" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index f43d2f8..206c913 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -12,6 +12,7 @@ using GameCult.Eve.UnityScene.Fields; using GameCult.Mesh; using NUnit.Framework; using UnityEngine; +using UnityEngine.Rendering.Universal; using UnityEngine.TestTools; using UnityEngine.UIElements; @@ -811,6 +812,13 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(fieldParticles.ParticleCount, Is.EqualTo(65536)); Assert.That(fieldParticles.DispatchCount, Is.GreaterThan(0)); Assert.That(fieldParticles.DrawCount, Is.GreaterThan(0)); + Assert.That(fieldVolume.LastGridCenter, Is.EqualTo(fieldParticles.LastGridCenter), + "Fog and stateless particles were lowered against different world-grid frames."); + var pilotCameraData = camera.GetUniversalAdditionalCameraData(); + Assert.That(pilotCameraData.antialiasing, Is.EqualTo(AntialiasingMode.TemporalAntiAliasing), + "The advertised temporal reconstruction contract did not reach the pilot camera."); + Assert.That(pilotCameraData.taaSettings.baseBlendFactor, Is.EqualTo(0.99f).Within(0.0001f)); + Assert.That(pilotCameraData.taaSettings.jitterScale, Is.EqualTo(0.1f).Within(0.0001f)); var fieldDocument = typeof(EveUnityFieldsVolumeRenderer) .GetField("_document", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) ?.GetValue(fieldVolume); diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 3828f0f..93d9bc4 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.15", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.55", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.56", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 07e9785..34af8c4 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.55", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.56", "depth": 0, "source": "git", "dependencies": { @@ -155,7 +155,7 @@ "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "745ce7cc24c514ad442d53807722b6a4651d9521" + "hash": "dfa843cde2c0d9328bb2fa342c9378bf3cdfe6f4" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/TestProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef b/TestProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef index 4eadab4..d387468 100644 --- a/TestProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef +++ b/TestProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef @@ -6,7 +6,8 @@ "GameCult.Eve.Surface", "GameCult.Eve.UnityUIToolkit", "GameCult.Eve.UnityScene", - "GameCult.Eve.UnityScene.CultMesh" + "GameCult.Eve.UnityScene.CultMesh", + "Unity.RenderPipelines.Universal.Runtime" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index f43d2f8..206c913 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -12,6 +12,7 @@ using GameCult.Eve.UnityScene.Fields; using GameCult.Mesh; using NUnit.Framework; using UnityEngine; +using UnityEngine.Rendering.Universal; using UnityEngine.TestTools; using UnityEngine.UIElements; @@ -811,6 +812,13 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(fieldParticles.ParticleCount, Is.EqualTo(65536)); Assert.That(fieldParticles.DispatchCount, Is.GreaterThan(0)); Assert.That(fieldParticles.DrawCount, Is.GreaterThan(0)); + Assert.That(fieldVolume.LastGridCenter, Is.EqualTo(fieldParticles.LastGridCenter), + "Fog and stateless particles were lowered against different world-grid frames."); + var pilotCameraData = camera.GetUniversalAdditionalCameraData(); + Assert.That(pilotCameraData.antialiasing, Is.EqualTo(AntialiasingMode.TemporalAntiAliasing), + "The advertised temporal reconstruction contract did not reach the pilot camera."); + Assert.That(pilotCameraData.taaSettings.baseBlendFactor, Is.EqualTo(0.99f).Within(0.0001f)); + Assert.That(pilotCameraData.taaSettings.jitterScale, Is.EqualTo(0.1f).Within(0.0001f)); var fieldDocument = typeof(EveUnityFieldsVolumeRenderer) .GetField("_document", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) ?.GetValue(fieldVolume); diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index b277427..6c288f0 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -118,7 +118,7 @@ present in the map-only frame. This is transport, authority, field-production, and lowering proof, not visual parity: the current pilot capture contains the blue fog field and provider geometry, but nearby ships remain too dark and the celestial composition lacks the fossil capture's readable close body. Release -`0.3.55` includes generic lowering for Eve Fields `0.2.3`'s +`0.3.56` includes generic lowering for Eve Fields `0.2.3`'s `AnimatedRadialCosine` source alongside `PowerPulse`, dither-scale, temporal-history, and finite-look-at semantics. Aetheria publishes the fossil global animated simplex/cellular/ambient producer stack, body-owned radial wave @@ -131,6 +131,16 @@ presentation/composition or provider art calibration rather than missing state delivery. Color, lighting, patch structure, readable world geometry, and gravity-wave visual parity remain open. EveUnity must not compensate with Aetheria-specific lighting or fog rules. + +The `0.3.56` run also closes two camera-contract omissions. Aetheria advertises +the fossil pilot camera's neutral temporal-reprojection values; the released +generic lowerer applies URP TAA with `0.99` history blend, `0.1` jitter scale, +high quality, and zero sharpening. Fog and Stardust now consume one generic +camera-relative viewport-frame resolver rather than projecting the same splat +document independently. The live witness fails unless their snapped grid +centers are identical; this run resolved both to `(-6,-6)` after movement. +The capture is still visibly rough and is not a visual-parity claim. + Substance is not part of this path; later texture baking belongs in Blender. Aetheria currently bundles ambientCG's 1K `Metal012` color, normal, and metalness maps under CC0. The same pre-generated maps now replace dead diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 772a54a..2e47ca3 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "745ce7cc24c514ad442d53807722b6a4651d9521" +$expectedEveUnityCommit = "dfa843cde2c0d9328bb2fa342c9378bf3cdfe6f4" $expectedEveFieldsCommit = "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "419053ebe2325848051c4f4d8ba352cd4286c424" From 6ed222542d41c302a1fe2ab8c80f122212cfaa3f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 18:10:33 +0100 Subject: [PATCH 134/292] Lower provider particle material frame inputs --- docs/authority-map.md | 6 ++ .../org.gamecult.eve.unity-scene/README.md | 6 ++ .../Fields/EveUnityFieldsParticleRenderer.cs | 78 ++++++++++++++++++- .../EveFieldsUnitySceneProjectionTests.cs | 40 ++++++++++ 4 files changed, 129 insertions(+), 1 deletion(-) diff --git a/docs/authority-map.md b/docs/authority-map.md index bc92db2..ac00b6d 100644 --- a/docs/authority-map.md +++ b/docs/authority-map.md @@ -47,6 +47,12 @@ projected `EveFieldsViewport`. URP TAA state is likewise derived from the world's neutral `temporal-reprojection.v1` camera contract and is restored on release. +Particle materials may consume advertised presentation-only inputs through +logical native metadata: provider-owned textures, viewport-to-texture scale, +and the current render-frame index. The runtime frame index owns temporal +coverage only; it cannot drive particle identity, phase, position, or any +daemon-visible state. + ## Forbidden Writers Unity transforms, input drivers, camera rigs, scene sinks, asset caches, and diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index c1c66a0..40f8f0b 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -82,6 +82,12 @@ the field textures it samples. A `world.scene3d` may also request quality, and sharpening values to URP TAA and restores the previous camera state when the world releases the camera. +`field.particles3d` may advertise provider material textures, viewport-relative +texture scales, and a logical render-frame index port. These are presentation +inputs: EveUnity binds the selected provider assets and current display frame +without assigning product meaning to the shader. Stateless simulation time and +particle positions remain provider-document inputs. + Providers may advertise per-layer resolution scales, mipmap use, and filtering through `layerTargetDescriptors`; the Unity lowerer applies those native target requirements without assigning gameplay or product meaning to the layer keys. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs index 34bfb2c..ca43dc5 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs @@ -27,6 +27,7 @@ namespace GameCult.Eve.UnityScene.Fields private IReadOnlyDictionary? _computeMetadata; private IReadOnlyDictionary? _materialMetadata; private readonly Dictionary _assetTextures = new Dictionary(StringComparer.Ordinal); + private readonly Dictionary _materialAssetTextures = new Dictionary(StringComparer.Ordinal); private ComputeBuffer? _particles; private ComputeBuffer? _quadPoints; private EveUnityFieldsParticlePass? _renderPass; @@ -170,11 +171,22 @@ namespace GameCult.Eve.UnityScene.Fields if (texture == null) return false; resolvedTextures[binding.Value] = texture; } + var resolvedMaterialTextures = new Dictionary(StringComparer.Ordinal); + foreach (var binding in ParseBindings(Prop(field, "materialAssetTextureBindings"))) + { + var texture = provider.ResolveAsset( + new EveUnityPlayableWorldAssetBinding(binding.Key, "", "provider-asset-ref"), + typeof(Texture)) as Texture; + if (texture == null) return false; + resolvedMaterialTextures[binding.Value] = texture; + } if (ReferenceEquals(compute, _computeSource) && ReferenceEquals(material, _materialSource) && string.Equals(field.NodeId, _activeNodeId, StringComparison.Ordinal) && ProgramReady && resolvedTextures.Count == _assetTextures.Count && resolvedTextures.All(binding => - _assetTextures.TryGetValue(binding.Key, out var current) && ReferenceEquals(current, binding.Value))) + _assetTextures.TryGetValue(binding.Key, out var current) && ReferenceEquals(current, binding.Value)) && + resolvedMaterialTextures.Count == _materialAssetTextures.Count && resolvedMaterialTextures.All(binding => + _materialAssetTextures.TryGetValue(binding.Key, out var current) && ReferenceEquals(current, binding.Value))) return true; ReleaseProgramsAndBuffers(); @@ -187,6 +199,12 @@ namespace GameCult.Eve.UnityScene.Fields _material = new Material(material) { hideFlags = HideFlags.DontSave }; _assetTextures.Clear(); foreach (var binding in resolvedTextures) _assetTextures[binding.Key] = binding.Value; + _materialAssetTextures.Clear(); + foreach (var binding in resolvedMaterialTextures) + { + _materialAssetTextures[binding.Key] = binding.Value; + SetMaterialTexture(binding.Key, binding.Value); + } foreach (var feature in Prop(field, "features").Split(';')) { var logical = feature.Trim(); @@ -259,6 +277,7 @@ namespace GameCult.Eve.UnityScene.Fields if (!HasPort(computeMetadata, "texturePort", binding.Value)) return false; foreach (var binding in ParseBindingPairs(Prop(field, "assetTextureBindings"))) if (!HasPort(computeMetadata, "texturePort", binding.Value)) return false; + if (!TryValidateMaterialPresentationContract(field, materialMetadata, out _)) return false; foreach (var parameter in ParseBindingPairs(Prop(field, "floatParameters"))) if (!HasPort(computeMetadata, "floatPort", parameter.Key) || !TryFloat(parameter.Value, out _)) return false; @@ -293,6 +312,30 @@ namespace GameCult.Eve.UnityScene.Fields PositiveInt(field, "textureHeight", 0) > 0; } + public static bool TryValidateMaterialPresentationContract( + EveUnityFieldParticlesProjection field, + IReadOnlyDictionary? materialMetadata, + out string error) + { + if (field == null || materialMetadata == null) + return Fail("Field-particle material presentation contract is missing.", out error); + var textures = ParseBindings(Prop(field, "materialAssetTextureBindings")); + foreach (var binding in textures) + if (!HasPort(materialMetadata, "texturePort", binding.Value)) + return Fail("Field-particle material texture binding has no native logical port.", out error); + foreach (var binding in ParseBindingPairs(Prop(field, "materialViewportTextureScaleBindings"))) + { + if (!HasPort(materialMetadata, "vectorPort", binding.Key) || + !textures.Values.Contains(binding.Value)) + return Fail("Field-particle viewport texture-scale binding is incomplete.", out error); + } + var framePort = Prop(field, "materialRenderFrameIndexPort"); + if (!string.IsNullOrWhiteSpace(framePort) && !HasPort(materialMetadata, "intPort", framePort)) + return Fail("Field-particle render-frame binding has no native logical port.", out error); + error = ""; + return true; + } + private static bool HasPort( IReadOnlyDictionary metadata, string kind, @@ -367,6 +410,20 @@ namespace GameCult.Eve.UnityScene.Fields TextureHandle depthTarget) { if (_host == null || _host.ActiveCameraTransform != camera.transform || !ProgramReady) return; + var field = ActiveField(); + if (field == null) return; + foreach (var binding in ParseBindings(Prop(field, "materialViewportTextureScaleBindings"))) + { + if (!_materialAssetTextures.TryGetValue(binding.Value, out var texture) || + texture.width <= 0 || texture.height <= 0) return; + SetMaterialVector(binding.Key, new Vector4( + (float)Math.Max(1, camera.pixelWidth) / texture.width, + (float)Math.Max(1, camera.pixelHeight) / texture.height, + 0f, + 0f)); + } + var framePort = Prop(field, "materialRenderFrameIndexPort"); + if (!string.IsNullOrWhiteSpace(framePort)) SetMaterialInt(framePort, Time.frameCount); if (depthTarget.IsValid()) commands.SetRenderTarget(colorTarget, depthTarget); else commands.SetRenderTarget(colorTarget); commands.SetViewport(camera.pixelRect); @@ -471,6 +528,24 @@ namespace GameCult.Eve.UnityScene.Fields _material!.SetBuffer(property, buffer); } + private void SetMaterialTexture(string logical, Texture texture) + { + if (TryMaterialProgramValue($"unity.particles.texturePort.{logical}", out var property)) + _material!.SetTexture(property, texture); + } + + private void SetMaterialVector(string logical, Vector4 value) + { + if (TryMaterialProgramValue($"unity.particles.vectorPort.{logical}", out var property)) + _material!.SetVector(property, value); + } + + private void SetMaterialInt(string logical, int value) + { + if (TryMaterialProgramValue($"unity.particles.intPort.{logical}", out var property)) + _material!.SetInt(property, value); + } + private void SetComputeFloat(string logical, float value) { if (TryComputeProgramValue($"unity.particles.floatPort.{logical}", out var property)) @@ -533,6 +608,7 @@ namespace GameCult.Eve.UnityScene.Fields _computeMetadata = null; _materialMetadata = null; _assetTextures.Clear(); + _materialAssetTextures.Clear(); _activeNodeId = ""; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index 5f5f4e2..a8349a4 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -137,6 +137,46 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(error, Does.Contain("logical port")); } + [Test] + public void ParticleMaterialAcceptsAdvertisedTemporalDitherInputs() + { + var field = new EveUnityFieldParticlesProjection( + "particles", "document", EveFieldsSchemas.Splats, "compute", "material", "world.transparent", + new Dictionary(StringComparer.Ordinal) + { + ["materialAssetTextureBindings"] = "texture.noise=dither", + ["materialViewportTextureScaleBindings"] = "ditherCoordinates=dither", + ["materialRenderFrameIndexPort"] = "frameIndex" + }); + var metadata = RequiredParticleMaterialMetadata(); + metadata["unity.particles.texturePort.dither"] = "_DitheringTex"; + metadata["unity.particles.vectorPort.ditherCoordinates"] = "_DitheringCoords"; + metadata["unity.particles.intPort.frameIndex"] = "_FrameNumber"; + + Assert.That(EveUnityFieldsParticleRenderer.TryValidateMaterialPresentationContract( + field, metadata, out var error), Is.True, error); + } + + [Test] + public void ParticleMaterialRejectsPartialTemporalDitherInputs() + { + var field = new EveUnityFieldParticlesProjection( + "particles", "document", EveFieldsSchemas.Splats, "compute", "material", "world.transparent", + new Dictionary(StringComparer.Ordinal) + { + ["materialAssetTextureBindings"] = "texture.noise=dither", + ["materialViewportTextureScaleBindings"] = "ditherCoordinates=dither", + ["materialRenderFrameIndexPort"] = "frameIndex" + }); + var metadata = RequiredParticleMaterialMetadata(); + metadata["unity.particles.texturePort.dither"] = "_DitheringTex"; + metadata["unity.particles.vectorPort.ditherCoordinates"] = "_DitheringCoords"; + + Assert.That(EveUnityFieldsParticleRenderer.TryValidateMaterialPresentationContract( + field, metadata, out var error), Is.False); + Assert.That(error, Does.Contain("render-frame")); + } + [Test] public void ParticleRendererPreservesRepeatedDocumentSourcesForDistinctPorts() { From 0974154fa7c7982c9a8ea0ca006d2698a53e1235 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 18:10:33 +0100 Subject: [PATCH 135/292] Release EveUnity scene 0.3.57 --- packages/org.gamecult.eve.unity-scene/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 1d5d0f0..64b70e2 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.56", + "version": "0.3.57", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 79c4332c6acb1df083ae851b0b8e2758c7522aa1 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 18:17:02 +0100 Subject: [PATCH 136/292] Prove released Stardust temporal inputs --- .../Tests/PlayMode/GenericWorldCaptureTests.cs | 16 ++++++++++++++++ ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 ++-- .../Tests/PlayMode/GenericWorldCaptureTests.cs | 16 ++++++++++++++++ docs/aetheria-released-package-witness.md | 14 ++++++++++++-- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 6 files changed, 48 insertions(+), 6 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 206c913..cfed0c4 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -808,6 +808,22 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests particleAbiError); Assert.That(fieldParticles.ProgramReady, Is.True, "The generic field-particle lowerer rejected the advertised provider ABI or assets."); + var liveParticleMaterial = typeof(EveUnityFieldsParticleRenderer) + .GetField("_material", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) + ?.GetValue(fieldParticles) as Material; + Assert.That(liveParticleMaterial, Is.Not.Null); + var ditherTextureProperty = particleMaterialMetadata["unity.particles.texturePort.dither"]; + var ditherCoordinatesProperty = particleMaterialMetadata["unity.particles.vectorPort.ditherCoordinates"]; + var renderFrameProperty = particleMaterialMetadata["unity.particles.intPort.frameIndex"]; + var liveDitherTexture = liveParticleMaterial.GetTexture(ditherTextureProperty); + Assert.That(liveDitherTexture, Is.Not.Null, + "The generic lowerer did not bind the advertised provider dither texture."); + Assert.That(liveParticleMaterial.GetVector(ditherCoordinatesProperty).x, + Is.EqualTo((float)camera.pixelWidth / liveDitherTexture.width).Within(0.0001f)); + Assert.That(liveParticleMaterial.GetVector(ditherCoordinatesProperty).y, + Is.EqualTo((float)camera.pixelHeight / liveDitherTexture.height).Within(0.0001f)); + Assert.That(liveParticleMaterial.GetInt(renderFrameProperty), Is.GreaterThan(0), + "The generic lowerer did not advance the advertised render-frame port."); Assert.That(fieldParticles.PresentedFrameId, Is.GreaterThanOrEqualTo(0)); Assert.That(fieldParticles.ParticleCount, Is.EqualTo(65536)); Assert.That(fieldParticles.DispatchCount, Is.GreaterThan(0)); diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 93d9bc4..fe38f4c 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.15", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.56", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.57", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 34af8c4..5f8e801 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.56", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.57", "depth": 0, "source": "git", "dependencies": { @@ -155,7 +155,7 @@ "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "dfa843cde2c0d9328bb2fa342c9378bf3cdfe6f4" + "hash": "0974154fa7c7982c9a8ea0ca006d2698a53e1235" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 206c913..cfed0c4 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -808,6 +808,22 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests particleAbiError); Assert.That(fieldParticles.ProgramReady, Is.True, "The generic field-particle lowerer rejected the advertised provider ABI or assets."); + var liveParticleMaterial = typeof(EveUnityFieldsParticleRenderer) + .GetField("_material", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) + ?.GetValue(fieldParticles) as Material; + Assert.That(liveParticleMaterial, Is.Not.Null); + var ditherTextureProperty = particleMaterialMetadata["unity.particles.texturePort.dither"]; + var ditherCoordinatesProperty = particleMaterialMetadata["unity.particles.vectorPort.ditherCoordinates"]; + var renderFrameProperty = particleMaterialMetadata["unity.particles.intPort.frameIndex"]; + var liveDitherTexture = liveParticleMaterial.GetTexture(ditherTextureProperty); + Assert.That(liveDitherTexture, Is.Not.Null, + "The generic lowerer did not bind the advertised provider dither texture."); + Assert.That(liveParticleMaterial.GetVector(ditherCoordinatesProperty).x, + Is.EqualTo((float)camera.pixelWidth / liveDitherTexture.width).Within(0.0001f)); + Assert.That(liveParticleMaterial.GetVector(ditherCoordinatesProperty).y, + Is.EqualTo((float)camera.pixelHeight / liveDitherTexture.height).Within(0.0001f)); + Assert.That(liveParticleMaterial.GetInt(renderFrameProperty), Is.GreaterThan(0), + "The generic lowerer did not advance the advertised render-frame port."); Assert.That(fieldParticles.PresentedFrameId, Is.GreaterThanOrEqualTo(0)); Assert.That(fieldParticles.ParticleCount, Is.EqualTo(65536)); Assert.That(fieldParticles.DispatchCount, Is.GreaterThan(0)); diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 6c288f0..ea876bf 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -118,7 +118,7 @@ present in the map-only frame. This is transport, authority, field-production, and lowering proof, not visual parity: the current pilot capture contains the blue fog field and provider geometry, but nearby ships remain too dark and the celestial composition lacks the fossil capture's readable close body. Release -`0.3.56` includes generic lowering for Eve Fields `0.2.3`'s +`0.3.57` includes generic lowering for Eve Fields `0.2.3`'s `AnimatedRadialCosine` source alongside `PowerPulse`, dither-scale, temporal-history, and finite-look-at semantics. Aetheria publishes the fossil global animated simplex/cellular/ambient producer stack, body-owned radial wave @@ -132,7 +132,7 @@ delivery. Color, lighting, patch structure, readable world geometry, and gravity-wave visual parity remain open. EveUnity must not compensate with Aetheria-specific lighting or fog rules. -The `0.3.56` run also closes two camera-contract omissions. Aetheria advertises +The `0.3.57` run also closes two camera-contract omissions. Aetheria advertises the fossil pilot camera's neutral temporal-reprojection values; the released generic lowerer applies URP TAA with `0.99` history blend, `0.1` jitter scale, high quality, and zero sharpening. Fog and Stardust now consume one generic @@ -141,6 +141,16 @@ document independently. The live witness fails unless their snapped grid centers are identical; this run resolved both to `(-6,-6)` after movement. The capture is still visibly rough and is not a visual-parity claim. +The fossil Stardust material used a screen-space coverage mask that changes on +every rendered frame and depends on TAA to reconstruct stable subpixel stars. +The earlier URP port had replaced that with a fixed alpha threshold and +additive blending. Aetheria now advertises the provider dither texture, +viewport scale, and logical render-frame port; generic EveUnity binds them +without knowing their product meaning. The live witness resolves those native +ports from provider metadata and checks the actual material instance. The +provider bundle source gate requires the temporal clip, overwrite blend, and +depth write and rejects `_AlphaClip`. + Substance is not part of this path; later texture baking belongs in Blender. Aetheria currently bundles ambientCG's 1K `Metal012` color, normal, and metalness maps under CC0. The same pre-generated maps now replace dead diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 2e47ca3..024e2b3 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "dfa843cde2c0d9328bb2fa342c9378bf3cdfe6f4" +$expectedEveUnityCommit = "0974154fa7c7982c9a8ea0ca006d2698a53e1235" $expectedEveFieldsCommit = "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "419053ebe2325848051c4f4d8ba352cd4286c424" From 24e0a19e1703f8382e13a735c400af5f40928d2b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 19:01:09 +0100 Subject: [PATCH 137/292] Prove stellar environment and shield-free hulls --- .../PlayMode/GenericWorldCaptureTests.cs | 42 +++++++++++++++---- .../PlayMode/GenericWorldCaptureTests.cs | 42 +++++++++++++++---- docs/aetheria-released-package-witness.md | 36 +++++++++------- 3 files changed, 89 insertions(+), 31 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index cfed0c4..253e8a7 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -12,6 +12,7 @@ using GameCult.Eve.UnityScene.Fields; using GameCult.Mesh; using NUnit.Framework; using UnityEngine; +using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using UnityEngine.TestTools; using UnityEngine.UIElements; @@ -612,6 +613,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var playerMarker = FindMarker(root, playerId); var playerRenderers = playerMarker.GetComponentsInChildren(); Assert.That(playerRenderers, Is.Not.Empty, "The provider-authored player prefab has no renderable visual."); + Assert.That(playerRenderers.Any(renderer => renderer.name == "Shield Visual"), Is.False, + "The provider hull retained an always-on shield envelope instead of using receipt-driven shield feedback."); Assert.That(playerMarker.GetComponentsInChildren(), Is.Empty, "The provider-authored player presentation retained gameplay colliders."); Assert.That(playerMarker.GetComponentsInChildren(), Is.Empty, @@ -642,18 +645,21 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests cameraRig.CameraTransform = camera.transform; cameraRig.RenderPolicySource = provider; Assert.That(cameraRig.ApplyRig(0f), Is.True); - var skybox = provider.ResolveAsset(new EveUnityPlayableWorldAssetBinding( - runtime.ActiveWorld.SkyboxAssetRef, "", "provider-asset-ref"), typeof(Material)) as Material; var reflection = provider.ResolveAsset(new EveUnityPlayableWorldAssetBinding( runtime.ActiveWorld.ReflectionAssetRef, "", "provider-asset-ref"), typeof(Cubemap)) as Cubemap; - Assert.That(runtime.ActiveWorld.SkyboxAssetRef, Is.Not.Empty); + Assert.That(runtime.ActiveWorld.SkyboxAssetRef, Is.Empty, + "Aetheria's raymarch owns the frame; its reflection HDRI must not become a skybox-lighting authority."); Assert.That(runtime.ActiveWorld.ReflectionAssetRef, Is.Not.Empty); - Assert.That(skybox, Is.Not.Null); Assert.That(reflection, Is.Not.Null); - Assert.That(RenderSettings.skybox, Is.SameAs(skybox)); + Assert.That(RenderSettings.ambientMode, Is.EqualTo(AmbientMode.Flat)); + Assert.That(RenderSettings.ambientLight.r, + Is.EqualTo(runtime.ActiveWorld.AmbientLightR * runtime.ActiveWorld.AmbientLightIntensity).Within(0.001f)); + Assert.That(RenderSettings.ambientLight.g, + Is.EqualTo(runtime.ActiveWorld.AmbientLightG * runtime.ActiveWorld.AmbientLightIntensity).Within(0.001f)); + Assert.That(RenderSettings.ambientLight.b, + Is.EqualTo(runtime.ActiveWorld.AmbientLightB * runtime.ActiveWorld.AmbientLightIntensity).Within(0.001f)); Assert.That(RenderSettings.customReflectionTexture, Is.SameAs(reflection)); - Assert.That(camera.clearFlags, Is.EqualTo(CameraClearFlags.Skybox)); - var environmentPresentation = RenderSettings.skybox == skybox && + var environmentPresentation = RenderSettings.ambientMode == AmbientMode.Flat && RenderSettings.customReflectionTexture == reflection; aimRenderer.RefreshNow(); Assert.That(aimRenderer.ViewDotVisible, Is.True); @@ -679,7 +685,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests .Where(material => material != null) .Distinct() .Count(HasAnyTexture); - Assert.That(texturedPlayerMaterialCount, Is.GreaterThanOrEqualTo(9), + Assert.That(texturedPlayerMaterialCount, Is.GreaterThanOrEqualTo(8), "The provider bundle must retain its advertised native material textures."); var directionalLightIntensity = UnityEngine.Object.FindObjectsOfType() .Where(light => light != null && light.enabled && light.type == LightType.Directional) @@ -994,6 +1000,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests providerAssets = true, assetBodyTransport = assetBodyTransport, environmentPresentation = environmentPresentation, + ambientProbeEnergy = AmbientProbeEnergy(), + ambientMode = RenderSettings.ambientMode.ToString(), + ambientLight = RenderSettings.ambientLight, + reflectionTexture = RenderSettings.customReflectionTexture != null + ? RenderSettings.customReflectionTexture.name + : "", fieldVolumeFrameId = fieldVolumeFrameId, fieldVolumeLayerCount = fieldVolumeLayerCount, fieldVolumeCompositeCount = fieldVolumeCompositeCount, @@ -1077,6 +1089,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public bool providerAssets; public string assetBodyTransport; public bool environmentPresentation; + public float ambientProbeEnergy; + public string ambientMode; + public Color ambientLight; + public string reflectionTexture; public long fieldVolumeFrameId; public int fieldVolumeLayerCount; public long fieldVolumeCompositeCount; @@ -1147,6 +1163,16 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public WitnessReceipt[] receipts; } + private static float AmbientProbeEnergy() + { + var probe = RenderSettings.ambientProbe; + var energy = 0f; + for (var channel = 0; channel < 3; channel++) + for (var coefficient = 0; coefficient < 9; coefficient++) + energy += Mathf.Abs(probe[channel, coefficient]); + return energy; + } + [Serializable] private sealed class WitnessReceipt { diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index cfed0c4..253e8a7 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -12,6 +12,7 @@ using GameCult.Eve.UnityScene.Fields; using GameCult.Mesh; using NUnit.Framework; using UnityEngine; +using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using UnityEngine.TestTools; using UnityEngine.UIElements; @@ -612,6 +613,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var playerMarker = FindMarker(root, playerId); var playerRenderers = playerMarker.GetComponentsInChildren(); Assert.That(playerRenderers, Is.Not.Empty, "The provider-authored player prefab has no renderable visual."); + Assert.That(playerRenderers.Any(renderer => renderer.name == "Shield Visual"), Is.False, + "The provider hull retained an always-on shield envelope instead of using receipt-driven shield feedback."); Assert.That(playerMarker.GetComponentsInChildren(), Is.Empty, "The provider-authored player presentation retained gameplay colliders."); Assert.That(playerMarker.GetComponentsInChildren(), Is.Empty, @@ -642,18 +645,21 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests cameraRig.CameraTransform = camera.transform; cameraRig.RenderPolicySource = provider; Assert.That(cameraRig.ApplyRig(0f), Is.True); - var skybox = provider.ResolveAsset(new EveUnityPlayableWorldAssetBinding( - runtime.ActiveWorld.SkyboxAssetRef, "", "provider-asset-ref"), typeof(Material)) as Material; var reflection = provider.ResolveAsset(new EveUnityPlayableWorldAssetBinding( runtime.ActiveWorld.ReflectionAssetRef, "", "provider-asset-ref"), typeof(Cubemap)) as Cubemap; - Assert.That(runtime.ActiveWorld.SkyboxAssetRef, Is.Not.Empty); + Assert.That(runtime.ActiveWorld.SkyboxAssetRef, Is.Empty, + "Aetheria's raymarch owns the frame; its reflection HDRI must not become a skybox-lighting authority."); Assert.That(runtime.ActiveWorld.ReflectionAssetRef, Is.Not.Empty); - Assert.That(skybox, Is.Not.Null); Assert.That(reflection, Is.Not.Null); - Assert.That(RenderSettings.skybox, Is.SameAs(skybox)); + Assert.That(RenderSettings.ambientMode, Is.EqualTo(AmbientMode.Flat)); + Assert.That(RenderSettings.ambientLight.r, + Is.EqualTo(runtime.ActiveWorld.AmbientLightR * runtime.ActiveWorld.AmbientLightIntensity).Within(0.001f)); + Assert.That(RenderSettings.ambientLight.g, + Is.EqualTo(runtime.ActiveWorld.AmbientLightG * runtime.ActiveWorld.AmbientLightIntensity).Within(0.001f)); + Assert.That(RenderSettings.ambientLight.b, + Is.EqualTo(runtime.ActiveWorld.AmbientLightB * runtime.ActiveWorld.AmbientLightIntensity).Within(0.001f)); Assert.That(RenderSettings.customReflectionTexture, Is.SameAs(reflection)); - Assert.That(camera.clearFlags, Is.EqualTo(CameraClearFlags.Skybox)); - var environmentPresentation = RenderSettings.skybox == skybox && + var environmentPresentation = RenderSettings.ambientMode == AmbientMode.Flat && RenderSettings.customReflectionTexture == reflection; aimRenderer.RefreshNow(); Assert.That(aimRenderer.ViewDotVisible, Is.True); @@ -679,7 +685,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests .Where(material => material != null) .Distinct() .Count(HasAnyTexture); - Assert.That(texturedPlayerMaterialCount, Is.GreaterThanOrEqualTo(9), + Assert.That(texturedPlayerMaterialCount, Is.GreaterThanOrEqualTo(8), "The provider bundle must retain its advertised native material textures."); var directionalLightIntensity = UnityEngine.Object.FindObjectsOfType() .Where(light => light != null && light.enabled && light.type == LightType.Directional) @@ -994,6 +1000,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests providerAssets = true, assetBodyTransport = assetBodyTransport, environmentPresentation = environmentPresentation, + ambientProbeEnergy = AmbientProbeEnergy(), + ambientMode = RenderSettings.ambientMode.ToString(), + ambientLight = RenderSettings.ambientLight, + reflectionTexture = RenderSettings.customReflectionTexture != null + ? RenderSettings.customReflectionTexture.name + : "", fieldVolumeFrameId = fieldVolumeFrameId, fieldVolumeLayerCount = fieldVolumeLayerCount, fieldVolumeCompositeCount = fieldVolumeCompositeCount, @@ -1077,6 +1089,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public bool providerAssets; public string assetBodyTransport; public bool environmentPresentation; + public float ambientProbeEnergy; + public string ambientMode; + public Color ambientLight; + public string reflectionTexture; public long fieldVolumeFrameId; public int fieldVolumeLayerCount; public long fieldVolumeCompositeCount; @@ -1147,6 +1163,16 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public WitnessReceipt[] receipts; } + private static float AmbientProbeEnergy() + { + var probe = RenderSettings.ambientProbe; + var energy = 0f; + for (var channel = 0; channel < 3; channel++) + for (var coefficient = 0; coefficient < 9; coefficient++) + energy += Mathf.Abs(probe[channel, coefficient]); + return energy; + } + [Serializable] private sealed class WitnessReceipt { diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index ea876bf..bf58a56 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,8 +4,8 @@ The released-package warm-gameplay witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.55`, commit - `745ce7cc24c514ad442d53807722b6a4651d9521`; +- `org.gamecult.eve.unity-scene` `0.3.57`, commit + `0974154fa7c7982c9a8ea0ca006d2698a53e1235`; - `org.gamecult.eve.plugin-fields` `0.2.3`, commit `c5a4a75c1b727499b16c2dae1895f29e2a9f72f0`; - `org.gamecult.eve.surface` `0.2.2`, commit @@ -30,14 +30,14 @@ ships were `Djinni`, and no `LonginusX` appeared. The player moved 14.34 world units and completed its authoritative hardware-driven look before combat. The daemon-owned look direction reached the SoA body rotation, and the generic aim presentation rendered at the advertised 50-unit minimum -convergence distance. The advertised skybox material and reflection cubemap -resolved from the provider bundle as their required native types and became the -active camera rig's leased Unity environment. -The witness supplies no client-authored light. The fossil `ARPG.unity` scene has -no Unity `Light` component, so Aetheria advertises its skybox ambient intensity -and custom reflection but no key light. The generic camera rig records zero live -directional intensity. The earlier invented `0.75` light has been deleted rather -than retained as a material-conversion compensator. +convergence distance. Aetheria derives ambient color from daemon-owned stellar +`LightColor` values and advertises `studio2` only as the reflection cubemap; the +gravity-fog raymarch owns the visible frame. The released lowerer records flat +ambient mode, color `(1.46,1.2556,0.8468)`, ambient-probe energy `4.6354`, and +custom reflection texture `studio2`. No skybox asset or key light is advertised, +and the generic camera rig records zero live directional intensity. The earlier +invented `0.75` light and false skybox authority were deleted rather than retained +as material-conversion compensators. Aetheria also advertises the original undocked `ARPG.unity` Third Person Rig: entity-forward perspective follow, `30` unit distance, `60` degree vertical field of view, canonical bottom-origin target screen position `0.64,0.19`, zero position damping, and @@ -73,7 +73,7 @@ daemon lock progress `1.0`; Unity does not manufacture or smooth that value. Camera-channel facts: -- provider-authored player renderers: `13` total / `9` enabled, with no embedded shield renderer; +- provider-authored player renderers: `7` active, with no embedded shield renderer; - pilot changed pixels: `217,323`; - pilot average luminance: `0.1322155`; - pilot bright pixels: `101,510`; @@ -116,8 +116,9 @@ without resurrecting the false scene light. Visual inspection confirms that map glyphs are absent from the pilot frame and present in the map-only frame. This is transport, authority, field-production, and lowering proof, not visual parity: the current pilot capture contains the -blue fog field and provider geometry, but nearby ships remain too dark and the -celestial composition lacks the fossil capture's readable close body. Release +blue fog field and the provider's magenta-and-gold Longinus hull with no opaque +shield ellipsoid. The celestial composition still lacks the fossil capture's +readable close body. Release `0.3.57` includes generic lowering for Eve Fields `0.2.3`'s `AnimatedRadialCosine` source alongside `PowerPulse`, dither-scale, temporal-history, and finite-look-at semantics. Aetheria publishes the fossil @@ -138,7 +139,12 @@ generic lowerer applies URP TAA with `0.99` history blend, `0.1` jitter scale, high quality, and zero sharpening. Fog and Stardust now consume one generic camera-relative viewport-frame resolver rather than projecting the same splat document independently. The live witness fails unless their snapped grid -centers are identical; this run resolved both to `(-6,-6)` after movement. +centers are identical; the latest run resolved both to `(-6,-6)` after movement. +The provider HLSL now consumes Eve Fields' positive world-axis viewport directly; +it no longer retains the two-axis compensation once required by the deleted +fossil gravity camera. An asymmetric GPU fixture fails unless positive-world X +samples positive texture X, while the existing one-cell remap proof still keeps +705 overlapping world particles bit-identical. The capture is still visibly rough and is not a visual-parity claim. The fossil Stardust material used a screen-space coverage mask that changes on @@ -156,7 +162,7 @@ Aetheria currently bundles ambientCG's 1K `Metal012` color, normal, and metalness maps under CC0. The same pre-generated maps now replace dead Substance archive sub-assets on the brushed aluminium, tinted car paint, cockpit, steel, black-metal, and radiator materials. The released witness - resolves textured native URP materials in `9` distinct player-material slots, + resolves textured native URP materials in `8` distinct player-material slots, up from `3`, without adding another texture payload. This is temporary provider art with recorded provenance, not a replacement texture pipeline. From 86f8efae2625cc2e0df46620ddf1b08c656f3b8e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 19:07:28 +0100 Subject: [PATCH 138/292] Prove shield-free cold Aetheria lowering --- docs/aetheria-released-package-witness.md | 27 ++++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index bf58a56..044b1ef 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -199,15 +199,20 @@ Primary artifacts: ## Cold delivery proof -Cold CDN transfer is currently failing and is not part of the passing claim. -The approximately 13 MB provider bundle still times out because its bytes are -carried through batched snapshot records instead of the intended mapped/network -body transport. Increasing the timeout would only conceal the transport error. +Cold CDN transfer passes with the current released package set. The +`cold-start-lowering` run in `artifacts/aetheria-daemon-current-cold` began with +zero cached bodies, received one typed manifest snapshot, transferred bounded +payload chunks through the managed CultMesh content session, and atomically +promoted exactly one `56,168,017`-byte body named by SHA-256 +`f54177cc8aadd5cd39ba0757cfd2b90a403a1b9efcfcb32060b86e8c152dce10`. +It left zero partial files, selected `SharedFileMapping` over that verified path, +lowered the shield-free provider hull, and preserved pilot/map camera isolation +within the unchanged 300-second deadline; total witness time was `102,912.852` +milliseconds. -The passing `full-session-gameplay` witness starts from a previously verified -warm body and negotiates `SharedFileMapping` over that provider-owned path. It -proves released-package lowering and gameplay, not cold acquisition, -provider-to-client network zero-copy, shared-memory delivery, or GPU zero-copy. -Cold proof remains blocked until CultMesh transfers the bundle through the -network-body path and atomically promotes the verified result without routing -the payload through snapshot-record batching. +The separate passing `full-session-gameplay` witness starts from the verified +warm body and proves movement, combat, destruction-created canonical loot, Ymir +contact collection, and exactly-once cargo mutation. This proves cold acquisition +and local mapped reuse, not provider-to-client network zero-copy, shared-memory +delivery, or GPU zero-copy. Managed network bytes are still copied and fragmented +before the transfer owner commits the mapped file. From 02203ff134570c301ce58969397c296ee14e69ee Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 19:25:57 +0100 Subject: [PATCH 139/292] Preserve Unity shader camera transform semantics --- .../Fields/EveUnityFieldsVolumeRenderer.cs | 5 +++- .../EveFieldsUnitySceneProjectionTests.cs | 30 +++++++++++++++++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs index f4e823a..061d55c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -140,7 +140,7 @@ namespace GameCult.Eve.UnityScene.Fields var gpuProjection = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true); var viewProjection = gpuProjection * camera.worldToCameraMatrix; SetMatrixPort("cameraInverseViewProjection", viewProjection.inverse); - SetMatrixPort("cameraToWorld", camera.cameraToWorldMatrix); + SetMatrixPort("cameraToWorld", CameraTransformToWorld(camera)); SetVectorPort("cameraProjectionExtents", ProjectionExtents(camera)); SetFloatPort("raymarchOffset", Halton(Time.frameCount & 1023, 3)); @@ -594,6 +594,9 @@ namespace GameCult.Eve.UnityScene.Fields return new Vector4(y * camera.aspect, y, 0f, 0f); } + private static Matrix4x4 CameraTransformToWorld(Camera camera) => + camera.transform.localToWorldMatrix; + private static float Halton(int index, int radix) { var result = 0f; diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index a8349a4..3591dbd 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Linq; using System.Collections.Generic; +using System.Reflection; using GameCult.Eve.PluginFields; using GameCult.Eve.UnityScene.Fields; using NUnit.Framework; @@ -12,6 +13,35 @@ namespace GameCult.Eve.UnityScene.Tests { public sealed class EveFieldsUnitySceneProjectionTests { + [Test] + public void VolumeCameraToWorldPreservesUnityShaderPositiveZForward() + { + var cameraObject = new GameObject("eve-volume-camera-matrix"); + try + { + var camera = cameraObject.AddComponent(); + camera.transform.SetPositionAndRotation( + new Vector3(12f, -3f, 8f), + Quaternion.Euler(17f, 43f, -9f)); + var method = typeof(EveUnityFieldsVolumeRenderer).GetMethod( + "CameraTransformToWorld", + BindingFlags.NonPublic | BindingFlags.Static); + + Assert.That(method, Is.Not.Null); + var matrix = (Matrix4x4)method!.Invoke(null, new object[] { camera }); + Assert.That(Vector3.Dot( + matrix.MultiplyVector(Vector3.forward).normalized, + camera.transform.forward), Is.EqualTo(1f).Within(0.00001f)); + Assert.That(Vector3.Distance( + matrix.MultiplyPoint3x4(Vector3.zero), + camera.transform.position), Is.LessThan(0.00001f)); + } + finally + { + UnityEngine.Object.DestroyImmediate(cameraObject); + } + } + [Test] public void RasterizerConsumesPluginOwnedDocumentContract() { diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 64b70e2..d3c13c8 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.57", + "version": "0.3.58", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 32ac89a7a4b637a23efab6d2d4e9892c9d1809c8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 19:34:04 +0100 Subject: [PATCH 140/292] Pin restored gravity fog camera contract --- ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 ++-- docs/aetheria-released-package-witness.md | 15 +++++++++++++-- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index fe38f4c..4ba9303 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.15", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.57", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.58", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 5f8e801..31f1751 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.57", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.58", "depth": 0, "source": "git", "dependencies": { @@ -155,7 +155,7 @@ "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "0974154fa7c7982c9a8ea0ca006d2698a53e1235" + "hash": "02203ff134570c301ce58969397c296ee14e69ee" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 044b1ef..eaf7cb3 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,8 +4,8 @@ The released-package warm-gameplay witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.57`, commit - `0974154fa7c7982c9a8ea0ca006d2698a53e1235`; +- `org.gamecult.eve.unity-scene` `0.3.58`, commit + `02203ff134570c301ce58969397c296ee14e69ee`; - `org.gamecult.eve.plugin-fields` `0.2.3`, commit `c5a4a75c1b727499b16c2dae1895f29e2a9f72f0`; - `org.gamecult.eve.surface` `0.2.2`, commit @@ -17,6 +17,17 @@ The released-package warm-gameplay witness passes with: - the generic `ReleaseConsumerProject` client connected directly to the Aetheria daemon. +The cold `0.3.58` camera-contract witness in +`artifacts/aetheria-daemon-gravity-fog-historical` resolves the tagged package, +transfers and lowers bundle +`e299b50ded9c9ba8964c567964c30342b9b13323665b20e740210b4806dd6e66`, and +passes in 51.0 seconds. EveUnity now supplies the positive-Z camera transform +used by Unity shader globals, so Aetheria keeps its historical ray direction +instead of compensating for the negative-Z `Camera.cameraToWorldMatrix` API. +The resulting capture exposes a separate unresolved mismatch: restoring the +historical scattering law makes the current projected fog frame severely +overexposed. That is not claimed as visual parity. + Current released-package evidence is in `artifacts/aetheria-daemon-mapped-body-cold`; despite that retained directory name, `runtime-witness.warm.json` is the current passing run. The warm PlayMode test diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 024e2b3..95712b6 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "0974154fa7c7982c9a8ea0ca006d2698a53e1235" +$expectedEveUnityCommit = "02203ff134570c301ce58969397c296ee14e69ee" $expectedEveFieldsCommit = "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "419053ebe2325848051c4f4d8ba352cd4286c424" From 4d5257b8ca09767974e8eb350a9695191ec39fdc Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 19:51:25 +0100 Subject: [PATCH 141/292] Lower adaptive exposure from Eve camera semantics --- .../EveUnityAdaptiveExposureRenderer.cs | 220 ++++++++++++++++++ .../EveUnityAdaptiveExposureRenderer.cs.meta | 2 + .../Runtime/EveUnityPlayableWorldCameraRig.cs | 37 ++- .../Runtime/EveUnitySceneSurfaceLowerer.cs | 49 +++- .../Runtime/Resources.meta | 8 + .../Runtime/Resources/EveUnity.meta | 8 + .../EveUnity/AdaptiveExposure.compute | 98 ++++++++ .../EveUnity/AdaptiveExposure.compute.meta | 8 + .../EveUnity/AdaptiveExposureMultiply.shader | 39 ++++ .../AdaptiveExposureMultiply.shader.meta | 9 + .../EveFieldsUnitySceneProjectionTests.cs | 83 +++++++ .../EveUnitySceneSurfaceLowererTests.cs | 45 +++- .../org.gamecult.eve.unity-scene/package.json | 2 +- 13 files changed, 602 insertions(+), 6 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs.meta create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Resources.meta create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity.meta create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposure.compute create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposure.compute.meta create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposureMultiply.shader create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposureMultiply.shader.meta diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs new file mode 100644 index 0000000..04e1f0b --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs @@ -0,0 +1,220 @@ +using System; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.Rendering.RenderGraphModule; +using UnityEngine.Rendering.Universal; + +#nullable enable + +namespace GameCult.Eve.UnityScene +{ + public sealed class EveUnityAdaptiveExposureRenderer : MonoBehaviour + { + private const int HistogramBins = 128; + private ComputeShader? _compute; + private Material? _multiplyMaterial; + private ComputeBuffer? _histogram; + private readonly RenderTexture?[] _exposure = new RenderTexture?[2]; + private int _currentExposure = -1; + private Camera? _camera; + private AdaptiveExposurePass? _pass; + + private float _lowPercent; + private float _highPercent; + private float _minimumEv; + private float _maximumEv; + private float _keyValue; + private float _speedUp; + private float _speedDown; + private bool _progressive; + + public long AppliedFrameCount { get; private set; } + public bool IsConfigured { get; private set; } + public static RenderPassEvent ExposureRenderPassEvent => + (RenderPassEvent)((int)RenderPassEvent.BeforeRenderingPostProcessing + 1); + + public void Configure( + float lowPercent, + float highPercent, + float minimumEv, + float maximumEv, + float keyValue, + string adaptation, + float speedUp, + float speedDown) + { + _lowPercent = Mathf.Clamp(lowPercent, 1f, 98.99f); + _highPercent = Mathf.Clamp(highPercent, _lowPercent + 0.01f, 99f); + _minimumEv = Mathf.Min(minimumEv, maximumEv); + _maximumEv = Mathf.Max(minimumEv, maximumEv); + _keyValue = Mathf.Max(0f, keyValue); + _speedUp = Mathf.Max(0f, speedUp); + _speedDown = Mathf.Max(0f, speedDown); + _progressive = string.Equals(adaptation, "progressive", StringComparison.Ordinal); + IsConfigured = true; + enabled = true; + } + + private void OnEnable() + { + _camera = GetComponent(); + RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering; + } + + private void OnDisable() + { + RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering; + ReleaseResources(); + } + + private void OnDestroy() => ReleaseResources(); + + private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera) + { + if (!IsConfigured || _camera == null || camera != _camera) return; + _pass ??= new AdaptiveExposurePass(this); + camera.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(_pass); + } + + private bool EnsureResources() + { + _compute ??= Resources.Load("EveUnity/AdaptiveExposure"); + if (_multiplyMaterial == null) + { + var shader = Shader.Find("Hidden/EveUnity/AdaptiveExposureMultiply"); + if (shader != null) _multiplyMaterial = new Material(shader) { hideFlags = HideFlags.HideAndDontSave }; + } + _histogram ??= new ComputeBuffer(HistogramBins, sizeof(uint), ComputeBufferType.Structured); + for (var index = 0; index < _exposure.Length; index++) + { + if (_exposure[index] != null && _exposure[index]!.IsCreated()) continue; + _exposure[index] = new RenderTexture(1, 1, 0, RenderTextureFormat.RFloat, RenderTextureReadWrite.Linear) + { + name = $"Eve Adaptive Exposure {index}", + enableRandomWrite = true, + filterMode = FilterMode.Point, + wrapMode = TextureWrapMode.Clamp, + hideFlags = HideFlags.HideAndDontSave + }; + _exposure[index]!.Create(); + } + return _compute != null && _multiplyMaterial != null && _histogram != null; + } + + private void Apply(UnsafeCommandBuffer commands, TextureHandle colorTarget, int width, int height) + { + if (!EnsureResources() || _compute == null || _histogram == null || _multiplyMaterial == null) return; + + var clear = _compute.FindKernel("ClearHistogram"); + commands.SetComputeBufferParam(_compute, clear, "_Histogram", _histogram); + commands.DispatchCompute(_compute, clear, Mathf.CeilToInt(HistogramBins / 64f), 1, 1); + + var build = _compute.FindKernel("BuildHistogram"); + commands.SetComputeBufferParam(_compute, build, "_Histogram", _histogram); + commands.SetComputeTextureParam(_compute, build, "_Source", colorTarget); + commands.SetComputeVectorParam(_compute, "_HistogramScaleOffsetResolution", new Vector4( + 1f / 18f, + 0.5f, + Mathf.Max(1, width), + Mathf.Max(1, height))); + commands.DispatchCompute( + _compute, + build, + Mathf.CeilToInt(width / 2f / 16f), + Mathf.CeilToInt(height / 2f / 16f), + 1); + + var destination = _currentExposure < 0 ? 0 : (_currentExposure + 1) % 2; + var calculate = _compute.FindKernel( + _currentExposure < 0 || !_progressive ? "CalculateFixedExposure" : "CalculateProgressiveExposure"); + commands.SetComputeBufferParam(_compute, calculate, "_Histogram", _histogram); + commands.SetComputeVectorParam(_compute, "_ExposureFilterAndBrightness", new Vector4( + _lowPercent * 0.01f, + _highPercent * 0.01f, + Mathf.Pow(2f, _minimumEv), + Mathf.Pow(2f, _maximumEv))); + commands.SetComputeVectorParam(_compute, "_ExposureAdaptation", new Vector4( + _speedDown, + _speedUp, + _keyValue, + Mathf.Max(0f, Time.deltaTime))); + if (_currentExposure >= 0) + commands.SetComputeTextureParam(_compute, calculate, "_PreviousExposure", _exposure[_currentExposure]!); + commands.SetComputeTextureParam(_compute, calculate, "_DestinationExposure", _exposure[destination]!); + commands.DispatchCompute(_compute, calculate, 1, 1, 1); + + _currentExposure = destination; + _multiplyMaterial.SetTexture("_ExposureTexture", _exposure[_currentExposure]); + commands.SetRenderTarget(colorTarget); + commands.DrawProcedural(Matrix4x4.identity, _multiplyMaterial, 0, MeshTopology.Triangles, 3, 1); + AppliedFrameCount++; + } + + internal static float ExposureForAverageLuminance(float averageLuminance, float keyValue) => + Mathf.Max(0f, keyValue) / Mathf.Max(1e-6f, averageLuminance); + + internal static float AdaptExposure(float previous, float next, float deltaTime, float speedUp, float speedDown) + { + var speed = next > previous ? Mathf.Max(0f, speedDown) : Mathf.Max(0f, speedUp); + return previous + (next - previous) * (1f - Mathf.Pow(2f, -Mathf.Max(0f, deltaTime) * speed)); + } + + private void ReleaseResources() + { + _histogram?.Release(); + _histogram = null; + foreach (var texture in _exposure) + { + if (texture == null) continue; + texture.Release(); + if (Application.isPlaying) Destroy(texture); else DestroyImmediate(texture); + } + _exposure[0] = null; + _exposure[1] = null; + _currentExposure = -1; + if (_multiplyMaterial != null) + { + if (Application.isPlaying) Destroy(_multiplyMaterial); else DestroyImmediate(_multiplyMaterial); + _multiplyMaterial = null; + } + } + + private sealed class AdaptiveExposurePass : ScriptableRenderPass + { + private readonly EveUnityAdaptiveExposureRenderer _owner; + + private sealed class PassData + { + internal EveUnityAdaptiveExposureRenderer Owner = null!; + internal TextureHandle ColorTarget; + internal int Width; + internal int Height; + } + + internal AdaptiveExposurePass(EveUnityAdaptiveExposureRenderer owner) + { + _owner = owner; + renderPassEvent = ExposureRenderPassEvent; + } + + public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) + { + var cameraData = frameData.Get(); + if (cameraData.camera == null || cameraData.camera != _owner._camera) return; + var resourceData = frameData.Get(); + var colorTarget = resourceData.activeColorTexture; + if (!colorTarget.IsValid()) return; + + using var builder = renderGraph.AddUnsafePass("Eve Adaptive Exposure", out var passData); + passData.Owner = _owner; + passData.ColorTarget = colorTarget; + passData.Width = cameraData.camera.pixelWidth; + passData.Height = cameraData.camera.pixelHeight; + builder.UseTexture(passData.ColorTarget, AccessFlags.ReadWrite); + builder.AllowPassCulling(false); + builder.SetRenderFunc(static (PassData data, UnsafeGraphContext context) => + data.Owner.Apply(context.cmd, data.ColorTarget, data.Width, data.Height)); + } + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs.meta new file mode 100644 index 0000000..fc25c4a --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3cb42e7da8b34febb348f01a598f2eea diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index 044b2c9..01f0b81 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -38,6 +38,7 @@ namespace GameCult.Eve.UnityScene private GameObject? _postProcessVolumeObject; private Volume? _postProcessVolume; private UniversalAdditionalCameraData? _postProcessCameraData; + private EveUnityAdaptiveExposureRenderer? _adaptiveExposure; private bool _createdPostProcessCameraData; private bool _previousRenderPostProcessing; private AntialiasingMode _previousAntialiasing; @@ -477,7 +478,11 @@ namespace GameCult.Eve.UnityScene world.CameraReconstruction, "temporal-reprojection.v1", System.StringComparison.Ordinal); - if (profile == null && !usesTemporalReprojection) + var usesAdaptiveExposure = string.Equals( + world.ExposureMode, + "histogram.v1", + System.StringComparison.Ordinal); + if (profile == null && !usesTemporalReprojection && !usesAdaptiveExposure) { RestorePostProcess(); return; @@ -530,6 +535,25 @@ namespace GameCult.Eve.UnityScene { _postProcessVolumeObject.SetActive(false); } + + if (usesAdaptiveExposure) + { + if (_adaptiveExposure == null) + _adaptiveExposure = camera.gameObject.AddComponent(); + _adaptiveExposure.Configure( + world.ExposureLowPercent, + world.ExposureHighPercent, + world.ExposureMinimumEv, + world.ExposureMaximumEv, + world.ExposureKeyValue, + world.ExposureAdaptation, + world.ExposureSpeedUp, + world.ExposureSpeedDown); + } + else + { + ReleaseAdaptiveExposure(); + } } private static bool IsTemporalQuality(string quality) => @@ -550,6 +574,7 @@ namespace GameCult.Eve.UnityScene private void RestorePostProcess() { + ReleaseAdaptiveExposure(); if (_postProcessCameraData != null) { if (_createdPostProcessCameraData) @@ -579,6 +604,16 @@ namespace GameCult.Eve.UnityScene _postProcessVolume = null; } + private void ReleaseAdaptiveExposure() + { + if (_adaptiveExposure == null) return; + if (Application.isPlaying) + Destroy(_adaptiveExposure); + else + DestroyImmediate(_adaptiveExposure); + _adaptiveExposure = null; + } + private void RestoreAmbientEnvironment() { ReleaseKeyLight(); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index bcc3637..374750c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -161,7 +161,16 @@ namespace GameCult.Eve.UnityScene worldRoot.GetProp("temporalQuality", "high"), ParseFloat(worldRoot.GetProp("temporalHistoryBlend"), 0f), ParseFloat(worldRoot.GetProp("temporalJitterScale"), 0f), - ParseFloat(worldRoot.GetProp("temporalSharpening"), 0f)); + ParseFloat(worldRoot.GetProp("temporalSharpening"), 0f), + worldRoot.GetProp("exposureMode"), + ParseFloat(worldRoot.GetProp("exposureLowPercent"), 50f), + ParseFloat(worldRoot.GetProp("exposureHighPercent"), 95f), + ParseFloat(worldRoot.GetProp("exposureMinimumEv"), 0f), + ParseFloat(worldRoot.GetProp("exposureMaximumEv"), 0f), + ParseFloat(worldRoot.GetProp("exposureKeyValue"), 1f), + worldRoot.GetProp("exposureAdaptation", "progressive"), + ParseFloat(worldRoot.GetProp("exposureSpeedUp"), 2f), + ParseFloat(worldRoot.GetProp("exposureSpeedDown"), 1f)); } private static EveUnityFieldVolumeProjection BuildFieldVolume(EveSurfaceComponent component) => @@ -421,7 +430,16 @@ namespace GameCult.Eve.UnityScene string temporalQuality = "high", float temporalHistoryBlend = 0f, float temporalJitterScale = 0f, - float temporalSharpening = 0f) + float temporalSharpening = 0f, + string exposureMode = "", + float exposureLowPercent = 50f, + float exposureHighPercent = 95f, + float exposureMinimumEv = 0f, + float exposureMaximumEv = 0f, + float exposureKeyValue = 1f, + string exposureAdaptation = "progressive", + float exposureSpeedUp = 2f, + float exposureSpeedDown = 1f) { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -474,6 +492,15 @@ namespace GameCult.Eve.UnityScene TemporalHistoryBlend = temporalHistoryBlend; TemporalJitterScale = temporalJitterScale; TemporalSharpening = temporalSharpening; + ExposureMode = exposureMode ?? ""; + ExposureLowPercent = exposureLowPercent; + ExposureHighPercent = exposureHighPercent; + ExposureMinimumEv = exposureMinimumEv; + ExposureMaximumEv = exposureMaximumEv; + ExposureKeyValue = exposureKeyValue; + ExposureAdaptation = exposureAdaptation ?? ""; + ExposureSpeedUp = exposureSpeedUp; + ExposureSpeedDown = exposureSpeedDown; } public string WorldRootId { get; } @@ -508,6 +535,24 @@ namespace GameCult.Eve.UnityScene public float TemporalSharpening { get; } + public string ExposureMode { get; } + + public float ExposureLowPercent { get; } + + public float ExposureHighPercent { get; } + + public float ExposureMinimumEv { get; } + + public float ExposureMaximumEv { get; } + + public float ExposureKeyValue { get; } + + public string ExposureAdaptation { get; } + + public float ExposureSpeedUp { get; } + + public float ExposureSpeedDown { get; } + public float CameraDistance { get; } public float CameraVerticalFieldOfViewDegrees { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Resources.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Resources.meta new file mode 100644 index 0000000..fb9f65a --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 63a640b4e19e5764eb030d665a2b7c00 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity.meta new file mode 100644 index 0000000..d1f9c0e --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6435ff06293834c49aa631e74b51a552 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposure.compute b/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposure.compute new file mode 100644 index 0000000..f1e1f27 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposure.compute @@ -0,0 +1,98 @@ +#pragma kernel ClearHistogram +#pragma kernel BuildHistogram +#pragma kernel CalculateFixedExposure +#pragma kernel CalculateProgressiveExposure + +RWStructuredBuffer _Histogram; +Texture2D _Source; +SamplerState sampler_LinearClamp; +Texture2D _PreviousExposure; +RWTexture2D _DestinationExposure; + +float4 _HistogramScaleOffsetResolution; +float4 _ExposureFilterAndBrightness; +float4 _ExposureAdaptation; + +groupshared uint GroupHistogram[128]; + +[numthreads(64, 1, 1)] +void ClearHistogram(uint threadId : SV_DispatchThreadID) +{ + if (threadId < 128) _Histogram[threadId] = 0; +} + +[numthreads(16, 16, 1)] +void BuildHistogram(uint2 dispatchId : SV_DispatchThreadID, uint2 groupId : SV_GroupThreadID) +{ + uint localId = groupId.y * 16 + groupId.x; + if (localId < 128) GroupHistogram[localId] = 0; + GroupMemoryBarrierWithGroupSync(); + + float2 pixel = float2(dispatchId) * 2.0; + if (pixel.x < _HistogramScaleOffsetResolution.z && pixel.y < _HistogramScaleOffsetResolution.w) + { + float2 uv = pixel / _HistogramScaleOffsetResolution.zw; + float2 distanceFromCenter = abs(uv - 0.5.xx); + float vignette = saturate(1.0 - dot(distanceFromCenter, distanceFromCenter)); + uint weight = (uint)(64.0 * vignette * vignette); + float3 color = _Source.SampleLevel(sampler_LinearClamp, uv, 0).rgb; + float luminance = max(1e-6, dot(color, float3(0.2126729, 0.7151522, 0.0721750))); + float normalizedLogLuminance = saturate( + log2(luminance) * _HistogramScaleOffsetResolution.x + _HistogramScaleOffsetResolution.y); + uint bin = (uint)(normalizedLogLuminance * 127.0); + InterlockedAdd(GroupHistogram[bin], weight); + } + + GroupMemoryBarrierWithGroupSync(); + if (localId < 128) InterlockedAdd(_Histogram[localId], GroupHistogram[localId]); +} + +float LuminanceForBin(uint bin) +{ + float normalized = (float)bin / 128.0; + return exp2((normalized - _HistogramScaleOffsetResolution.y) / _HistogramScaleOffsetResolution.x); +} + +float CalculateExposure() +{ + float total = 0.0; + for (uint index = 0; index < 128; index++) total += (float)_Histogram[index]; + + float discardDark = total * _ExposureFilterAndBrightness.x; + float includeThrough = total * _ExposureFilterAndBrightness.y; + float weightedLuminance = 0.0; + float includedWeight = 0.0; + for (uint index = 0; index < 128; index++) + { + float count = (float)_Histogram[index]; + float darkOffset = min(discardDark, count); + count -= darkOffset; + discardDark -= darkOffset; + includeThrough -= darkOffset; + count = min(includeThrough, count); + includeThrough -= count; + weightedLuminance += LuminanceForBin(index) * count; + includedWeight += count; + } + + float average = weightedLuminance / max(includedWeight, 1e-6); + average = clamp(average, _ExposureFilterAndBrightness.z, _ExposureFilterAndBrightness.w); + return _ExposureAdaptation.z / max(average, 1e-6); +} + +[numthreads(1, 1, 1)] +void CalculateFixedExposure(uint3 id : SV_DispatchThreadID) +{ + _DestinationExposure[uint2(0, 0)] = CalculateExposure(); +} + +[numthreads(1, 1, 1)] +void CalculateProgressiveExposure(uint3 id : SV_DispatchThreadID) +{ + float nextExposure = CalculateExposure(); + float previousExposure = _PreviousExposure[uint2(0, 0)]; + float delta = nextExposure - previousExposure; + float speed = delta > 0.0 ? _ExposureAdaptation.x : _ExposureAdaptation.y; + _DestinationExposure[uint2(0, 0)] = previousExposure + + delta * (1.0 - exp2(-_ExposureAdaptation.w * speed)); +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposure.compute.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposure.compute.meta new file mode 100644 index 0000000..3fe7574 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposure.compute.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c8c45489b6ba44558342ebc5381c12d1 +ComputeShaderImporter: + externalObjects: {} + currentAPIMask: 4 + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposureMultiply.shader b/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposureMultiply.shader new file mode 100644 index 0000000..d54ded1 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposureMultiply.shader @@ -0,0 +1,39 @@ +Shader "Hidden/EveUnity/AdaptiveExposureMultiply" +{ + SubShader + { + Pass + { + Cull Off ZWrite Off ZTest Always + Blend DstColor Zero + ColorMask RGB + + HLSLPROGRAM + #pragma target 4.5 + #pragma vertex Vert + #pragma fragment Frag + + Texture2D _ExposureTexture; + + struct Varyings + { + float4 position : SV_POSITION; + }; + + Varyings Vert(uint vertexId : SV_VertexID) + { + float2 uv = float2((vertexId << 1) & 2, vertexId & 2); + Varyings output; + output.position = float4(uv * 2.0 - 1.0, 0.0, 1.0); + return output; + } + + float4 Frag(Varyings input) : SV_Target + { + float exposure = _ExposureTexture.Load(int3(0, 0, 0)); + return exposure.xxxx; + } + ENDHLSL + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposureMultiply.shader.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposureMultiply.shader.meta new file mode 100644 index 0000000..daf9128 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposureMultiply.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 48315c6017f548e7a4b46698b3d7229e +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index 3591dbd..9b8391c 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -42,6 +42,89 @@ namespace GameCult.Eve.UnityScene.Tests } } + [Test] + public void AdaptiveExposureMatchesFossilCompensationAndProgressiveDirection() + { + var exposureMethod = typeof(EveUnityAdaptiveExposureRenderer).GetMethod( + "ExposureForAverageLuminance", + BindingFlags.NonPublic | BindingFlags.Static); + var adaptationMethod = typeof(EveUnityAdaptiveExposureRenderer).GetMethod( + "AdaptExposure", + BindingFlags.NonPublic | BindingFlags.Static); + + Assert.That(exposureMethod, Is.Not.Null); + Assert.That(adaptationMethod, Is.Not.Null); + var brightBound = Mathf.Pow(2f, 0.3f); + var exposure = (float)exposureMethod!.Invoke(null, new object[] { brightBound, 0.5f }); + Assert.That(exposure, Is.EqualTo(0.5f / brightBound).Within(0.000001f)); + + var darkening = (float)adaptationMethod!.Invoke( + null, + new object[] { 1f, 0.25f, 0.5f, 2f, 1f }); + var brightening = (float)adaptationMethod.Invoke( + null, + new object[] { 1f, 4f, 0.5f, 2f, 1f }); + Assert.That(darkening, Is.EqualTo(0.625f).Within(0.000001f)); + Assert.That(brightening, Is.EqualTo(1f + 3f * (1f - Mathf.Pow(2f, -0.5f))).Within(0.000001f)); + } + + [Test] + public void AdaptiveExposureGpuHistogramClampsBrightFrameToAdvertisedMaximumEv() + { + Assert.That(SystemInfo.supportsComputeShaders, Is.True); + var compute = Resources.Load("EveUnity/AdaptiveExposure"); + Assert.That(compute, Is.Not.Null); + var source = new Texture2D(32, 32, TextureFormat.RGBAFloat, false, true); + var pixels = Enumerable.Repeat(new Color(16f, 16f, 16f, 1f), 32 * 32).ToArray(); + source.SetPixels(pixels); + source.Apply(false, false); + using var histogram = new ComputeBuffer(128, sizeof(uint), ComputeBufferType.Structured); + var exposure = new RenderTexture(1, 1, 0, RenderTextureFormat.RFloat, RenderTextureReadWrite.Linear) + { + enableRandomWrite = true + }; + exposure.Create(); + try + { + var clear = compute!.FindKernel("ClearHistogram"); + compute.SetBuffer(clear, "_Histogram", histogram); + compute.Dispatch(clear, 2, 1, 1); + + var build = compute.FindKernel("BuildHistogram"); + compute.SetBuffer(build, "_Histogram", histogram); + compute.SetTexture(build, "_Source", source); + compute.SetVector("_HistogramScaleOffsetResolution", new Vector4(1f / 18f, 0.5f, 32f, 32f)); + compute.Dispatch(build, 1, 1, 1); + + var calculate = compute.FindKernel("CalculateFixedExposure"); + compute.SetBuffer(calculate, "_Histogram", histogram); + compute.SetVector("_ExposureFilterAndBrightness", new Vector4( + 0.4737294f, + 0.99f, + Mathf.Pow(2f, -3f), + Mathf.Pow(2f, 0.3f))); + compute.SetVector("_ExposureAdaptation", new Vector4(1f, 2f, 0.5f, 1f / 60f)); + compute.SetTexture(calculate, "_DestinationExposure", exposure); + compute.Dispatch(calculate, 1, 1, 1); + + var previous = RenderTexture.active; + RenderTexture.active = exposure; + var readback = new Texture2D(1, 1, TextureFormat.RFloat, false, true); + readback.ReadPixels(new Rect(0, 0, 1, 1), 0, 0); + readback.Apply(); + RenderTexture.active = previous; + var actual = readback.GetPixel(0, 0).r; + UnityEngine.Object.DestroyImmediate(readback); + Assert.That(actual, Is.EqualTo(0.5f / Mathf.Pow(2f, 0.3f)).Within(0.001f)); + } + finally + { + exposure.Release(); + UnityEngine.Object.DestroyImmediate(exposure); + UnityEngine.Object.DestroyImmediate(source); + } + } + [Test] public void RasterizerConsumesPluginOwnedDocumentContract() { diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index c716cd1..fa24946 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -1159,7 +1159,16 @@ namespace GameCult.Eve.UnityScene.Tests temporalQuality: "high", temporalHistoryBlend: "0.99", temporalJitterScale: "0.1", - temporalSharpening: "0"), + temporalSharpening: "0", + exposureMode: "histogram.v1", + exposureLowPercent: "47.37294", + exposureHighPercent: "99", + exposureMinimumEv: "-3", + exposureMaximumEv: "0.3", + exposureKeyValue: "0.5", + exposureAdaptation: "progressive", + exposureSpeedUp: "2", + exposureSpeedDown: "1"), Advertisement("aetheria.daemon.game"), "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", 1), @@ -1172,6 +1181,17 @@ namespace GameCult.Eve.UnityScene.Tests host.Configure(rootObject.transform, provider, provider, provider, provider, nativeAssets); host.Connect(); hostObject.SetActive(true); + var exposedWorld = host.ActiveProjection?.PlayableWorld; + Assert.That(exposedWorld, Is.Not.Null); + Assert.That(exposedWorld!.ExposureMode, Is.EqualTo("histogram.v1")); + Assert.That(exposedWorld.ExposureLowPercent, Is.EqualTo(47.37294f).Within(0.00001f)); + Assert.That(exposedWorld.ExposureHighPercent, Is.EqualTo(99f)); + Assert.That(exposedWorld.ExposureMinimumEv, Is.EqualTo(-3f)); + Assert.That(exposedWorld.ExposureMaximumEv, Is.EqualTo(0.3f).Within(0.00001f)); + Assert.That(exposedWorld.ExposureKeyValue, Is.EqualTo(0.5f)); + Assert.That(exposedWorld.ExposureAdaptation, Is.EqualTo("progressive")); + Assert.That(exposedWorld.ExposureSpeedUp, Is.EqualTo(2f)); + Assert.That(exposedWorld.ExposureSpeedDown, Is.EqualTo(1f)); var rig = hostObject.AddComponent(); rig.Host = host; @@ -1217,6 +1237,9 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(cameraData.taaSettings.baseBlendFactor, Is.EqualTo(0.99f).Within(0.0001f)); Assert.That(cameraData.taaSettings.jitterScale, Is.EqualTo(0.1f).Within(0.0001f)); Assert.That(cameraData.taaSettings.contrastAdaptiveSharpening, Is.Zero); + var adaptiveExposure = camera.GetComponent(); + Assert.That(adaptiveExposure, Is.Not.Null); + Assert.That(adaptiveExposure.IsConfigured, Is.True); var postProcessTransform = hostObject.transform.Find("Eve World Post Process"); Assert.That(postProcessTransform, Is.Not.Null); Assert.That(postProcessTransform.GetComponent().sharedProfile, Is.SameAs(postProcessProfile)); @@ -1910,7 +1933,16 @@ namespace GameCult.Eve.UnityScene.Tests string temporalQuality = "high", string temporalHistoryBlend = "0", string temporalJitterScale = "0", - string temporalSharpening = "0") + string temporalSharpening = "0", + string exposureMode = "", + string exposureLowPercent = "50", + string exposureHighPercent = "95", + string exposureMinimumEv = "0", + string exposureMaximumEv = "0", + string exposureKeyValue = "1", + string exposureAdaptation = "progressive", + string exposureSpeedUp = "2", + string exposureSpeedDown = "1") { var playableChildren = new List { @@ -2083,6 +2115,15 @@ namespace GameCult.Eve.UnityScene.Tests ["temporalHistoryBlend"] = temporalHistoryBlend, ["temporalJitterScale"] = temporalJitterScale, ["temporalSharpening"] = temporalSharpening, + ["exposureMode"] = exposureMode, + ["exposureLowPercent"] = exposureLowPercent, + ["exposureHighPercent"] = exposureHighPercent, + ["exposureMinimumEv"] = exposureMinimumEv, + ["exposureMaximumEv"] = exposureMaximumEv, + ["exposureKeyValue"] = exposureKeyValue, + ["exposureAdaptation"] = exposureAdaptation, + ["exposureSpeedUp"] = exposureSpeedUp, + ["exposureSpeedDown"] = exposureSpeedDown, ["keyLightDirection"] = "0.4,-1,0.25", ["keyLightColor"] = "1,0.95,0.9", ["keyLightIntensity"] = "0.75", diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index d3c13c8..20f654f 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.58", + "version": "0.3.59", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 1d23be18b664675fe3f7f254a9adbb9ace58b968 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 19:57:25 +0100 Subject: [PATCH 142/292] Pin adaptive exposure release witness --- ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 +-- docs/aetheria-released-package-witness.md | 28 ++++++++++--------- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 4ba9303..8745e90 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.15", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.58", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.59", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 31f1751..5a4c1a5 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.58", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.59", "depth": 0, "source": "git", "dependencies": { @@ -155,7 +155,7 @@ "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "02203ff134570c301ce58969397c296ee14e69ee" + "hash": "4d5257b8ca09767974e8eb350a9695191ec39fdc" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index eaf7cb3..dea611f 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -2,10 +2,10 @@ ## Current result -The released-package warm-gameplay witness passes with: +The released-package cold world witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.58`, commit - `02203ff134570c301ce58969397c296ee14e69ee`; +- `org.gamecult.eve.unity-scene` `0.3.59`, commit + `4d5257b8ca09767974e8eb350a9695191ec39fdc`; - `org.gamecult.eve.plugin-fields` `0.2.3`, commit `c5a4a75c1b727499b16c2dae1895f29e2a9f72f0`; - `org.gamecult.eve.surface` `0.2.2`, commit @@ -17,16 +17,18 @@ The released-package warm-gameplay witness passes with: - the generic `ReleaseConsumerProject` client connected directly to the Aetheria daemon. -The cold `0.3.58` camera-contract witness in -`artifacts/aetheria-daemon-gravity-fog-historical` resolves the tagged package, -transfers and lowers bundle -`e299b50ded9c9ba8964c567964c30342b9b13323665b20e740210b4806dd6e66`, and -passes in 51.0 seconds. EveUnity now supplies the positive-Z camera transform -used by Unity shader globals, so Aetheria keeps its historical ray direction -instead of compensating for the negative-Z `Camera.cameraToWorldMatrix` API. -The resulting capture exposes a separate unresolved mismatch: restoring the -historical scattering law makes the current projected fog frame severely -overexposed. That is not claimed as visual parity. +The cold `0.3.59` witness in +`artifacts/aetheria-daemon-adaptive-exposure-cold` resolves the tagged package, +transfers and lowers the 56,170,916-byte provider bundle +`34dd156038c9c55b96136e10805f35f2bc7f2aa4e8dda8a519e57aa5a83329c8`, and +passes in 53.3 seconds. EveUnity lowers Aetheria's advertised historical +histogram exposure as a provider-agnostic camera semantic; the bundled static +volume profile remains neutral and is no longer a competing exposure owner. +Average pilot-frame luminance fell from `0.8716` in the restored-scattering +camera witness to `0.5096`, recovering visible cloud structure. The capture is +still cyan and visibly posterized, so temporal/color parity remains open. +Map-channel isolation also passes: the pilot camera excludes the 11 map +renderers and the map camera includes them. Current released-package evidence is in `artifacts/aetheria-daemon-mapped-body-cold`; despite that retained directory diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 95712b6..9ef4862 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "02203ff134570c301ce58969397c296ee14e69ee" +$expectedEveUnityCommit = "4d5257b8ca09767974e8eb350a9695191ec39fdc" $expectedEveFieldsCommit = "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "419053ebe2325848051c4f4d8ba352cd4286c424" From 7588763999b1b0d40aa784dcdd59f8f6936f0f02 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 20:04:19 +0100 Subject: [PATCH 143/292] Honor provider temporal reprojection semantics --- .../Fields/EveUnityFieldsVolumeRenderer.cs | 30 +++++++++++- .../EveFieldsUnitySceneProjectionTests.cs | 49 +++++++++++++++++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs index 061d55c..4f6bb84 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -28,6 +28,7 @@ namespace GameCult.Eve.UnityScene.Fields private readonly RenderTexture?[] _historyTextures = new RenderTexture?[2]; private int _historyTextureIndex = -1; private Matrix4x4 _previousViewProjection; + private Matrix4x4 _previousView; private EveUnityFieldsVolumePass? _renderPass; private string _activeNodeId = ""; private long _rasterizedFrameId = -1; @@ -160,7 +161,15 @@ namespace GameCult.Eve.UnityScene.Fields finalCloudTexture = _historyTextures[historyWriteIndex]!; SetTexturePort("currentSample", _raymarchTexture); SetTexturePort("history", historyReadTexture!); - SetMatrixPort("previousViewProjection", _historyTextureIndex < 0 ? viewProjection : _previousViewProjection); + SetMatrixPort( + "previousViewProjection", + _historyTextureIndex < 0 + ? viewProjection + : ResolvePreviousViewProjection( + _programMetadata, + _previousViewProjection, + gpuProjection, + _previousView)); SetFloatPort("resetHistory", _historyTextureIndex < 0 ? 1f : 0f); } SetTexturePort("cloud", finalCloudTexture); @@ -186,6 +195,7 @@ namespace GameCult.Eve.UnityScene.Fields { _historyTextureIndex = historyWriteIndex; _previousViewProjection = viewProjection; + _previousView = camera.worldToCameraMatrix; } CompositeCount++; } @@ -454,6 +464,20 @@ namespace GameCult.Eve.UnityScene.Fields { _historyTextureIndex = -1; _previousViewProjection = Matrix4x4.identity; + _previousView = Matrix4x4.identity; + } + + public static Matrix4x4 ResolvePreviousViewProjection( + IReadOnlyDictionary? metadata, + Matrix4x4 previousViewProjection, + Matrix4x4 currentGpuProjection, + Matrix4x4 previousView) + { + return metadata != null && + metadata.TryGetValue("unity.volume.matrixSemantic.previousViewProjection", out var semantic) && + string.Equals(semantic, "current-projection.previous-view.v1", StringComparison.Ordinal) + ? currentGpuProjection * previousView + : previousViewProjection; } private bool HasTemporalProgram() => ProgramPass("temporal") >= 0; @@ -532,6 +556,10 @@ namespace GameCult.Eve.UnityScene.Fields !ValidPort(metadata, "matrix", "previousViewProjection") || !ValidPort(metadata, "float", "resetHistory")) return Fail("Native volume temporal pass is missing a required history port.", out error); + if (metadata.TryGetValue("unity.volume.matrixSemantic.previousViewProjection", out var semantic) && + !string.Equals(semantic, "previous-view-projection.v1", StringComparison.Ordinal) && + !string.Equals(semantic, "current-projection.previous-view.v1", StringComparison.Ordinal)) + return Fail("Native volume temporal pass has an unsupported previous-view-projection semantic.", out error); return true; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index 9b8391c..e25a967 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -327,6 +327,55 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(EveUnityFieldsVolumeRenderer.TryValidateProgramMetadata(metadata, out var error), Is.True, error); } + [Test] + public void VolumeRendererUsesCurrentProjectionWithPreviousViewWhenAdvertised() + { + var metadata = RequiredVolumeProgramMetadata(); + metadata["unity.volume.matrixSemantic.previousViewProjection"] = + "current-projection.previous-view.v1"; + var previousViewProjection = Matrix4x4.Translate(new Vector3(1f, 2f, 3f)); + var currentProjection = Matrix4x4.Scale(new Vector3(2f, 3f, 4f)); + var previousView = Matrix4x4.Rotate(Quaternion.Euler(10f, 20f, 30f)); + + var resolved = EveUnityFieldsVolumeRenderer.ResolvePreviousViewProjection( + metadata, + previousViewProjection, + currentProjection, + previousView); + + Assert.That(resolved, Is.EqualTo(currentProjection * previousView)); + Assert.That(resolved, Is.Not.EqualTo(previousViewProjection)); + } + + [Test] + public void VolumeRendererDefaultsToTruePreviousViewProjection() + { + var previousViewProjection = Matrix4x4.Translate(new Vector3(1f, 2f, 3f)); + + var resolved = EveUnityFieldsVolumeRenderer.ResolvePreviousViewProjection( + RequiredVolumeProgramMetadata(), + previousViewProjection, + Matrix4x4.Scale(Vector3.one * 2f), + Matrix4x4.Rotate(Quaternion.Euler(10f, 20f, 30f))); + + Assert.That(resolved, Is.EqualTo(previousViewProjection)); + } + + [Test] + public void VolumeProgramRejectsUnknownPreviousViewProjectionSemantic() + { + var metadata = RequiredVolumeProgramMetadata(); + metadata["unity.volume.pass.temporal"] = "1"; + metadata["unity.volume.texturePort.currentSample"] = "_CurrentVolume"; + metadata["unity.volume.texturePort.history"] = "_HistoryVolume"; + metadata["unity.volume.matrixPort.previousViewProjection"] = "_PreviousViewProjection"; + metadata["unity.volume.floatPort.resetHistory"] = "_ResetHistory"; + metadata["unity.volume.matrixSemantic.previousViewProjection"] = "made-up"; + + Assert.That(EveUnityFieldsVolumeRenderer.TryValidateProgramMetadata(metadata, out var error), Is.False); + Assert.That(error, Does.Contain("previous-view-projection semantic")); + } + [Test] public void VolumeProgramRejectsPartialTemporalLifecycle() { diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 20f654f..e10d407 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.59", + "version": "0.3.60", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 887de62db84526511ae99a9d59429ae05c86476c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 20:13:29 +0100 Subject: [PATCH 144/292] Bootstrap temporal volume history at provider quality --- .../Fields/EveUnityFieldsVolumeRenderer.cs | 38 +++++++++++++++++-- .../EveFieldsUnitySceneProjectionTests.cs | 26 +++++++++++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs index 4f6bb84..6b2760f 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -137,6 +137,7 @@ namespace GameCult.Eve.UnityScene.Fields camera.depthTextureMode |= DepthTextureMode.Depth; EnsureVolumeTextures(camera, field); if (_raymarchTexture == null || !ApplyViewportTextureScaleBindings(field, camera)) return; + ApplyQuality(field, _historyTextureIndex < 0); var gpuProjection = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true); var viewProjection = gpuProjection * camera.worldToCameraMatrix; @@ -244,10 +245,6 @@ namespace GameCult.Eve.UnityScene.Fields SetTexturePort(binding.Value, texture); } } - var quality = Prop(field, "quality").Trim().ToLowerInvariant(); - if (TryProgramValue($"unity.volume.quality.{quality}.keyword", out var keyword) && - !string.IsNullOrWhiteSpace(keyword)) - _material.EnableKeyword(keyword); foreach (var feature in Prop(field, "features").Split(';')) { var logicalFeature = feature.Trim(); @@ -259,6 +256,34 @@ namespace GameCult.Eve.UnityScene.Fields return true; } + private void ApplyQuality(EveUnityFieldVolumeProjection field, bool bootstrap) + { + if (_material == null || _programMetadata == null) return; + foreach (var binding in _programMetadata) + { + if (binding.Key.StartsWith("unity.volume.quality.", StringComparison.Ordinal) && + binding.Key.EndsWith(".keyword", StringComparison.Ordinal) && + !string.IsNullOrWhiteSpace(binding.Value)) + _material.DisableKeyword(binding.Value); + } + var quality = ResolveQuality(_programMetadata, Prop(field, "quality"), bootstrap); + if (TryProgramValue($"unity.volume.quality.{quality}.keyword", out var keyword) && + !string.IsNullOrWhiteSpace(keyword)) + _material.EnableKeyword(keyword); + } + + public static string ResolveQuality( + IReadOnlyDictionary? metadata, + string steadyQuality, + bool bootstrap) + { + if (bootstrap && metadata != null && + metadata.TryGetValue("unity.volume.quality.bootstrap", out var bootstrapQuality) && + !string.IsNullOrWhiteSpace(bootstrapQuality)) + return bootstrapQuality.Trim().ToLowerInvariant(); + return (steadyQuality ?? "").Trim().ToLowerInvariant(); + } + private void ApplyFieldBindings(EveUnityFieldVolumeProjection field, EveFieldsSplatsDocument document) { if (_material == null) return; @@ -560,6 +585,11 @@ namespace GameCult.Eve.UnityScene.Fields !string.Equals(semantic, "previous-view-projection.v1", StringComparison.Ordinal) && !string.Equals(semantic, "current-projection.previous-view.v1", StringComparison.Ordinal)) return Fail("Native volume temporal pass has an unsupported previous-view-projection semantic.", out error); + if (metadata.TryGetValue("unity.volume.quality.bootstrap", out var bootstrapQuality) && + (!metadata.TryGetValue( + $"unity.volume.quality.{bootstrapQuality.Trim().ToLowerInvariant()}.keyword", + out var bootstrapKeyword) || string.IsNullOrWhiteSpace(bootstrapKeyword))) + return Fail("Native volume temporal pass has no keyword for its bootstrap quality.", out error); return true; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index e25a967..7a834b4 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -376,6 +376,32 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(error, Does.Contain("previous-view-projection semantic")); } + [Test] + public void VolumeRendererUsesAdvertisedBootstrapQualityOnlyForEmptyHistory() + { + var metadata = RequiredVolumeProgramMetadata(); + metadata["unity.volume.quality.bootstrap"] = "ultra"; + metadata["unity.volume.quality.ultra.keyword"] = "ULTRA_QUALITY"; + + Assert.That(EveUnityFieldsVolumeRenderer.ResolveQuality(metadata, "high", true), Is.EqualTo("ultra")); + Assert.That(EveUnityFieldsVolumeRenderer.ResolveQuality(metadata, "high", false), Is.EqualTo("high")); + } + + [Test] + public void VolumeProgramRejectsBootstrapQualityWithoutAdvertisedKeyword() + { + var metadata = RequiredVolumeProgramMetadata(); + metadata["unity.volume.pass.temporal"] = "1"; + metadata["unity.volume.texturePort.currentSample"] = "_CurrentVolume"; + metadata["unity.volume.texturePort.history"] = "_HistoryVolume"; + metadata["unity.volume.matrixPort.previousViewProjection"] = "_PreviousViewProjection"; + metadata["unity.volume.floatPort.resetHistory"] = "_ResetHistory"; + metadata["unity.volume.quality.bootstrap"] = "ultra"; + + Assert.That(EveUnityFieldsVolumeRenderer.TryValidateProgramMetadata(metadata, out var error), Is.False); + Assert.That(error, Does.Contain("bootstrap quality")); + } + [Test] public void VolumeProgramRejectsPartialTemporalLifecycle() { diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index e10d407..3372014 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.60", + "version": "0.3.61", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 16199126dd12bbb78cf2b404fac7d82c8ac3c44c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 20:18:39 +0100 Subject: [PATCH 145/292] Pin historical temporal fog release --- ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 ++-- docs/aetheria-released-package-witness.md | 15 +++++++++++++-- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 8745e90..02d2252 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.15", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.59", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.61", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 5a4c1a5..9a585dc 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.59", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.61", "depth": 0, "source": "git", "dependencies": { @@ -155,7 +155,7 @@ "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "4d5257b8ca09767974e8eb350a9695191ec39fdc" + "hash": "887de62db84526511ae99a9d59429ae05c86476c" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index dea611f..cd937e6 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,8 +4,8 @@ The released-package cold world witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.59`, commit - `4d5257b8ca09767974e8eb350a9695191ec39fdc`; +- `org.gamecult.eve.unity-scene` `0.3.61`, commit + `887de62db84526511ae99a9d59429ae05c86476c`; - `org.gamecult.eve.plugin-fields` `0.2.3`, commit `c5a4a75c1b727499b16c2dae1895f29e2a9f72f0`; - `org.gamecult.eve.surface` `0.2.2`, commit @@ -30,6 +30,17 @@ still cyan and visibly posterized, so temporal/color parity remains open. Map-channel isolation also passes: the pilot camera excludes the 11 map renderers and the map camera includes them. +The subsequent released `0.3.61` warm witness in +`artifacts/aetheria-daemon-temporal-bootstrap-warm` restores two temporal +details from the fossil renderer as explicit provider-program semantics. The +fog history projects the previous view through the current projection, and the +first history frame uses the provider-advertised `ultra` quality before +settling to the scene's serialized `high` quality. Average luminance remains +effectively unchanged while mean neighboring-pixel variation falls from +`0.0332` to `0.0174` and Laplacian high-frequency energy falls from `0.1050` +to `0.0531`. This proves the temporal history is accumulating and that its +bootstrap seed mattered. The remaining cyan/tonemap mismatch is still open. + Current released-package evidence is in `artifacts/aetheria-daemon-mapped-body-cold`; despite that retained directory name, `runtime-witness.warm.json` is the current passing run. The warm PlayMode test diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 9ef4862..6cfe2c6 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "4d5257b8ca09767974e8eb350a9695191ec39fdc" +$expectedEveUnityCommit = "887de62db84526511ae99a9d59429ae05c86476c" $expectedEveFieldsCommit = "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "419053ebe2325848051c4f4d8ba352cd4286c424" From b25d9140499be657c746281ed787908d6967ce13 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 20:40:40 +0100 Subject: [PATCH 146/292] Capture settled HD temporal witness --- .../PlayMode/GenericWorldCaptureTests.cs | 23 +++++++++++++++---- .../PlayMode/GenericWorldCaptureTests.cs | 23 +++++++++++++++---- docs/aetheria-released-package-witness.md | 12 ++++++++++ scripts/run-aetheria-daemon-world-witness.ps1 | 12 +++++----- 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 253e8a7..144c9bf 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -638,7 +638,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } Assert.That(mapRenderers, Is.Not.Empty, "The live provider prefab has no renderer assigned to its advertised map channel."); - target = new RenderTexture(640, 360, 24, RenderTextureFormat.ARGB32); + const int captureWidth = 1280; + const int captureHeight = 720; + target = new RenderTexture(captureWidth, captureHeight, 24, RenderTextureFormat.ARGB32); camera.targetTexture = target; var cameraRig = cameraObject.AddComponent(); cameraRig.Host = host; @@ -697,11 +699,22 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var mapCamera = mapCameraObject.AddComponent(); mapCamera.cullingMask = 1 << mapLayer; Assert.That(mapCamera.cullingMask, Is.EqualTo(1 << mapLayer)); - yield return null; + var captureFieldVolume = root.GetComponent(); + Assert.That(captureFieldVolume, Is.Not.Null, + "The generic client did not install its Fields volume lowerer before temporal settling."); + const int temporalSettlingFrames = 128; + var initialCompositeCount = captureFieldVolume.CompositeCount; + var temporalSettlingDeadline = Time.realtimeSinceStartup + 20f; + while (captureFieldVolume.CompositeCount - initialCompositeCount < temporalSettlingFrames && + Time.realtimeSinceStartup < temporalSettlingDeadline) + yield return null; + Assert.That(captureFieldVolume.CompositeCount - initialCompositeCount, + Is.GreaterThanOrEqualTo(temporalSettlingFrames), + "The pilot capture ran before the advertised temporal reconstruction had settled."); camera.Render(); RenderTexture.active = target; - pixels = new Texture2D(640, 360, TextureFormat.RGB24, false); - pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); + pixels = new Texture2D(captureWidth, captureHeight, TextureFormat.RGB24, false); + pixels.ReadPixels(new Rect(0, 0, captureWidth, captureHeight), 0, 0); pixels.Apply(); var pilotChangedPixels = CountChangedPixels(pixels, camera.backgroundColor); MeasureLuminance(pixels, out var pilotAverageLuminance, out var pilotBrightPixelCount); @@ -749,7 +762,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(particleMapCameraIsolated, Is.True, "The map camera executed the pilot-only field-particle pass."); RenderTexture.active = target; - pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); + pixels.ReadPixels(new Rect(0, 0, captureWidth, captureHeight), 0, 0); pixels.Apply(); var mapChangedPixels = CountChangedPixels(pixels, mapCamera.backgroundColor); Assert.That(mapChangedPixels, Is.GreaterThan(25), "The map-only camera rendered no provider-authored map glyph pixels."); diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 253e8a7..144c9bf 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -638,7 +638,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } Assert.That(mapRenderers, Is.Not.Empty, "The live provider prefab has no renderer assigned to its advertised map channel."); - target = new RenderTexture(640, 360, 24, RenderTextureFormat.ARGB32); + const int captureWidth = 1280; + const int captureHeight = 720; + target = new RenderTexture(captureWidth, captureHeight, 24, RenderTextureFormat.ARGB32); camera.targetTexture = target; var cameraRig = cameraObject.AddComponent(); cameraRig.Host = host; @@ -697,11 +699,22 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var mapCamera = mapCameraObject.AddComponent(); mapCamera.cullingMask = 1 << mapLayer; Assert.That(mapCamera.cullingMask, Is.EqualTo(1 << mapLayer)); - yield return null; + var captureFieldVolume = root.GetComponent(); + Assert.That(captureFieldVolume, Is.Not.Null, + "The generic client did not install its Fields volume lowerer before temporal settling."); + const int temporalSettlingFrames = 128; + var initialCompositeCount = captureFieldVolume.CompositeCount; + var temporalSettlingDeadline = Time.realtimeSinceStartup + 20f; + while (captureFieldVolume.CompositeCount - initialCompositeCount < temporalSettlingFrames && + Time.realtimeSinceStartup < temporalSettlingDeadline) + yield return null; + Assert.That(captureFieldVolume.CompositeCount - initialCompositeCount, + Is.GreaterThanOrEqualTo(temporalSettlingFrames), + "The pilot capture ran before the advertised temporal reconstruction had settled."); camera.Render(); RenderTexture.active = target; - pixels = new Texture2D(640, 360, TextureFormat.RGB24, false); - pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); + pixels = new Texture2D(captureWidth, captureHeight, TextureFormat.RGB24, false); + pixels.ReadPixels(new Rect(0, 0, captureWidth, captureHeight), 0, 0); pixels.Apply(); var pilotChangedPixels = CountChangedPixels(pixels, camera.backgroundColor); MeasureLuminance(pixels, out var pilotAverageLuminance, out var pilotBrightPixelCount); @@ -749,7 +762,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(particleMapCameraIsolated, Is.True, "The map camera executed the pilot-only field-particle pass."); RenderTexture.active = target; - pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); + pixels.ReadPixels(new Rect(0, 0, captureWidth, captureHeight), 0, 0); pixels.Apply(); var mapChangedPixels = CountChangedPixels(pixels, mapCamera.backgroundColor); Assert.That(mapChangedPixels, Is.GreaterThan(25), "The map-only camera rendered no provider-authored map glyph pixels."); diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index cd937e6..fe399f8 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -41,6 +41,18 @@ effectively unchanged while mean neighboring-pixel variation falls from to `0.0531`. This proves the temporal history is accumulating and that its bootstrap seed mattered. The remaining cyan/tonemap mismatch is still open. +The cold HD witness in `artifacts/aetheria-daemon-fog-alpha-cold-hd` then +transfers the rebuilt 56,168,970-byte provider bundle +`a829e57731fbe08f4564fa131468148e99f6b4204708ad3e64498885532b5091` +through `SharedFileMapping` and captures after 128 temporal composites at +1280x720. It restores the historical fog history ABI: the temporal pass stores +density directly in alpha, so the composite consumes that density directly +instead of decoding it as the raymarch pass's packed distance/density payload. +The flat grey opacity slabs disappear. The released client test passes in 52.7 +seconds with 238 fog composites and Stardust draws; the pilot camera still +excludes map renderers and the map camera includes them. Remaining visual work +is color/composition parity, not broken fog opacity or an unsettled witness. + Current released-package evidence is in `artifacts/aetheria-daemon-mapped-body-cold`; despite that retained directory name, `runtime-witness.warm.json` is the current passing run. The warm PlayMode test diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 6cfe2c6..f45e3db 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -346,8 +346,8 @@ try { assertions = $facts receipts = @($facts.receipts) screenshotMetrics = [ordered]@{ - width = 640 - height = 360 + width = 1280 + height = 720 encodedSizeBytes = $capture.Length sha256 = (Get-FileHash -LiteralPath $capture.FullName -Algorithm SHA256).Hash.ToLowerInvariant() mapEncodedSizeBytes = $mapCapture.Length @@ -360,16 +360,16 @@ try { path = $capture.Name sha256 = (Get-FileHash -LiteralPath $capture.FullName -Algorithm SHA256).Hash.ToLowerInvariant() sizeBytes = $capture.Length - width = 640 - height = 360 + width = 1280 + height = 720 }, [ordered]@{ kind = "unity-map-channel-png" path = $mapCapture.Name sha256 = (Get-FileHash -LiteralPath $mapCapture.FullName -Algorithm SHA256).Hash.ToLowerInvariant() sizeBytes = $mapCapture.Length - width = 640 - height = 360 + width = 1280 + height = 720 }, [ordered]@{ kind = "unity-test-results" From ed92fa7be0e2e7e386ab50ad36d6fd954d2d86a5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 20:49:22 +0100 Subject: [PATCH 147/292] Lower advertised HDR grading space --- .../Runtime/EveUnityPlayableWorldCameraRig.cs | 38 +++++++++++++++++++ .../Runtime/EveUnitySceneSurfaceLowerer.cs | 9 ++++- .../EveUnitySceneSurfaceLowererTests.cs | 22 ++++++++++- .../org.gamecult.eve.unity-scene/package.json | 2 +- 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index 01f0b81..19d1a63 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -43,6 +43,8 @@ namespace GameCult.Eve.UnityScene private bool _previousRenderPostProcessing; private AntialiasingMode _previousAntialiasing; private TemporalAA.Settings _previousTaaSettings; + private UniversalRenderPipelineAsset? _colorGradingPipelineAsset; + private ColorGradingMode _previousColorGradingMode; public EveUnityPlayableWorldClientHost? Host { @@ -141,6 +143,11 @@ namespace GameCult.Eve.UnityScene ReleaseRig(); return false; } + if (!ApplyColorGradingSpace(activeWorld)) + { + ReleaseRig(); + return false; + } ApplyAmbientEnvironment(activeWorld, cameraComponent, skybox, reflection, postProcessProfile); ApplyKeyLight(activeWorld); if (cameraComponent != null && _renderPolicySource != null) @@ -316,6 +323,8 @@ namespace GameCult.Eve.UnityScene !IsFinite(world.TemporalSharpening) || (!string.IsNullOrWhiteSpace(world.CameraReconstruction) && !string.Equals(world.CameraReconstruction, "temporal-reprojection.v1", System.StringComparison.Ordinal)) || + (!string.IsNullOrWhiteSpace(world.ColorGradingSpace) && + !string.Equals(world.ColorGradingSpace, "hdr-before-tonemap.v1", System.StringComparison.Ordinal)) || (string.Equals(world.CameraReconstruction, "temporal-reprojection.v1", System.StringComparison.Ordinal) && (world.TemporalHistoryBlend < 0f || world.TemporalHistoryBlend > 1f || world.TemporalJitterScale < 0f || world.TemporalJitterScale > 1f || @@ -359,6 +368,34 @@ namespace GameCult.Eve.UnityScene return true; } + private bool ApplyColorGradingSpace(EveUnityPlayableWorldProjection world) + { + if (string.IsNullOrWhiteSpace(world.ColorGradingSpace)) + { + RestoreColorGradingSpace(); + return true; + } + var asset = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset; + if (asset == null || + !string.Equals(world.ColorGradingSpace, "hdr-before-tonemap.v1", System.StringComparison.Ordinal)) + return false; + if (!ReferenceEquals(_colorGradingPipelineAsset, asset)) + { + RestoreColorGradingSpace(); + _colorGradingPipelineAsset = asset; + _previousColorGradingMode = asset.colorGradingMode; + } + asset.colorGradingMode = ColorGradingMode.HighDynamicRange; + return true; + } + + private void RestoreColorGradingSpace() + { + if (_colorGradingPipelineAsset == null) return; + _colorGradingPipelineAsset.colorGradingMode = _previousColorGradingMode; + _colorGradingPipelineAsset = null; + } + private void ApplyKeyLight(EveUnityPlayableWorldProjection world) { var direction = new Vector3( @@ -574,6 +611,7 @@ namespace GameCult.Eve.UnityScene private void RestorePostProcess() { + RestoreColorGradingSpace(); ReleaseAdaptiveExposure(); if (_postProcessCameraData != null) { diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index 374750c..713085d 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -170,7 +170,8 @@ namespace GameCult.Eve.UnityScene ParseFloat(worldRoot.GetProp("exposureKeyValue"), 1f), worldRoot.GetProp("exposureAdaptation", "progressive"), ParseFloat(worldRoot.GetProp("exposureSpeedUp"), 2f), - ParseFloat(worldRoot.GetProp("exposureSpeedDown"), 1f)); + ParseFloat(worldRoot.GetProp("exposureSpeedDown"), 1f), + worldRoot.GetProp("colorGradingSpace")); } private static EveUnityFieldVolumeProjection BuildFieldVolume(EveSurfaceComponent component) => @@ -439,7 +440,8 @@ namespace GameCult.Eve.UnityScene float exposureKeyValue = 1f, string exposureAdaptation = "progressive", float exposureSpeedUp = 2f, - float exposureSpeedDown = 1f) + float exposureSpeedDown = 1f, + string colorGradingSpace = "") { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -501,6 +503,7 @@ namespace GameCult.Eve.UnityScene ExposureAdaptation = exposureAdaptation ?? ""; ExposureSpeedUp = exposureSpeedUp; ExposureSpeedDown = exposureSpeedDown; + ColorGradingSpace = colorGradingSpace ?? ""; } public string WorldRootId { get; } @@ -537,6 +540,8 @@ namespace GameCult.Eve.UnityScene public string ExposureMode { get; } + public string ColorGradingSpace { get; } + public float ExposureLowPercent { get; } public float ExposureHighPercent { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index fa24946..88e0841 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -1133,6 +1133,14 @@ namespace GameCult.Eve.UnityScene.Tests var previousReflectionMode = RenderSettings.defaultReflectionMode; var previousCustomReflection = RenderSettings.customReflectionTexture; var previousReflectionIntensity = RenderSettings.reflectionIntensity; + var previousDefaultPipeline = GraphicsSettings.defaultRenderPipeline; + var previousQualityPipeline = QualitySettings.renderPipeline; + var gradingRendererData = ScriptableObject.CreateInstance(); + var gradingPipelineAsset = UniversalRenderPipelineAsset.Create(gradingRendererData); + gradingPipelineAsset.colorGradingMode = ColorGradingMode.LowDynamicRange; + GraphicsSettings.defaultRenderPipeline = gradingPipelineAsset; + QualitySettings.renderPipeline = gradingPipelineAsset; + var previousGradingMode = gradingPipelineAsset.colorGradingMode; Material? skyboxMaterial = null; Cubemap? reflectionCubemap = null; VolumeProfile? postProcessProfile = null; @@ -1168,7 +1176,8 @@ namespace GameCult.Eve.UnityScene.Tests exposureKeyValue: "0.5", exposureAdaptation: "progressive", exposureSpeedUp: "2", - exposureSpeedDown: "1"), + exposureSpeedDown: "1", + colorGradingSpace: "hdr-before-tonemap.v1"), Advertisement("aetheria.daemon.game"), "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", 1), @@ -1184,6 +1193,7 @@ namespace GameCult.Eve.UnityScene.Tests var exposedWorld = host.ActiveProjection?.PlayableWorld; Assert.That(exposedWorld, Is.Not.Null); Assert.That(exposedWorld!.ExposureMode, Is.EqualTo("histogram.v1")); + Assert.That(exposedWorld.ColorGradingSpace, Is.EqualTo("hdr-before-tonemap.v1")); Assert.That(exposedWorld.ExposureLowPercent, Is.EqualTo(47.37294f).Within(0.00001f)); Assert.That(exposedWorld.ExposureHighPercent, Is.EqualTo(99f)); Assert.That(exposedWorld.ExposureMinimumEv, Is.EqualTo(-3f)); @@ -1240,6 +1250,7 @@ namespace GameCult.Eve.UnityScene.Tests var adaptiveExposure = camera.GetComponent(); Assert.That(adaptiveExposure, Is.Not.Null); Assert.That(adaptiveExposure.IsConfigured, Is.True); + Assert.That(gradingPipelineAsset.colorGradingMode, Is.EqualTo(ColorGradingMode.HighDynamicRange)); var postProcessTransform = hostObject.transform.Find("Eve World Post Process"); Assert.That(postProcessTransform, Is.Not.Null); Assert.That(postProcessTransform.GetComponent().sharedProfile, Is.SameAs(postProcessProfile)); @@ -1305,6 +1316,7 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(hostObject.transform.Find("Eve World Key Light"), Is.Null); Assert.That(hostObject.transform.Find("Eve World Post Process"), Is.Null); Assert.That(camera.GetComponent(), Is.Null); + Assert.That(gradingPipelineAsset.colorGradingMode, Is.EqualTo(previousGradingMode)); aim.RefreshNow(); Assert.That(aim.ViewDotVisible, Is.False); Assert.That(RenderSettings.ambientMode, Is.EqualTo(ambientMode)); @@ -1335,6 +1347,10 @@ namespace GameCult.Eve.UnityScene.Tests RenderSettings.defaultReflectionMode = previousReflectionMode; RenderSettings.customReflectionTexture = previousCustomReflection; RenderSettings.reflectionIntensity = previousReflectionIntensity; + QualitySettings.renderPipeline = previousQualityPipeline; + GraphicsSettings.defaultRenderPipeline = previousDefaultPipeline; + UnityEngine.Object.DestroyImmediate(gradingPipelineAsset); + UnityEngine.Object.DestroyImmediate(gradingRendererData); if (skyboxMaterial != null) UnityEngine.Object.DestroyImmediate(skyboxMaterial); if (reflectionCubemap != null) UnityEngine.Object.DestroyImmediate(reflectionCubemap); if (postProcessProfile != null) UnityEngine.Object.DestroyImmediate(postProcessProfile); @@ -1942,7 +1958,8 @@ namespace GameCult.Eve.UnityScene.Tests string exposureKeyValue = "1", string exposureAdaptation = "progressive", string exposureSpeedUp = "2", - string exposureSpeedDown = "1") + string exposureSpeedDown = "1", + string colorGradingSpace = "") { var playableChildren = new List { @@ -2124,6 +2141,7 @@ namespace GameCult.Eve.UnityScene.Tests ["exposureAdaptation"] = exposureAdaptation, ["exposureSpeedUp"] = exposureSpeedUp, ["exposureSpeedDown"] = exposureSpeedDown, + ["colorGradingSpace"] = colorGradingSpace, ["keyLightDirection"] = "0.4,-1,0.25", ["keyLightColor"] = "1,0.95,0.9", ["keyLightIntensity"] = "0.75", diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 3372014..8e4e5b7 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.61", + "version": "0.3.62", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From db9dcb866f76e0d4ede009c9524a1286a80ab905 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 20:57:34 +0100 Subject: [PATCH 148/292] Pin and prove HDR grading release --- .../Tests/PlayMode/GenericWorldCaptureTests.cs | 5 +++++ ReleaseConsumerProject/Packages/manifest.json | 2 +- ReleaseConsumerProject/Packages/packages-lock.json | 4 ++-- .../Tests/PlayMode/GenericWorldCaptureTests.cs | 5 +++++ docs/aetheria-released-package-witness.md | 12 ++++++++++-- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 144c9bf..5fe6c4a 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -647,6 +647,11 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests cameraRig.CameraTransform = camera.transform; cameraRig.RenderPolicySource = provider; Assert.That(cameraRig.ApplyRig(0f), Is.True); + var activePipelineAsset = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset; + Assert.That(activePipelineAsset, Is.Not.Null, + "The generic client has no active URP asset for the advertised grading space."); + Assert.That(activePipelineAsset.colorGradingMode, Is.EqualTo(ColorGradingMode.HighDynamicRange), + "The advertised HDR-before-tonemap grading space did not reach the active URP pipeline."); var reflection = provider.ResolveAsset(new EveUnityPlayableWorldAssetBinding( runtime.ActiveWorld.ReflectionAssetRef, "", "provider-asset-ref"), typeof(Cubemap)) as Cubemap; Assert.That(runtime.ActiveWorld.SkyboxAssetRef, Is.Empty, diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 02d2252..3c9427b 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.15", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.61", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.62", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 9a585dc..92907d8 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.61", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.62", "depth": 0, "source": "git", "dependencies": { @@ -155,7 +155,7 @@ "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "887de62db84526511ae99a9d59429ae05c86476c" + "hash": "ed92fa7be0e2e7e386ab50ad36d6fd954d2d86a5" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 144c9bf..5fe6c4a 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -647,6 +647,11 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests cameraRig.CameraTransform = camera.transform; cameraRig.RenderPolicySource = provider; Assert.That(cameraRig.ApplyRig(0f), Is.True); + var activePipelineAsset = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset; + Assert.That(activePipelineAsset, Is.Not.Null, + "The generic client has no active URP asset for the advertised grading space."); + Assert.That(activePipelineAsset.colorGradingMode, Is.EqualTo(ColorGradingMode.HighDynamicRange), + "The advertised HDR-before-tonemap grading space did not reach the active URP pipeline."); var reflection = provider.ResolveAsset(new EveUnityPlayableWorldAssetBinding( runtime.ActiveWorld.ReflectionAssetRef, "", "provider-asset-ref"), typeof(Cubemap)) as Cubemap; Assert.That(runtime.ActiveWorld.SkyboxAssetRef, Is.Empty, diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index fe399f8..aa04bc2 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,8 +4,8 @@ The released-package cold world witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.61`, commit - `887de62db84526511ae99a9d59429ae05c86476c`; +- `org.gamecult.eve.unity-scene` `0.3.62`, commit + `ed92fa7be0e2e7e386ab50ad36d6fd954d2d86a5`; - `org.gamecult.eve.plugin-fields` `0.2.3`, commit `c5a4a75c1b727499b16c2dae1895f29e2a9f72f0`; - `org.gamecult.eve.surface` `0.2.2`, commit @@ -53,6 +53,14 @@ seconds with 238 fog composites and Stardust draws; the pilot camera still excludes map renderers and the map camera includes them. Remaining visual work is color/composition parity, not broken fog opacity or an unsettled witness. +The released `0.3.62` warm HD witness in +`artifacts/aetheria-daemon-hdr-grading-warm-hd` lowers the provider's +`hdr-before-tonemap.v1` grading-space contract. This restores the fossil +profile's HDR ordering—grading before ACES—rather than inheriting the released +consumer project's default LDR grading after tonemapping. EveUnity restores the +consumer's prior pipeline mode when the camera is released and fails closed +when the requested grading space cannot be lowered. + Current released-package evidence is in `artifacts/aetheria-daemon-mapped-body-cold`; despite that retained directory name, `runtime-witness.warm.json` is the current passing run. The warm PlayMode test diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index f45e3db..5773583 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "887de62db84526511ae99a9d59429ae05c86476c" +$expectedEveUnityCommit = "ed92fa7be0e2e7e386ab50ad36d6fd954d2d86a5" $expectedEveFieldsCommit = "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "419053ebe2325848051c4f4d8ba352cd4286c424" From 3763383854eabaced559c236f44ec5a95566ace7 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 20:59:52 +0100 Subject: [PATCH 149/292] Record current contact-gated loot witness --- docs/aetheria-released-package-witness.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index aa04bc2..fc6de39 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -61,6 +61,15 @@ consumer project's default LDR grading after tonemapping. EveUnity restores the consumer's prior pipeline mode when the camera is released and fails closed when the requested grading space cannot be lowered. +The asserted rerun in `artifacts/aetheria-daemon-hdr-grading-asserted-warm-hd` +also completes the live destruction-loot path on the same released client. One +daemon destruction produces the canonical pickup; tractor power reaches `1` +and returns to `0`; one Ymir `begin` fact produces exactly one +`pickup.collected` event; cargo changes from `0` to `1`; and no client pickup +command participates in the transaction. The event identity begins +`ymir-fact:` and contains the retained Box3D session, step, contact episode, +and begin-fact identity. + Current released-package evidence is in `artifacts/aetheria-daemon-mapped-body-cold`; despite that retained directory name, `runtime-witness.warm.json` is the current passing run. The warm PlayMode test From 3135d24bb84f40719b121b51634811c85af0a7af Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 21:41:18 +0100 Subject: [PATCH 150/292] Preserve provider temporal projection convention --- .../org.gamecult.eve.unity-scene/README.md | 6 +++++ .../Fields/EveUnityFieldsVolumeRenderer.cs | 19 ++++++++++----- .../EveFieldsUnitySceneProjectionTests.cs | 24 +++++++++++++++++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index 40f8f0b..765188c 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -82,6 +82,12 @@ the field textures it samples. A `world.scene3d` may also request quality, and sharpening values to URP TAA and restores the previous camera state when the world releases the camera. +Native volume programs also advertise the projection convention consumed by +their history matrix. `non-render-target-projection.previous-view.v1` preserves +the projection convention used by legacy `Graphics.Blit` temporal shaders even +when EveUnity executes the program through a render-target-backed URP pass; +providers must not rely on a runtime guessing that Y-axis convention. + `field.particles3d` may advertise provider material textures, viewport-relative texture scales, and a logical render-frame index port. These are presentation inputs: EveUnity binds the selected provider assets and current display frame diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs index 6b2760f..4cfd8d1 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -140,6 +140,7 @@ namespace GameCult.Eve.UnityScene.Fields ApplyQuality(field, _historyTextureIndex < 0); var gpuProjection = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true); + var nonRenderTargetProjection = GL.GetGPUProjectionMatrix(camera.projectionMatrix, false); var viewProjection = gpuProjection * camera.worldToCameraMatrix; SetMatrixPort("cameraInverseViewProjection", viewProjection.inverse); SetMatrixPort("cameraToWorld", CameraTransformToWorld(camera)); @@ -170,6 +171,7 @@ namespace GameCult.Eve.UnityScene.Fields _programMetadata, _previousViewProjection, gpuProjection, + nonRenderTargetProjection, _previousView)); SetFloatPort("resetHistory", _historyTextureIndex < 0 ? 1f : 0f); } @@ -496,13 +498,17 @@ namespace GameCult.Eve.UnityScene.Fields IReadOnlyDictionary? metadata, Matrix4x4 previousViewProjection, Matrix4x4 currentGpuProjection, + Matrix4x4 currentNonRenderTargetProjection, Matrix4x4 previousView) { - return metadata != null && - metadata.TryGetValue("unity.volume.matrixSemantic.previousViewProjection", out var semantic) && - string.Equals(semantic, "current-projection.previous-view.v1", StringComparison.Ordinal) - ? currentGpuProjection * previousView - : previousViewProjection; + if (metadata == null || + !metadata.TryGetValue("unity.volume.matrixSemantic.previousViewProjection", out var semantic)) + return previousViewProjection; + if (string.Equals(semantic, "current-projection.previous-view.v1", StringComparison.Ordinal)) + return currentGpuProjection * previousView; + if (string.Equals(semantic, "non-render-target-projection.previous-view.v1", StringComparison.Ordinal)) + return currentNonRenderTargetProjection * previousView; + return previousViewProjection; } private bool HasTemporalProgram() => ProgramPass("temporal") >= 0; @@ -583,7 +589,8 @@ namespace GameCult.Eve.UnityScene.Fields return Fail("Native volume temporal pass is missing a required history port.", out error); if (metadata.TryGetValue("unity.volume.matrixSemantic.previousViewProjection", out var semantic) && !string.Equals(semantic, "previous-view-projection.v1", StringComparison.Ordinal) && - !string.Equals(semantic, "current-projection.previous-view.v1", StringComparison.Ordinal)) + !string.Equals(semantic, "current-projection.previous-view.v1", StringComparison.Ordinal) && + !string.Equals(semantic, "non-render-target-projection.previous-view.v1", StringComparison.Ordinal)) return Fail("Native volume temporal pass has an unsupported previous-view-projection semantic.", out error); if (metadata.TryGetValue("unity.volume.quality.bootstrap", out var bootstrapQuality) && (!metadata.TryGetValue( diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index 7a834b4..242112d 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -341,6 +341,7 @@ namespace GameCult.Eve.UnityScene.Tests metadata, previousViewProjection, currentProjection, + Matrix4x4.identity, previousView); Assert.That(resolved, Is.EqualTo(currentProjection * previousView)); @@ -356,11 +357,34 @@ namespace GameCult.Eve.UnityScene.Tests RequiredVolumeProgramMetadata(), previousViewProjection, Matrix4x4.Scale(Vector3.one * 2f), + Matrix4x4.identity, Matrix4x4.Rotate(Quaternion.Euler(10f, 20f, 30f))); Assert.That(resolved, Is.EqualTo(previousViewProjection)); } + [Test] + public void VolumeRendererUsesNonRenderTargetProjectionWhenAdvertised() + { + var metadata = RequiredVolumeProgramMetadata(); + metadata["unity.volume.matrixSemantic.previousViewProjection"] = + "non-render-target-projection.previous-view.v1"; + var previousViewProjection = Matrix4x4.Translate(new Vector3(1f, 2f, 3f)); + var renderTargetProjection = Matrix4x4.Scale(new Vector3(2f, -3f, 4f)); + var nonRenderTargetProjection = Matrix4x4.Scale(new Vector3(2f, 3f, 4f)); + var previousView = Matrix4x4.Rotate(Quaternion.Euler(10f, 20f, 30f)); + + var resolved = EveUnityFieldsVolumeRenderer.ResolvePreviousViewProjection( + metadata, + previousViewProjection, + renderTargetProjection, + nonRenderTargetProjection, + previousView); + + Assert.That(resolved, Is.EqualTo(nonRenderTargetProjection * previousView)); + Assert.That(resolved, Is.Not.EqualTo(renderTargetProjection * previousView)); + } + [Test] public void VolumeProgramRejectsUnknownPreviousViewProjectionSemantic() { diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 8e4e5b7..c9bcdb6 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.62", + "version": "0.3.63", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { From 190f5871edc4ec46ef1fc8609a3d308389d8ee13 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 21:58:52 +0100 Subject: [PATCH 151/292] Pin and prove temporal projection release --- .../Tests/PlayMode/GenericWorldCaptureTests.cs | 2 +- ReleaseConsumerProject/Packages/manifest.json | 2 +- .../Packages/packages-lock.json | 4 ++-- .../Tests/PlayMode/GenericWorldCaptureTests.cs | 2 +- docs/aetheria-released-package-witness.md | 18 ++++++++++++++++-- scripts/run-aetheria-daemon-world-witness.ps1 | 2 +- 6 files changed, 22 insertions(+), 8 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 5fe6c4a..f98cfd1 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -716,7 +716,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(captureFieldVolume.CompositeCount - initialCompositeCount, Is.GreaterThanOrEqualTo(temporalSettlingFrames), "The pilot capture ran before the advertised temporal reconstruction had settled."); - camera.Render(); + yield return null; RenderTexture.active = target; pixels = new Texture2D(captureWidth, captureHeight, TextureFormat.RGB24, false); pixels.ReadPixels(new Rect(0, 0, captureWidth, captureHeight), 0, 0); diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index 3c9427b..f27304a 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.15", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.62", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.63", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 92907d8..9c3c276 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.62", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.63", "depth": 0, "source": "git", "dependencies": { @@ -155,7 +155,7 @@ "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "ed92fa7be0e2e7e386ab50ad36d6fd954d2d86a5" + "hash": "3135d24bb84f40719b121b51634811c85af0a7af" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 5fe6c4a..f98cfd1 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -716,7 +716,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(captureFieldVolume.CompositeCount - initialCompositeCount, Is.GreaterThanOrEqualTo(temporalSettlingFrames), "The pilot capture ran before the advertised temporal reconstruction had settled."); - camera.Render(); + yield return null; RenderTexture.active = target; pixels = new Texture2D(captureWidth, captureHeight, TextureFormat.RGB24, false); pixels.ReadPixels(new Rect(0, 0, captureWidth, captureHeight), 0, 0); diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index fc6de39..a71e860 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,8 +4,8 @@ The released-package cold world witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.62`, commit - `ed92fa7be0e2e7e386ab50ad36d6fd954d2d86a5`; +- `org.gamecult.eve.unity-scene` `0.3.63`, commit + `3135d24bb84f40719b121b51634811c85af0a7af`; - `org.gamecult.eve.plugin-fields` `0.2.3`, commit `c5a4a75c1b727499b16c2dae1895f29e2a9f72f0`; - `org.gamecult.eve.surface` `0.2.2`, commit @@ -70,6 +70,20 @@ command participates in the transaction. The event identity begins `ymir-fact:` and contains the retained Box3D session, step, contact episode, and begin-fact identity. +The released `0.3.63` warm HD witness in +`artifacts/aetheria-daemon-temporal-projection-0363-warm-hd` makes the native +volume history projection convention an explicit provider-program semantic. +Aetheria advertises the non-render-target projection convention used by its +historical `Graphics.Blit` temporal pass; the generic URP lowerer applies that +convention without naming Aetheria or interpreting fog. The witness captures +the naturally rendered camera target after 359 volume composites instead of +issuing a second out-of-band `Camera.Render()`. Package tests pass 105/105 and +the released gameplay witness still proves movement, combat, destruction loot, +one Ymir contact collection, held tractor input, and camera-channel isolation. +The remaining visual controls (fog parameters, stellar tint, ambient intensity, +histogram exposure, grading, and temporal settings) remain provider-owned +levers; this proof does not claim a final art grade. + Current released-package evidence is in `artifacts/aetheria-daemon-mapped-body-cold`; despite that retained directory name, `runtime-witness.warm.json` is the current passing run. The warm PlayMode test diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 5773583..86fb3a0 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "ed92fa7be0e2e7e386ab50ad36d6fd954d2d86a5" +$expectedEveUnityCommit = "3135d24bb84f40719b121b51634811c85af0a7af" $expectedEveFieldsCommit = "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "419053ebe2325848051c4f4d8ba352cd4286c424" From e73da9faf5611bd9ba43b7469fe50cb1df58fa38 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 22:26:29 +0100 Subject: [PATCH 152/292] Prove contact-gated cargo rejection --- .../PlayMode/GenericWorldCaptureTests.cs | 131 ++++++++++++++---- .../PlayMode/GenericWorldCaptureTests.cs | 131 ++++++++++++++---- docs/aetheria-released-package-witness.md | 15 +- scripts/run-aetheria-daemon-world-witness.ps1 | 32 ++++- 4 files changed, 240 insertions(+), 69 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index f98cfd1..152bac1 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -210,7 +210,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float tractorReleasedPower = 1f; int tractorBeamParticleSystemCount = 0; EveUnityFeedbackEvent pickupCollection = null; + EveUnityFeedbackEvent pickupRejection = null; int pickupCollectionEventCount = 0; + int pickupRejectionEventCount = 0; + var pickupRejectionEventIds = new HashSet(StringComparer.Ordinal); int initialPickupEntityCount = 0; int finalPickupEntityCount = -1; bool destructionPickupObserved = false; @@ -225,6 +228,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests witnessProfile, "full-session-gameplay", StringComparison.Ordinal); + var gameplayScenario = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_GAMEPLAY_SCENARIO") ?? + "released-client-proof"; + Assert.That(gameplayScenario, + Is.EqualTo("released-client-proof").Or.EqualTo("cargo-capacity-rejection-proof"), + "The live witness must select an explicit daemon-owned gameplay scenario."); + var expectCargoRejection = string.Equals( + gameplayScenario, + "cargo-capacity-rejection-proof", + StringComparison.Ordinal); EveUnityAimPresentationRenderer aimRenderer = null; EveUnityBeamPresentation beamPresentation = null; EveUnityBeamPresentationRenderer beamRenderer = null; @@ -311,16 +323,25 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests if (requireSessionGameplay) Assert.That(initialPickupEntityCount, Is.Zero, "The witness scenario must obtain salvage from daemon-owned destruction, not a boot-seeded pickup."); - runtime.FeedbackAvailable += feedback => - { - if (!string.Equals(feedback.Kind, "pickup.collected", StringComparison.Ordinal)) return; - pickupCollectionEventCount++; - pickupCollection = feedback; - }; - var playerId = runtime.ActiveWorld.PlayerEntityId; var playerFact = host.PresentedEntities.CurrentGeneration.Entities .Single(entity => entity.EntityId == playerId); + runtime.FeedbackAvailable += feedback => + { + if (feedback.TargetEntityIndex != playerFact.SourceIndex) return; + if (string.Equals(feedback.Kind, "pickup.collected", StringComparison.Ordinal)) + { + pickupCollectionEventCount++; + pickupCollection = feedback; + } + else if (string.Equals(feedback.Kind, "pickup.rejected", StringComparison.Ordinal)) + { + pickupRejectionEventCount++; + pickupRejectionEventIds.Add(feedback.EventId); + pickupRejection = feedback; + } + }; + var playerPrefab = provider.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( playerFact.AssetRef, playerFact.EntityKind, @@ -529,7 +550,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests (observedShot == null || host.PresentedEntities.CurrentGeneration.Entities.Any(entity => entity.EntityId == targetEntityId) || - (!destructionPickupObserved && pickupCollection == null))) + (!destructionPickupObserved && pickupCollection == null && pickupRejection == null))) { yield return new WaitForSecondsRealtime(0.05f); runtime.Refresh(); @@ -544,11 +565,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(host.PresentedEntities.CurrentGeneration.Entities.Any(entity => entity.EntityId == targetEntityId), Is.False, "The low-hull witness raider was not destroyed by daemon combat."); - Assert.That(destructionPickupObserved || pickupCollection != null, Is.True, + Assert.That(destructionPickupObserved || pickupCollection != null || pickupRejection != null, Is.True, "Daemon destruction did not publish its guaranteed cargo as a pickup."); var collectionDeadline = Time.realtimeSinceStartup + 15f; var nextTractorAimAt = 0f; - while (Time.realtimeSinceStartup < collectionDeadline && pickupCollection == null) + while (Time.realtimeSinceStartup < collectionDeadline && + (expectCargoRejection ? pickupRejection == null : pickupCollection == null)) { yield return new WaitForSecondsRealtime(0.05f); runtime.Refresh(); @@ -571,19 +593,44 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } finalPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity => string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); - Assert.That(pickupCollection, Is.Not.Null, - "Destruction-created salvage never reached cargo through a Ymir contact fact."); - Assert.That(pickupCollectionEventCount, Is.EqualTo(1), - "One destruction-loot Ymir contact transaction must emit one collection event."); - Assert.That(pickupCollection.ItemKey, Is.Not.Empty, - "Destruction loot must retain its canonical typed item identity."); - Assert.That(pickupCollection.EventId, Does.StartWith("ymir-fact:"), - "Collection feedback must retain the consumed Ymir fact identity."); - Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1)); - Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero); - Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1)); - Assert.That(finalPickupEntityCount, Is.Zero, - "Collected destruction loot remained in the provider-authored entity generation."); + if (expectCargoRejection) + { + Assert.That(pickupRejection, Is.Not.Null, + "Full cargo did not reject destruction salvage from its Ymir contact fact."); + Assert.That(pickupRejectionEventCount, Is.EqualTo(1), + "The first destruction-loot Ymir contact transaction must emit one rejection event."); + Assert.That(pickupRejectionEventIds.Count, Is.EqualTo(pickupRejectionEventCount), + "One Ymir fact identity emitted duplicate player rejection feedback."); + Assert.That(pickupCollectionEventCount, Is.Zero, + "Capacity-rejected salvage was also collected."); + Assert.That(pickupRejection.ItemKey, Is.Not.Empty); + Assert.That(pickupRejection.EventId, Does.StartWith("ymir-fact:"), + "Rejection feedback must retain the consumed Ymir fact identity."); + Assert.That(pickupRejection.Reason, Is.EqualTo("cargo-capacity")); + Assert.That(pickupRejection.ScalarValue, Is.EqualTo(1)); + Assert.That(pickupRejection.CargoQuantityAfter, + Is.EqualTo(pickupRejection.CargoQuantityBefore)); + Assert.That(finalPickupEntityCount, Is.EqualTo(1), + "Capacity-rejected destruction loot disappeared from provider state."); + } + else + { + Assert.That(pickupCollection, Is.Not.Null, + "Destruction-created salvage never reached cargo through a Ymir contact fact."); + Assert.That(pickupCollectionEventCount, Is.EqualTo(1), + "One destruction-loot Ymir contact transaction must emit one collection event."); + Assert.That(pickupRejectionEventCount, Is.Zero, + "Collectible destruction salvage was also rejected."); + Assert.That(pickupCollection.ItemKey, Is.Not.Empty, + "Destruction loot must retain its canonical typed item identity."); + Assert.That(pickupCollection.EventId, Does.StartWith("ymir-fact:"), + "Collection feedback must retain the consumed Ymir fact identity."); + Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1)); + Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero); + Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1)); + Assert.That(finalPickupEntityCount, Is.Zero, + "Collected destruction loot remained in the provider-authored entity generation."); + } observedCombat = EveUnityCombatPresentation.Find(runtime.ActiveProjection); Assert.That(observedShot.TargetEntityIndex, Is.EqualTo(targetEntityIndex)); Assert.That(observedShot.LockQuality, Is.GreaterThanOrEqualTo(0.99)); @@ -869,6 +916,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(fieldSplats, Is.Not.Null); WriteWitnessFacts( witnessProfile, + gameplayScenario, provider.CurrentAssetBodyTransportKind?.ToString() ?? "", initialVersion, runtime.ActiveVersion, @@ -915,8 +963,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests initialPickupEntityCount, finalPickupEntityCount, pickupCollectionEventCount, + pickupRejectionEventCount, destructionPickupObserved, pickupCollection, + pickupRejection, camera.transform.position, camera.transform.forward, playerMarker.transform.position, @@ -949,6 +999,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests private static void WriteWitnessFacts( string witnessProfile, + string gameplayScenario, string assetBodyTransport, long initialVersion, long finalVersion, @@ -995,8 +1046,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests int initialPickupEntityCount, int finalPickupEntityCount, int pickupCollectionEventCount, + int pickupRejectionEventCount, bool destructionPickupObserved, EveUnityFeedbackEvent pickupCollection, + EveUnityFeedbackEvent pickupRejection, Vector3 cameraPosition, Vector3 cameraForward, Vector3 playerPosition, @@ -1011,9 +1064,11 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); if (string.IsNullOrWhiteSpace(path)) return; + var pickupEvent = pickupCollection ?? pickupRejection; var document = new WitnessFacts { witnessProfile = witnessProfile, + gameplayScenario = gameplayScenario, providerAdvertisement = true, providerAssets = true, assetBodyTransport = assetBodyTransport, @@ -1068,16 +1123,25 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamParticleSystemCount = tractorBeamParticleSystemCount, pickupCollection = pickupCollection != null && pickupCollectionEventCount == 1 && pickupCollection.CargoQuantityAfter - pickupCollection.CargoQuantityBefore == pickupCollection.ScalarValue, - destructionLoot = pickupCollection != null && - pickupCollection.EventId.StartsWith("ymir-fact:", StringComparison.Ordinal), + pickupRejection = pickupRejection != null && pickupRejectionEventCount >= 1 && + pickupCollectionEventCount == 0 && + string.Equals(pickupRejection.Reason, "cargo-capacity", StringComparison.Ordinal) && + pickupRejection.CargoQuantityAfter == pickupRejection.CargoQuantityBefore, + destructionLoot = (pickupCollection?.EventId ?? pickupRejection?.EventId ?? "") + .StartsWith("ymir-fact:", StringComparison.Ordinal), initialPickupEntityCount = initialPickupEntityCount, finalPickupEntityCount = finalPickupEntityCount, pickupCollectionEventCount = pickupCollectionEventCount, - pickupEventId = pickupCollection?.EventId ?? "", - pickupItemKey = pickupCollection?.ItemKey ?? "", - pickupQuantity = (float)(pickupCollection?.ScalarValue ?? 0), - cargoQuantityBeforePickup = (float)(pickupCollection?.CargoQuantityBefore ?? 0), - cargoQuantityAfterPickup = (float)(pickupCollection?.CargoQuantityAfter ?? 0), + pickupRejectionEventCount = pickupRejectionEventCount, + pickupRejectionEventId = pickupRejection?.EventId ?? "", + pickupRejectionReason = pickupRejection?.Reason ?? "", + cargoQuantityBeforeRejection = (float)(pickupRejection?.CargoQuantityBefore ?? 0), + cargoQuantityAfterRejection = (float)(pickupRejection?.CargoQuantityAfter ?? 0), + pickupEventId = pickupEvent?.EventId ?? "", + pickupItemKey = pickupEvent?.ItemKey ?? "", + pickupQuantity = (float)(pickupEvent?.ScalarValue ?? 0), + cargoQuantityBeforePickup = (float)(pickupEvent?.CargoQuantityBefore ?? 0), + cargoQuantityAfterPickup = (float)(pickupEvent?.CargoQuantityAfter ?? 0), cameraPosition = cameraPosition, cameraForward = cameraForward, playerPosition = playerPosition, @@ -1103,6 +1167,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests private sealed class WitnessFacts { public string witnessProfile; + public string gameplayScenario; public bool providerAdvertisement; public bool providerAssets; public string assetBodyTransport; @@ -1154,10 +1219,16 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float tractorReleasedPower; public int tractorBeamParticleSystemCount; public bool pickupCollection; + public bool pickupRejection; public bool destructionLoot; public int initialPickupEntityCount; public int finalPickupEntityCount; public int pickupCollectionEventCount; + public int pickupRejectionEventCount; + public string pickupRejectionEventId; + public string pickupRejectionReason; + public float cargoQuantityBeforeRejection; + public float cargoQuantityAfterRejection; public string pickupEventId; public string pickupItemKey; public float pickupQuantity; diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index f98cfd1..152bac1 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -210,7 +210,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float tractorReleasedPower = 1f; int tractorBeamParticleSystemCount = 0; EveUnityFeedbackEvent pickupCollection = null; + EveUnityFeedbackEvent pickupRejection = null; int pickupCollectionEventCount = 0; + int pickupRejectionEventCount = 0; + var pickupRejectionEventIds = new HashSet(StringComparer.Ordinal); int initialPickupEntityCount = 0; int finalPickupEntityCount = -1; bool destructionPickupObserved = false; @@ -225,6 +228,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests witnessProfile, "full-session-gameplay", StringComparison.Ordinal); + var gameplayScenario = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_GAMEPLAY_SCENARIO") ?? + "released-client-proof"; + Assert.That(gameplayScenario, + Is.EqualTo("released-client-proof").Or.EqualTo("cargo-capacity-rejection-proof"), + "The live witness must select an explicit daemon-owned gameplay scenario."); + var expectCargoRejection = string.Equals( + gameplayScenario, + "cargo-capacity-rejection-proof", + StringComparison.Ordinal); EveUnityAimPresentationRenderer aimRenderer = null; EveUnityBeamPresentation beamPresentation = null; EveUnityBeamPresentationRenderer beamRenderer = null; @@ -311,16 +323,25 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests if (requireSessionGameplay) Assert.That(initialPickupEntityCount, Is.Zero, "The witness scenario must obtain salvage from daemon-owned destruction, not a boot-seeded pickup."); - runtime.FeedbackAvailable += feedback => - { - if (!string.Equals(feedback.Kind, "pickup.collected", StringComparison.Ordinal)) return; - pickupCollectionEventCount++; - pickupCollection = feedback; - }; - var playerId = runtime.ActiveWorld.PlayerEntityId; var playerFact = host.PresentedEntities.CurrentGeneration.Entities .Single(entity => entity.EntityId == playerId); + runtime.FeedbackAvailable += feedback => + { + if (feedback.TargetEntityIndex != playerFact.SourceIndex) return; + if (string.Equals(feedback.Kind, "pickup.collected", StringComparison.Ordinal)) + { + pickupCollectionEventCount++; + pickupCollection = feedback; + } + else if (string.Equals(feedback.Kind, "pickup.rejected", StringComparison.Ordinal)) + { + pickupRejectionEventCount++; + pickupRejectionEventIds.Add(feedback.EventId); + pickupRejection = feedback; + } + }; + var playerPrefab = provider.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( playerFact.AssetRef, playerFact.EntityKind, @@ -529,7 +550,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests (observedShot == null || host.PresentedEntities.CurrentGeneration.Entities.Any(entity => entity.EntityId == targetEntityId) || - (!destructionPickupObserved && pickupCollection == null))) + (!destructionPickupObserved && pickupCollection == null && pickupRejection == null))) { yield return new WaitForSecondsRealtime(0.05f); runtime.Refresh(); @@ -544,11 +565,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(host.PresentedEntities.CurrentGeneration.Entities.Any(entity => entity.EntityId == targetEntityId), Is.False, "The low-hull witness raider was not destroyed by daemon combat."); - Assert.That(destructionPickupObserved || pickupCollection != null, Is.True, + Assert.That(destructionPickupObserved || pickupCollection != null || pickupRejection != null, Is.True, "Daemon destruction did not publish its guaranteed cargo as a pickup."); var collectionDeadline = Time.realtimeSinceStartup + 15f; var nextTractorAimAt = 0f; - while (Time.realtimeSinceStartup < collectionDeadline && pickupCollection == null) + while (Time.realtimeSinceStartup < collectionDeadline && + (expectCargoRejection ? pickupRejection == null : pickupCollection == null)) { yield return new WaitForSecondsRealtime(0.05f); runtime.Refresh(); @@ -571,19 +593,44 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } finalPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity => string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); - Assert.That(pickupCollection, Is.Not.Null, - "Destruction-created salvage never reached cargo through a Ymir contact fact."); - Assert.That(pickupCollectionEventCount, Is.EqualTo(1), - "One destruction-loot Ymir contact transaction must emit one collection event."); - Assert.That(pickupCollection.ItemKey, Is.Not.Empty, - "Destruction loot must retain its canonical typed item identity."); - Assert.That(pickupCollection.EventId, Does.StartWith("ymir-fact:"), - "Collection feedback must retain the consumed Ymir fact identity."); - Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1)); - Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero); - Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1)); - Assert.That(finalPickupEntityCount, Is.Zero, - "Collected destruction loot remained in the provider-authored entity generation."); + if (expectCargoRejection) + { + Assert.That(pickupRejection, Is.Not.Null, + "Full cargo did not reject destruction salvage from its Ymir contact fact."); + Assert.That(pickupRejectionEventCount, Is.EqualTo(1), + "The first destruction-loot Ymir contact transaction must emit one rejection event."); + Assert.That(pickupRejectionEventIds.Count, Is.EqualTo(pickupRejectionEventCount), + "One Ymir fact identity emitted duplicate player rejection feedback."); + Assert.That(pickupCollectionEventCount, Is.Zero, + "Capacity-rejected salvage was also collected."); + Assert.That(pickupRejection.ItemKey, Is.Not.Empty); + Assert.That(pickupRejection.EventId, Does.StartWith("ymir-fact:"), + "Rejection feedback must retain the consumed Ymir fact identity."); + Assert.That(pickupRejection.Reason, Is.EqualTo("cargo-capacity")); + Assert.That(pickupRejection.ScalarValue, Is.EqualTo(1)); + Assert.That(pickupRejection.CargoQuantityAfter, + Is.EqualTo(pickupRejection.CargoQuantityBefore)); + Assert.That(finalPickupEntityCount, Is.EqualTo(1), + "Capacity-rejected destruction loot disappeared from provider state."); + } + else + { + Assert.That(pickupCollection, Is.Not.Null, + "Destruction-created salvage never reached cargo through a Ymir contact fact."); + Assert.That(pickupCollectionEventCount, Is.EqualTo(1), + "One destruction-loot Ymir contact transaction must emit one collection event."); + Assert.That(pickupRejectionEventCount, Is.Zero, + "Collectible destruction salvage was also rejected."); + Assert.That(pickupCollection.ItemKey, Is.Not.Empty, + "Destruction loot must retain its canonical typed item identity."); + Assert.That(pickupCollection.EventId, Does.StartWith("ymir-fact:"), + "Collection feedback must retain the consumed Ymir fact identity."); + Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1)); + Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero); + Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1)); + Assert.That(finalPickupEntityCount, Is.Zero, + "Collected destruction loot remained in the provider-authored entity generation."); + } observedCombat = EveUnityCombatPresentation.Find(runtime.ActiveProjection); Assert.That(observedShot.TargetEntityIndex, Is.EqualTo(targetEntityIndex)); Assert.That(observedShot.LockQuality, Is.GreaterThanOrEqualTo(0.99)); @@ -869,6 +916,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(fieldSplats, Is.Not.Null); WriteWitnessFacts( witnessProfile, + gameplayScenario, provider.CurrentAssetBodyTransportKind?.ToString() ?? "", initialVersion, runtime.ActiveVersion, @@ -915,8 +963,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests initialPickupEntityCount, finalPickupEntityCount, pickupCollectionEventCount, + pickupRejectionEventCount, destructionPickupObserved, pickupCollection, + pickupRejection, camera.transform.position, camera.transform.forward, playerMarker.transform.position, @@ -949,6 +999,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests private static void WriteWitnessFacts( string witnessProfile, + string gameplayScenario, string assetBodyTransport, long initialVersion, long finalVersion, @@ -995,8 +1046,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests int initialPickupEntityCount, int finalPickupEntityCount, int pickupCollectionEventCount, + int pickupRejectionEventCount, bool destructionPickupObserved, EveUnityFeedbackEvent pickupCollection, + EveUnityFeedbackEvent pickupRejection, Vector3 cameraPosition, Vector3 cameraForward, Vector3 playerPosition, @@ -1011,9 +1064,11 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); if (string.IsNullOrWhiteSpace(path)) return; + var pickupEvent = pickupCollection ?? pickupRejection; var document = new WitnessFacts { witnessProfile = witnessProfile, + gameplayScenario = gameplayScenario, providerAdvertisement = true, providerAssets = true, assetBodyTransport = assetBodyTransport, @@ -1068,16 +1123,25 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamParticleSystemCount = tractorBeamParticleSystemCount, pickupCollection = pickupCollection != null && pickupCollectionEventCount == 1 && pickupCollection.CargoQuantityAfter - pickupCollection.CargoQuantityBefore == pickupCollection.ScalarValue, - destructionLoot = pickupCollection != null && - pickupCollection.EventId.StartsWith("ymir-fact:", StringComparison.Ordinal), + pickupRejection = pickupRejection != null && pickupRejectionEventCount >= 1 && + pickupCollectionEventCount == 0 && + string.Equals(pickupRejection.Reason, "cargo-capacity", StringComparison.Ordinal) && + pickupRejection.CargoQuantityAfter == pickupRejection.CargoQuantityBefore, + destructionLoot = (pickupCollection?.EventId ?? pickupRejection?.EventId ?? "") + .StartsWith("ymir-fact:", StringComparison.Ordinal), initialPickupEntityCount = initialPickupEntityCount, finalPickupEntityCount = finalPickupEntityCount, pickupCollectionEventCount = pickupCollectionEventCount, - pickupEventId = pickupCollection?.EventId ?? "", - pickupItemKey = pickupCollection?.ItemKey ?? "", - pickupQuantity = (float)(pickupCollection?.ScalarValue ?? 0), - cargoQuantityBeforePickup = (float)(pickupCollection?.CargoQuantityBefore ?? 0), - cargoQuantityAfterPickup = (float)(pickupCollection?.CargoQuantityAfter ?? 0), + pickupRejectionEventCount = pickupRejectionEventCount, + pickupRejectionEventId = pickupRejection?.EventId ?? "", + pickupRejectionReason = pickupRejection?.Reason ?? "", + cargoQuantityBeforeRejection = (float)(pickupRejection?.CargoQuantityBefore ?? 0), + cargoQuantityAfterRejection = (float)(pickupRejection?.CargoQuantityAfter ?? 0), + pickupEventId = pickupEvent?.EventId ?? "", + pickupItemKey = pickupEvent?.ItemKey ?? "", + pickupQuantity = (float)(pickupEvent?.ScalarValue ?? 0), + cargoQuantityBeforePickup = (float)(pickupEvent?.CargoQuantityBefore ?? 0), + cargoQuantityAfterPickup = (float)(pickupEvent?.CargoQuantityAfter ?? 0), cameraPosition = cameraPosition, cameraForward = cameraForward, playerPosition = playerPosition, @@ -1103,6 +1167,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests private sealed class WitnessFacts { public string witnessProfile; + public string gameplayScenario; public bool providerAdvertisement; public bool providerAssets; public string assetBodyTransport; @@ -1154,10 +1219,16 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float tractorReleasedPower; public int tractorBeamParticleSystemCount; public bool pickupCollection; + public bool pickupRejection; public bool destructionLoot; public int initialPickupEntityCount; public int finalPickupEntityCount; public int pickupCollectionEventCount; + public int pickupRejectionEventCount; + public string pickupRejectionEventId; + public string pickupRejectionReason; + public float cargoQuantityBeforeRejection; + public float cargoQuantityAfterRejection; public string pickupEventId; public string pickupItemKey; public float pickupQuantity; diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index a71e860..daaec94 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -84,6 +84,17 @@ The remaining visual controls (fog parameters, stellar tint, ambient intensity, histogram exposure, grading, and temporal settings) remain provider-owned levers; this proof does not claim a final art grade. +The released `0.3.63` warm rejection witness in +`artifacts/aetheria-daemon-cargo-rejection-0363-warm` selects the daemon-owned +`cargo-capacity-rejection-proof` scenario. The controlled ship begins with all +generated cargo bays filled from typed item volume and bay capacity. Combat +creates the salvage pickup normally; player Ymir Begin contacts emit +`pickup.rejected` with `cargo-capacity`, preserve cargo at `5` items, and leave +the pickup live. The final run observed two later player recontacts with two +distinct `ymir-fact:` identities and no player collection event. The generic +witness correlates feedback through advertised SoA `SourceIndex` and feedback +`TargetEntityIndex`; events from other ships are not attributed to the pilot. + Current released-package evidence is in `artifacts/aetheria-daemon-mapped-body-cold`; despite that retained directory name, `runtime-witness.warm.json` is the current passing run. The warm PlayMode test @@ -251,8 +262,8 @@ changes cargo from `0` to `1`, and emits one `pickup.collected` feedback event whose identity begins with `ymir-fact:`. Aetheria's Ymir body mapping excludes stations and non-ship world bodies from pickup collision facts, so ambient station contact cannot become a cargo writer. Capacity rejection and -its `cargo-capacity` reason are covered by the daemon smoke, not claimed as a -released-client live scenario here. +its `cargo-capacity` reason are also proven by the released-client live rejection +scenario described above. Effect tuning remains provider art work; the runtime must not compensate with an Aetheria-specific beam. diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 86fb3a0..0f7c4ed 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -9,6 +9,8 @@ param( [string] $OutputDirectory = "artifacts\aetheria-daemon", [ValidateSet("auto", "cold", "warm")] [string] $CacheState = "auto", + [ValidateSet("released-client-proof", "cargo-capacity-rejection-proof")] + [string] $GameplayScenario = "released-client-proof", [switch] $SkipAssetBundleBuild ) @@ -168,6 +170,7 @@ $daemonArguments = @( "--client-cultmesh-port", $Port, "--tick-interval-ms", 20, "--fixed-delta-ms", 20, + "--terminus-scenario", $GameplayScenario, "--no-odin-announcements" ) $env:AETHERIA_TRACE_EVE_SNAPSHOTS = "1" @@ -183,6 +186,7 @@ $env:EVEUNITY_ASSET_CACHE_PATH = $assetCachePath $env:EVEUNITY_WITNESS_FACTS_PATH = $factsPath $env:EVEUNITY_PROVIDER_READY_PATH = $providerReadyPath $env:EVEUNITY_WITNESS_PROFILE = $witnessProfile +$env:EVEUNITY_WITNESS_GAMEPLAY_SCENARIO = $GameplayScenario $arguments = @( "-batchmode", "-projectPath", $projectRoot, "-runTests", "-testPlatform", "PlayMode", @@ -271,11 +275,21 @@ try { @($presentedCelestials | Where-Object { $_.intersectsPilotFrustum }).Count -le 0) { throw "Live witness did not prove provider-owned celestial bodies and asteroids reached the generic scene and intersected the pilot frustum. bodies=$($presentedBodies.Count) asteroids=$($presentedAsteroids.Count) invalid=$($invalidCelestials.Count)" } - if ($witnessProfile -eq "full-session-gameplay" -and - (-not $facts.combatPresentation -or [string]::IsNullOrWhiteSpace($facts.shotId) -or - [double]$facts.lockProgress -le 0.99 -or -not $facts.pickupCollection -or - -not $facts.destructionLoot -or [int]$facts.pickupCollectionEventCount -ne 1)) { - throw "Full-session witness did not prove combat and exactly-once destruction-loot Ymir contact collection." + if ($witnessProfile -eq "full-session-gameplay") { + if (-not $facts.combatPresentation -or [string]::IsNullOrWhiteSpace($facts.shotId) -or + [double]$facts.lockProgress -le 0.99 -or -not $facts.destructionLoot) { + throw "Full-session witness did not prove combat and Ymir-contact destruction loot." + } + if ($GameplayScenario -eq "cargo-capacity-rejection-proof") { + if ($facts.gameplayScenario -ne $GameplayScenario -or -not $facts.pickupRejection -or + [int]$facts.pickupRejectionEventCount -lt 1 -or [int]$facts.pickupCollectionEventCount -ne 0 -or + $facts.pickupRejectionReason -ne "cargo-capacity" -or + [double]$facts.cargoQuantityBeforeRejection -ne [double]$facts.cargoQuantityAfterRejection) { + throw "Full-session rejection witness did not prove player cargo-capacity refusal from distinct Ymir contact facts." + } + } elseif (-not $facts.pickupCollection -or [int]$facts.pickupCollectionEventCount -ne 1) { + throw "Full-session collection witness did not prove exactly-once player destruction-loot collection from a Ymir contact fact." + } } $releaseLock = Get-Content -LiteralPath (Join-Path $repoRoot "ReleaseConsumerProject\Packages\packages-lock.json") -Raw | ConvertFrom-Json $releasedPackageClient = $projectRoot -eq "ReleaseConsumerProject" @@ -379,7 +393,11 @@ try { } ) authority = if ($witnessProfile -eq "full-session-gameplay") { - "released-generic-runtime-observes-provider-assets-authoritative-gameplay-receipts-ymir-contact-collection-and-camera-channel-separation" + if ($GameplayScenario -eq "cargo-capacity-rejection-proof") { + "released-generic-runtime-observes-provider-assets-authoritative-gameplay-receipts-ymir-contact-capacity-rejection-and-camera-channel-separation" + } else { + "released-generic-runtime-observes-provider-assets-authoritative-gameplay-receipts-ymir-contact-collection-and-camera-channel-separation" + } } else { "released-generic-runtime-cold-loads-provider-assets-lowers-playable-world-and-preserves-camera-channel-separation" } @@ -396,6 +414,6 @@ try { finally { if ($null -ne $unity -and -not $unity.HasExited) { Stop-Process -Id $unity.Id -Force -ErrorAction SilentlyContinue } if ($null -ne $daemon -and -not $daemon.HasExited) { Stop-Process -Id $daemon.Id -Force } - Remove-Item Env:EVEUNITY_RENDEZVOUS_ENDPOINT, Env:EVEUNITY_PROVIDER_ENDPOINT, Env:EVEUNITY_PROVIDER_ID, Env:EVEUNITY_SURFACE_ID, Env:EVEUNITY_REPLICA_PATH, Env:EVEUNITY_AETHERIA_CAPTURE_PATH, Env:EVEUNITY_AETHERIA_MAP_CAPTURE_PATH, Env:EVEUNITY_DISABLE_AUTO_LAUNCHER, Env:EVEUNITY_ASSET_CACHE_PATH, Env:EVEUNITY_WITNESS_FACTS_PATH, Env:EVEUNITY_PROVIDER_READY_PATH, Env:EVEUNITY_WITNESS_PROFILE -ErrorAction SilentlyContinue + Remove-Item Env:EVEUNITY_RENDEZVOUS_ENDPOINT, Env:EVEUNITY_PROVIDER_ENDPOINT, Env:EVEUNITY_PROVIDER_ID, Env:EVEUNITY_SURFACE_ID, Env:EVEUNITY_REPLICA_PATH, Env:EVEUNITY_AETHERIA_CAPTURE_PATH, Env:EVEUNITY_AETHERIA_MAP_CAPTURE_PATH, Env:EVEUNITY_DISABLE_AUTO_LAUNCHER, Env:EVEUNITY_ASSET_CACHE_PATH, Env:EVEUNITY_WITNESS_FACTS_PATH, Env:EVEUNITY_PROVIDER_READY_PATH, Env:EVEUNITY_WITNESS_PROFILE, Env:EVEUNITY_WITNESS_GAMEPLAY_SCENARIO -ErrorAction SilentlyContinue Remove-Item Env:AETHERIA_TRACE_EVE_SNAPSHOTS -ErrorAction SilentlyContinue } From 3bb7e6ae7d82e9c6b9b9889b351747c858e02b19 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 17 Jul 2026 01:21:07 +0100 Subject: [PATCH 153/292] Lower generic spatial inventory interactions --- .../Runtime/EveInventoryInteraction.cs | 82 ++++++++ .../Runtime/EveInventoryInteraction.cs.meta | 2 + .../Runtime/EveUiToolkitSurfaceLowerer.cs | 177 +++++++++++++++++- .../Editor/EveUiToolkitSurfaceLowererTests.cs | 66 +++++++ 4 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 packages/org.gamecult.eve.surface/Runtime/EveInventoryInteraction.cs create mode 100644 packages/org.gamecult.eve.surface/Runtime/EveInventoryInteraction.cs.meta diff --git a/packages/org.gamecult.eve.surface/Runtime/EveInventoryInteraction.cs b/packages/org.gamecult.eve.surface/Runtime/EveInventoryInteraction.cs new file mode 100644 index 0000000..c8c53e9 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveInventoryInteraction.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using GameCult.Mesh; + +#nullable enable + +namespace GameCult.Eve.Surface +{ + public static class EveInventoryInteraction + { + public const string GridKind = "inventory.grid"; + public const string ItemKind = "inventory.item"; + public const string DragSessionKind = "inventory.drag_session"; + + public static bool TryCreateDropRequest( + EveSurfaceDocument document, + EveSurfaceComponent source, + EveSurfaceComponent target, + int destinationX, + int destinationY, + string clientId, + out EveSurfaceCommandRequest? request) + { + request = null; + if (document == null || source == null || target == null || + !string.Equals(source.Kind, ItemKind, StringComparison.Ordinal) || + !string.Equals(target.Kind, GridKind, StringComparison.Ordinal)) + { + return false; + } + + var sourceKind = source.GetProp("sourceKind", source.GetProp("source")); + var command = target.GetProp($"dropCommand.{sourceKind}", target.GetProp("dropCommand")); + if (string.IsNullOrWhiteSpace(sourceKind) || string.IsNullOrWhiteSpace(command)) + return false; + + var sourceIndex = Int(source.GetProp("sourceIndex"), -1); + var targetIndex = Int(target.GetProp("targetIndex"), -1); + var targetKind = target.GetProp("targetKind"); + var payload = new Dictionary(StringComparer.Ordinal) + { + ["sourceKind"] = sourceKind, + ["originEntityKey"] = source.GetProp("sourceEntityKey", source.GetProp("entityKey")), + ["originIndex"] = sourceIndex.ToString(CultureInfo.InvariantCulture), + ["originCargoIndex"] = (string.Equals(sourceKind, "cargo", StringComparison.Ordinal) ? sourceIndex : -1) + .ToString(CultureInfo.InvariantCulture), + ["itemKey"] = source.GetProp("itemKey"), + ["quantity"] = Math.Max(1, Int(source.GetProp("quantity"), 1)).ToString(CultureInfo.InvariantCulture), + ["sourceX"] = Int(source.GetProp("x"), int.MinValue).ToString(CultureInfo.InvariantCulture), + ["sourceY"] = Int(source.GetProp("y"), int.MinValue).ToString(CultureInfo.InvariantCulture), + ["destinationKind"] = targetKind, + ["destinationEntityKey"] = target.GetProp("targetEntityKey", target.GetProp("entityKey")), + ["destinationIndex"] = targetIndex.ToString(CultureInfo.InvariantCulture), + ["destinationCargoIndex"] = (string.Equals(targetKind, "cargo", StringComparison.Ordinal) ? targetIndex : -1) + .ToString(CultureInfo.InvariantCulture), + ["destinationX"] = destinationX.ToString(CultureInfo.InvariantCulture), + ["destinationY"] = destinationY.ToString(CultureInfo.InvariantCulture), + ["hasDestinationPosition"] = "true" + }; + var template = document.Commands.FirstOrDefault(candidate => + string.Equals(candidate.Command, command, StringComparison.Ordinal)); + var operation = template == null + ? CultMesh.OperationInvocation(command) + : CultMesh.OperationInvocation(template.Operation); + request = new EveSurfaceCommandRequest( + document.ProviderId, + document.Surface.Id, + operation, + CultMesh.OperationPayload(payload), + DateTimeOffset.UtcNow, + string.IsNullOrWhiteSpace(clientId) ? "unity-uitoolkit" : clientId); + return true; + } + + private static int Int(string value, int fallback) => + int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) + ? parsed + : fallback; + } +} diff --git a/packages/org.gamecult.eve.surface/Runtime/EveInventoryInteraction.cs.meta b/packages/org.gamecult.eve.surface/Runtime/EveInventoryInteraction.cs.meta new file mode 100644 index 0000000..57cb330 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveInventoryInteraction.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 17fd4173e6e44b4b9c503d343021ffcb diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs index 6586d67..cf7690a 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs @@ -13,6 +13,10 @@ namespace GameCult.Eve.UnityUIToolkit public sealed class EveUiToolkitSurfaceLowerer { private readonly EveUiToolkitSurfaceOptions _options; + private EveSurfaceComponent? _inventoryDragSource; + private Vector2 _inventoryDragStart; + private bool _inventoryPointerMoved; + private bool _suppressInventoryClick; public EveUiToolkitSurfaceLowerer(EveUiToolkitSurfaceOptions? options = null) { @@ -43,7 +47,11 @@ namespace GameCult.Eve.UnityUIToolkit ApplyGeneratedLayout(element, component); foreach (var child in component.Children) - element.Add(LowerComponent(child, document, commandSink)); + { + var loweredChild = LowerComponent(child, document, commandSink); + PositionInventoryChild(component, child, loweredChild); + element.Add(loweredChild); + } foreach (var slot in component.EmbeddedDocuments) { @@ -97,6 +105,12 @@ namespace GameCult.Eve.UnityUIToolkit element.style.alignItems = Align.Stretch; return element; } + case EveInventoryInteraction.GridKind: + return InventoryGrid(component, document, commandSink); + case EveInventoryInteraction.ItemKind: + return InventoryItem(component, document, commandSink); + case EveInventoryInteraction.DragSessionKind: + return InventoryDragSession(component); case "partition": { var element = new VisualElement(); @@ -210,6 +224,167 @@ namespace GameCult.Eve.UnityUIToolkit } } + private VisualElement InventoryGrid( + EveSurfaceComponent component, + EveSurfaceDocument document, + Action? commandSink) + { + var grid = new VisualElement(); + grid.AddToClassList("eve-inventory-grid"); + grid.style.position = Position.Relative; + var columns = Math.Max(1, ParseInt(component.GetProp("columns"), 6)); + var rows = Math.Max(1, ParseInt(component.GetProp("rows"), 3)); + var cellSize = Math.Max(1f, ParseNumber(component.GetProp("cellSize"), 72f)); + var gap = Math.Max(0f, ParseNumber(component.GetProp("cellGap"), 4f)); + grid.style.width = columns * cellSize + Math.Max(0, columns - 1) * gap; + grid.style.height = rows * cellSize + Math.Max(0, rows - 1) * gap; + for (var index = 0; index < Math.Min(columns * rows, 256); index++) + { + var cell = new VisualElement(); + cell.AddToClassList("eve-inventory-cell"); + cell.style.position = Position.Absolute; + cell.style.left = (index % columns) * (cellSize + gap); + cell.style.top = (index / columns) * (cellSize + gap); + cell.style.width = cellSize; + cell.style.height = cellSize; + grid.Add(cell); + } + grid.RegisterCallback(evt => + { + if (_inventoryDragSource == null || _suppressInventoryClick) + return; + if (TryEmitInventoryDrop(document, _inventoryDragSource, component, grid, evt.position, commandSink)) + _inventoryDragSource = null; + }); + return grid; + } + + private VisualElement InventoryItem( + EveSurfaceComponent component, + EveSurfaceDocument document, + Action? commandSink) + { + var item = new Button { text = component.GetProp("label", component.GetProp("itemKey")) }; + item.AddToClassList("eve-inventory-item"); + item.RegisterCallback(evt => + { + if (!ParseBool(component.GetProp("draggable", "true"))) + return; + _inventoryDragSource = component; + _inventoryDragStart = evt.position; + _inventoryPointerMoved = false; + item.CapturePointer(evt.pointerId); + evt.StopPropagation(); + }); + item.RegisterCallback(evt => + { + if (_inventoryDragSource != component) + return; + var pointerPosition = new Vector2(evt.position.x, evt.position.y); + if ((pointerPosition - _inventoryDragStart).sqrMagnitude > 16f) + _inventoryPointerMoved = true; + }); + item.RegisterCallback(evt => + { + if (_inventoryDragSource != component) + return; + item.ReleasePointer(evt.pointerId); + if (_inventoryPointerMoved) + { + var picked = item.panel?.Pick(evt.position); + var targetElement = InventoryGridAncestor(picked); + if (targetElement?.userData is EveSurfaceComponent target) + TryEmitInventoryDrop(document, component, target, targetElement, evt.position, commandSink); + _inventoryDragSource = null; + _suppressInventoryClick = true; + } + evt.StopPropagation(); + }); + item.RegisterCallback(evt => + { + if (_suppressInventoryClick) + { + _suppressInventoryClick = false; + evt.StopPropagation(); + return; + } + _inventoryDragSource = component; + evt.StopPropagation(); + }); + return item; + } + + private static VisualElement InventoryDragSession(EveSurfaceComponent component) + { + var panel = new VisualElement(); + panel.AddToClassList("eve-inventory-drag-session"); + var active = ParseBool(component.GetProp("active")); + panel.Add(BodyLabel(active + ? component.GetProp("itemKey", "Dragging") + : "No active drag")); + return panel; + } + + private static void PositionInventoryChild( + EveSurfaceComponent parent, + EveSurfaceComponent child, + VisualElement element) + { + if (!string.Equals(parent.Kind, EveInventoryInteraction.GridKind, StringComparison.Ordinal) || + !string.Equals(child.Kind, EveInventoryInteraction.ItemKind, StringComparison.Ordinal)) + return; + var cellSize = Math.Max(1f, ParseNumber(parent.GetProp("cellSize"), 72f)); + var gap = Math.Max(0f, ParseNumber(parent.GetProp("cellGap"), 4f)); + element.style.position = Position.Absolute; + element.style.left = Math.Max(0, ParseInt(child.GetProp("x"), 0)) * (cellSize + gap); + element.style.top = Math.Max(0, ParseInt(child.GetProp("y"), 0)) * (cellSize + gap); + element.style.width = cellSize; + element.style.height = cellSize; + } + + private static VisualElement? InventoryGridAncestor(VisualElement? element) + { + while (element != null) + { + if (element.userData is EveSurfaceComponent component && + string.Equals(component.Kind, EveInventoryInteraction.GridKind, StringComparison.Ordinal)) + return element; + element = element.parent; + } + return null; + } + + private static bool TryEmitInventoryDrop( + EveSurfaceDocument document, + EveSurfaceComponent source, + EveSurfaceComponent target, + VisualElement targetElement, + Vector2 panelPosition, + Action? commandSink) + { + if (commandSink == null) + return false; + var local = targetElement.WorldToLocal(panelPosition); + var pitch = Math.Max(1f, ParseNumber(target.GetProp("cellSize"), 72f) + + Math.Max(0f, ParseNumber(target.GetProp("cellGap"), 4f))); + var x = Math.Max(0, (int)Math.Floor(local.x / pitch)); + var y = Math.Max(0, (int)Math.Floor(local.y / pitch)); + if (!EveInventoryInteraction.TryCreateDropRequest( + document, source, target, x, y, "unity-uitoolkit", out var request) || request == null) + return false; + commandSink(request); + return true; + } + + private static int ParseInt(string value, int fallback) => + int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) ? parsed : fallback; + + private static bool ParseBool(string value) => + bool.TryParse(value, out var parsed) && parsed; + + private static float ParseNumber(string value, float fallback) => + float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed) ? parsed : fallback; + private static void EmitCommand( EveSurfaceDocument document, EveSurfaceComponent component, diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Tests/Editor/EveUiToolkitSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-uitoolkit/Tests/Editor/EveUiToolkitSurfaceLowererTests.cs index 0b9dc54..7752398 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/Tests/Editor/EveUiToolkitSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-uitoolkit/Tests/Editor/EveUiToolkitSurfaceLowererTests.cs @@ -142,6 +142,72 @@ namespace GameCult.Eve.UnityUIToolkit.Tests Assert.That(root.style.bottom.value.value, Is.EqualTo(32f)); } + [Test] + public void InventoryDropBuildsAdvertisedTypedOperationPayload() + { + var source = Component("ore", EveInventoryInteraction.ItemKind, new Dictionary + { + ["sourceKind"] = "cargo", + ["sourceEntityKey"] = "zone.0.entity.1", + ["sourceIndex"] = "2", + ["itemKey"] = "ore", + ["quantity"] = "4", + ["x"] = "3", + ["y"] = "5" + }); + var target = Component("equipment", EveInventoryInteraction.GridKind, new Dictionary + { + ["targetKind"] = "equipment", + ["targetEntityKey"] = "zone.0.entity.1", + ["targetIndex"] = "-1", + ["dropCommand.cargo"] = "aetheria.daemon.commands.EquipItem" + }); + + Assert.That(EveInventoryInteraction.TryCreateDropRequest( + Document(target), source, target, 7, 9, "unity-test", out var request), Is.True); + Assert.That(request, Is.Not.Null); + Assert.That(request!.Command, Is.EqualTo("aetheria.daemon.commands.EquipItem")); + Assert.That(request.Payload.GetString("originEntityKey"), Is.EqualTo("zone.0.entity.1")); + Assert.That(request.Payload.GetString("originCargoIndex"), Is.EqualTo("2")); + Assert.That(request.Payload.GetString("destinationX"), Is.EqualTo("7")); + Assert.That(request.Payload.GetString("destinationY"), Is.EqualTo("9")); + Assert.That(request.Payload.GetString("hasDestinationPosition"), Is.EqualTo("true")); + } + + [Test] + public void InventoryGridAndItemsLowerToNativeSpatialElements() + { + var item = Component("ore", EveInventoryInteraction.ItemKind, new Dictionary + { + ["label"] = "Iron Ore", + ["sourceKind"] = "cargo", + ["x"] = "1", + ["y"] = "2" + }); + var grid = new EveSurfaceComponent( + "cargo", + EveInventoryInteraction.GridKind, + new Dictionary + { + ["columns"] = "4", + ["rows"] = "3", + ["cellSize"] = "32", + ["cellGap"] = "2" + }, + new[] { item }); + + var root = new EveUiToolkitSurfaceLowerer().Lower(Document(grid)); + var loweredItem = root.Query