Compare commits

..
Author SHA1 Message Date
Meta Vens
c5a4a75c1b Add portable radial cosine field source 2026-07-15 23:37:10 +01:00
Meta Vens
c4a61e818e Define world-anchored procedural field sources 2026-07-15 23:14:47 +01:00
Meta Vens
59924e324c Add portable power pulse field falloff 2026-07-15 22:35:12 +01:00
Meta Vens
382a23d8f8 Publish canonical fields splat documents 2026-07-15 17:11:11 +01:00
Meta Vens
dabea374b1 Complete fields ABI evidence 2026-07-10 16:28:10 +01:00
Meta Vens
97bbbcdc61 Graduate fields plugin runtime contract 2026-07-10 16:01:41 +01:00
Meta Vens
7d486b0089 Clean fields package manifest metadata 2026-07-10 15:44:17 +01:00
Meta Vens
030383470f Publish .NET fields contract project 2026-07-10 15:43:57 +01:00
Meta Vens
0b099274a4 Add stable Unity metadata to fields contracts 2026-07-10 15:33:52 +01:00
18 changed files with 209 additions and 7 deletions

3
.gitignore vendored
View file

@ -1,3 +1,4 @@
node_modules/
artifacts/
**/bin/
**/obj/

View file

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<Description>Runtime-neutral C# contracts for gamecult.fields documents.</Description>
<CultLibRoot Condition="'$(CultLibRoot)' == ''">$(MSBuildThisFileDirectory)..\..\..\CultLib</CultLibRoot>
<PackageId>GameCult.Eve.PluginFields</PackageId>
<Version>0.2.3</Version>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching\GameCult.Caching.csproj" />
<PackageReference Include="MessagePack" Version="3.1.7" />
</ItemGroup>
</Project>

View file

@ -2,7 +2,7 @@
"schema": "gamecult.eve.plugin_advertisement.v1",
"pluginId": "fields.surface",
"ownerService": "eveplugins",
"version": "0.1.0",
"version": "0.1.1",
"manifestAddress": "file://plugins/eve-plugin-fields/plugin.json",
"runtime": {
"invocationModel": "executable-sidecar",

View file

@ -8,9 +8,9 @@
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
"authority": "EvePlugins owns field semantics; providers own sampled field state and runtimes own native projection.",
"operations": [
{ "operation": "describe", "input": {}, "expect": { "pluginId": "fields.surface", "componentKinds": ["field.surface2d", "gravity.surface"], "commands": [] } },
{ "operation": "validate", "input": { "componentKinds": ["field.surface2d", "world.scene3d"], "document": { "schema": "gamecult.fields.surface2d.v1", "bounds": { "minX": -1, "minY": -1, "maxX": 1, "maxY": 1 } } }, "expect": { "acceptedComponentKinds": ["field.surface2d"], "rejectedComponentKinds": ["world.scene3d"], "validDocument": true } },
{ "operation": "describe", "input": {}, "expect": { "pluginId": "fields.surface", "componentKinds": ["field.surface2d", "gravity.surface"], "commands": [], "capabilities": ["field.surface2d", "gravity.surface", "field.scalar", "field.vector", "field.objects"] } },
{ "operation": "validate", "input": { "componentKinds": ["field.surface2d", "gravity.surface", "world.scene3d"], "document": { "schema": "gamecult.fields.surface2d.v1", "bounds": { "minX": -1, "minY": -1, "maxX": 1, "maxY": 1 } } }, "expect": { "acceptedComponentKinds": ["field.surface2d", "gravity.surface"], "rejectedComponentKinds": ["world.scene3d"], "validDocument": true } },
{ "operation": "project", "input": { "document": { "schema": "gamecult.fields.surface2d.v1", "fieldId": "fixture.field", "bounds": { "minX": -1, "minY": -1, "maxX": 1, "maxY": 1 }, "channels": [{ "channelId": "height", "kind": "scalar", "documentId": "cultmesh://fixture/height", "schemaId": "gamecult.fields.scalar.v1" }] } }, "expect": { "projectionKind": "semantic-field-surface", "ownedComponentKinds": ["field.surface2d", "gravity.surface"], "providerStateMutated": false } },
{ "operation": "measure", "input": { "document": { "schema": "gamecult.fields.surface2d.v1", "fieldId": "fixture.field", "bounds": { "minX": -1, "minY": -2, "maxX": 3, "maxY": 2 }, "channels": [] } }, "expect": { "measurementKind": "field-domain-metrics", "width": 4, "height": 4, "preservesProviderAuthority": true } }
{ "operation": "measure", "input": { "document": { "schema": "gamecult.fields.surface2d.v1", "fieldId": "fixture.field", "bounds": { "minX": -1, "minY": -2, "maxX": 3, "maxY": 2 }, "channels": [] } }, "expect": { "measurementKind": "field-domain-metrics", "measurementOutputs": ["width", "height", "channelCount"], "width": 4, "height": 4, "preservesProviderAuthority": true } }
]
}

View file

@ -4,11 +4,20 @@
"title": "Semantic Fields",
"owner": "EvePlugins",
"incubation": {
"status": "graduated",
"ownerRepo": "EvePlugins",
"currentHostRepo": "EvePlugins",
"splitTarget": "EvePlugins/eve-plugin-fields",
"graduationTrigger": "Split when field schemas, sidecar witness, and two runtime adapters are stable."
},
"graduation": {
"status": "graduated",
"runtimeAdapters": ["web-reference", "unity-scene"],
"evidence": [
"artifacts/fields-surface/runtime-witness.json",
"EveUnity/artifacts/fields-surface/latest/unity-fields.png"
]
},
"componentKinds": ["field.surface2d", "gravity.surface"],
"commands": [],
"runtime": {

View file

@ -62,6 +62,7 @@ export function measure(input) {
const projection = project(input);
return {
measurementKind: "field-domain-metrics",
measurementOutputs: ["width", "height", "channelCount"],
width: projection.bounds.maxX - projection.bounds.minX,
height: projection.bounds.maxY - projection.bounds.minY,
channelCount: projection.channels.length,

View file

@ -38,6 +38,7 @@ test("measures field domains without mutating provider state", () => {
assert.equal(result.width, 20);
assert.equal(result.height, 10);
assert.equal(result.channelCount, 1);
assert.deepEqual(result.measurementOutputs, ["width", "height", "channelCount"]);
assert.equal(result.preservesProviderAuthority, true);
});
@ -48,5 +49,15 @@ test("publishes a runtime-neutral Unity contract package", () => {
assert.equal(manifest.name, "org.gamecult.eve.plugin-fields");
assert.match(contracts, /interface IEveFieldsSplatsDocument/);
assert.match(contracts, /gamecult\.fields\.splats\.v1/);
assert.match(contracts, /AnimatedCellNoiseB = 3/);
assert.match(contracts, /AnimatedRadialCosine = 4/);
assert.match(contracts, /AbsoluteValue = 1/);
assert.doesNotMatch(contracts, /UnityEngine|Aetheria|MessagePack|GameCult\.Caching/);
assert.match(readFileSync(new URL("../GameCult.Eve.PluginFields.csproj", import.meta.url), "utf8"), /netstandard2\.1/);
});
test("records fields plugin graduation after two native runtime proofs", () => {
const manifest = JSON.parse(readFileSync(new URL("../plugin.json", import.meta.url), "utf8"));
assert.equal(manifest.graduation.status, "graduated");
assert.deepEqual(manifest.graduation.runtimeAdapters, ["web-reference", "unity-scene"]);
});

View file

@ -3,3 +3,26 @@
This package owns the portable C# view of `gamecult.fields.*` documents. It has
no Unity rendering, provider state, transport, or command authority. Providers
implement these read-only interfaces; Unity runtime adapters consume them.
`PowerPulse` is the portable compact-support radial falloff
`pow(saturate(1 - pow(distance01 * scale, 2)), exponent)`. Producers publish
`FalloffScale` and `FalloffExponent` alongside each SoA splat. Missing parameter
entries retain the neutral defaults `1` and `1`, so older v1 documents remain
readable.
Procedural sources are evaluated in field-world coordinates, not splat-local
UVs. `SimplexNoise` samples signed Ashima 3D simplex at
`(worldXY * frequency + phase, 0)`. `AnimatedSimplexNoise` uses
`simulationTimeSeconds * animationSpeed` for the third coordinate.
`AnimatedCellNoiseB` is the portable moving, wrappable cellular source whose
distance metric is `(length(frac(p) - .5) * 1.5 + .25) *
max(abs(x) * .866 + y * .5, -y)`. `AbsoluteValue` folds a signed source after
sampling. Constant offsets remain ordinary constant splats, so source sampling
and additive composition retain separate ownership.
`AnimatedRadialCosine` is splat-local rather than field-world anchored. It
evaluates
`cos(pow(distance01, FrequencyY) * FrequencyX + PhaseX + simulationTimeSeconds * AnimationSpeed)`.
`FrequencyX` is radial frequency and `FrequencyY` is the positive radial
exponent. The ordinary splat falloff owns the envelope, which keeps oscillation
and compact support independently composable.

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7122b42864d44e029d5d718a53d03186
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d6eb92523e7048c4844a7fe9d3bdcf4a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -43,6 +43,13 @@ namespace GameCult.Eve.PluginFields
public const int Constant = 0;
public const int SimplexNoise = 1;
public const int AnimatedSimplexNoise = 2;
public const int AnimatedCellNoiseB = 3;
public const int AnimatedRadialCosine = 4;
}
public static class EveFieldsSplatSourceFlags
{
public const int AbsoluteValue = 1;
}
public static class EveFieldsSplatFalloffs
@ -51,6 +58,7 @@ namespace GameCult.Eve.PluginFields
public const int Linear = 1;
public const int Smooth = 2;
public const int InverseSmooth = 3;
public const int PowerPulse = 4;
}
public interface IEveFieldsViewport
@ -99,6 +107,8 @@ namespace GameCult.Eve.PluginFields
IReadOnlyList<double> PhaseY { get; }
IReadOnlyList<double> AnimationSpeed { get; }
IReadOnlyList<double> SourceFlags { get; }
IReadOnlyList<double> FalloffScale { get; }
IReadOnlyList<double> FalloffExponent { get; }
}
public interface IEveFieldsSplatsDocument

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 115753bcd85347649537e429e9dd9e8f

View file

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using GameCult.Caching;
using MessagePack;
namespace GameCult.Eve.PluginFields
{
[CultDocument("gamecult.fields.splats", EveFieldsSchemas.Splats)]
[MessagePackObject]
public sealed class EveFieldsSplatsDocument : IEveFieldsSplatsDocument
{
[Key(0)] public string Schema { get; set; } = EveFieldsSchemas.Splats;
[Key(1)] public long FrameId { get; set; }
[Key(2)] public string PublishedAtUtc { get; set; } = "";
[Key(3)] public double SimulationTimeSeconds { get; set; }
[Key(4)] public string RunId { get; set; } = "";
[Key(5)] public int ZoneIndex { get; set; }
[Key(6)] public string ZoneName { get; set; } = "";
[Key(7)] public EveFieldsViewport Viewport { get; set; } = new EveFieldsViewport();
[Key(8)] public IReadOnlyList<EveFieldsSplatLayer> Layers { get; set; } = Array.Empty<EveFieldsSplatLayer>();
[Key(9)] public EveFieldsSplatSoa Splats { get; set; } = new EveFieldsSplatSoa();
IEveFieldsViewport IEveFieldsSplatsDocument.Viewport => Viewport;
IReadOnlyList<IEveFieldsSplatLayer> IEveFieldsSplatsDocument.Layers => Layers;
IEveFieldsSplatSoa IEveFieldsSplatsDocument.Splats => Splats;
}
[MessagePackObject]
public sealed class EveFieldsViewport : IEveFieldsViewport
{
[Key(0)] public double MinX { get; set; }
[Key(1)] public double MinY { get; set; }
[Key(2)] public double MaxX { get; set; }
[Key(3)] public double MaxY { get; set; }
}
[MessagePackObject]
public sealed class EveFieldsSplatLayer : IEveFieldsSplatLayer
{
[Key(0)] public string LayerKey { get; set; } = "";
[Key(1)] public string DisplayName { get; set; } = "";
[Key(2)] public int Channel { get; set; }
[Key(3)] public string BlendMode { get; set; } = EveFieldsSplatBlendModes.Add;
[Key(4)] public string GraphicsFormat { get; set; } = "R16_SFloat";
[Key(5)] public bool ClearBeforeDraw { get; set; } = true;
[Key(6)] public double ClearR { get; set; }
[Key(7)] public double ClearG { get; set; }
[Key(8)] public double ClearB { get; set; }
[Key(9)] public double ClearA { get; set; }
}
[MessagePackObject]
public sealed class EveFieldsSplatSoa : IEveFieldsSplatSoa
{
[Key(0)] public int Count { get; set; }
[Key(1)] public IReadOnlyList<double> CenterX { get; set; } = Array.Empty<double>();
[Key(2)] public IReadOnlyList<double> CenterY { get; set; } = Array.Empty<double>();
[Key(3)] public IReadOnlyList<double> HalfExtentX { get; set; } = Array.Empty<double>();
[Key(4)] public IReadOnlyList<double> HalfExtentY { get; set; } = Array.Empty<double>();
[Key(5)] public IReadOnlyList<double> RotationCos { get; set; } = Array.Empty<double>();
[Key(6)] public IReadOnlyList<double> RotationSin { get; set; } = Array.Empty<double>();
[Key(7)] public IReadOnlyList<int> Channel { get; set; } = Array.Empty<int>();
[Key(8)] public IReadOnlyList<int> Falloff { get; set; } = Array.Empty<int>();
[Key(9)] public IReadOnlyList<double> ValueR { get; set; } = Array.Empty<double>();
[Key(10)] public IReadOnlyList<double> ValueG { get; set; } = Array.Empty<double>();
[Key(11)] public IReadOnlyList<double> ValueB { get; set; } = Array.Empty<double>();
[Key(12)] public IReadOnlyList<double> ValueA { get; set; } = Array.Empty<double>();
[Key(13)] public IReadOnlyList<string> SourceKey { get; set; } = Array.Empty<string>();
[Key(14)] public IReadOnlyList<int> LayerIndex { get; set; } = Array.Empty<int>();
[Key(15)] public IReadOnlyList<int> SourceKind { get; set; } = Array.Empty<int>();
[Key(16)] public IReadOnlyList<double> FrequencyX { get; set; } = Array.Empty<double>();
[Key(17)] public IReadOnlyList<double> FrequencyY { get; set; } = Array.Empty<double>();
[Key(18)] public IReadOnlyList<double> PhaseX { get; set; } = Array.Empty<double>();
[Key(19)] public IReadOnlyList<double> PhaseY { get; set; } = Array.Empty<double>();
[Key(20)] public IReadOnlyList<double> AnimationSpeed { get; set; } = Array.Empty<double>();
[Key(21)] public IReadOnlyList<double> SourceFlags { get; set; } = Array.Empty<double>();
[Key(22)] public IReadOnlyList<double> FalloffScale { get; set; } = Array.Empty<double>();
[Key(23)] public IReadOnlyList<double> FalloffExponent { get; set; } = Array.Empty<double>();
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7e1b64023d224e8cb0bfaec57adad5c2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -2,6 +2,12 @@
"name": "GameCult.Eve.PluginFields",
"rootNamespace": "GameCult.Eve.PluginFields",
"references": [],
"overrideReferences": true,
"precompiledReferences": [
"GameCult.Caching.dll",
"MessagePack.Annotations.dll",
"MessagePack.dll"
],
"autoReferenced": true,
"noEngineReferences": true
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4b0812861c9b453cbd5b02c6dc84098c
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -1,8 +1,11 @@
{
"name": "org.gamecult.eve.plugin-fields",
"version": "0.1.0",
"version": "0.2.3",
"displayName": "Eve Fields Plugin Contracts",
"description": "Runtime-neutral C# contracts for gamecult.fields documents.",
"unity": "2022.3",
"unity": "2021.3",
"dependencies": {
"org.gamecult.cultlib": "1.0.13"
},
"license": "MPL-2.0"
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 59ad75f1e14ba2245b8766107f53a8b7
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: