Keep live entity transforms body-owned

This commit is contained in:
Meta Vens 2026-07-22 01:02:30 +01:00
commit e5556c3194
4 changed files with 65 additions and 6 deletions

View file

@ -23,14 +23,17 @@ namespace GameCult.Eve.UnityScene
{
private readonly IEveUnityPlayableWorldSceneSink _sceneSink;
private readonly IEveUnityPlayableWorldAssetResolver _assetResolver;
private readonly bool _liveEntityBodyOwner;
private readonly HashSet<string> _knownEntityIds = new HashSet<string>(StringComparer.Ordinal);
public EveUnityPlayableWorldPresenter(
IEveUnityPlayableWorldSceneSink sceneSink,
IEveUnityPlayableWorldAssetResolver assetResolver)
IEveUnityPlayableWorldAssetResolver assetResolver,
bool liveEntityBodyOwner = false)
{
_sceneSink = sceneSink ?? throw new ArgumentNullException(nameof(sceneSink));
_assetResolver = assetResolver ?? throw new ArgumentNullException(nameof(assetResolver));
_liveEntityBodyOwner = liveEntityBodyOwner;
}
public EveUnityPlayableWorldPresentation Apply(EveUnitySceneProjection projection)
@ -46,9 +49,26 @@ namespace GameCult.Eve.UnityScene
{
if (world == null) throw new ArgumentNullException(nameof(world));
var nextEntityIds = new HashSet<string>(StringComparer.Ordinal);
_sceneSink.ConfigureWorld(world);
// A world that advertises a live entity body has one transform and
// membership owner: the SoA generation presenter. The durable Eve
// surface remains configuration/topology and must not periodically
// overwrite realtime body state.
if (_liveEntityBodyOwner &&
(!string.IsNullOrWhiteSpace(world.EntityViewPointerId) ||
!string.IsNullOrWhiteSpace(world.EntityBodyId)))
return new EveUnityPlayableWorldPresentation(
world.WorldRootId,
world.PlayerEntityId,
world.InputProfile,
world.CameraRig,
0,
0,
_knownEntityIds.Count);
var nextEntityIds = new HashSet<string>(StringComparer.Ordinal);
var upserted = 0;
foreach (var entity in world.Entities)
{

View file

@ -41,7 +41,10 @@ namespace GameCult.Eve.UnityScene
_connection = new EveUnitySceneProviderConnection(_surfaceSource, commandSink);
_client = new EveUnityPlayableWorldLiveClient(
_connection,
new EveUnityPlayableWorldPresenter(sceneSink, assetResolver ?? new EveUnityAssetRefResolver()),
new EveUnityPlayableWorldPresenter(
sceneSink,
assetResolver ?? new EveUnityAssetRefResolver(),
liveEntityBodyOwner: _entityViews != null),
receiptSource);
if (assetManifestDocuments != null)

View file

@ -1856,7 +1856,7 @@ namespace GameCult.Eve.UnityScene.Tests
var presenter = new EveUnityPlayableWorldPresenter(sink, new EveUnityAssetRefResolver());
var firstProjection = lowerer.Lower(PlayableArpgDocument(), Advertisement("aetheria.daemon.game"));
var firstPresentation = presenter.Apply(firstProjection);
var firstPresentation = presenter.Apply(SnapshotWorld(firstProjection.PlayableWorld!));
Assert.That(firstPresentation.WorldRootId, Is.EqualTo("aetheria.daemon.game.playable"));
Assert.That(firstPresentation.PlayerEntityId, Is.EqualTo("player-vanguard"));
@ -1875,7 +1875,7 @@ namespace GameCult.Eve.UnityScene.Tests
var secondProjection = lowerer.Lower(
PlayableArpgDocument(includeRaider: false, playerPosition: "5,0,2"),
Advertisement("aetheria.daemon.game"));
var secondPresentation = presenter.Apply(secondProjection);
var secondPresentation = presenter.Apply(SnapshotWorld(secondProjection.PlayableWorld!));
Assert.That(secondPresentation.UpsertedEntities, Is.EqualTo(2));
Assert.That(secondPresentation.RemovedEntities, Is.EqualTo(1));
@ -1888,6 +1888,27 @@ namespace GameCult.Eve.UnityScene.Tests
Assert.That(sink.Upserts[3].entity.PositionZ, Is.EqualTo(2f));
}
[Test]
public void PlayableWorldSurfaceCannotOverwriteLiveBodyEntities()
{
var projection = new EveUnitySceneSurfaceLowerer()
.Lower(PlayableArpgDocument(), Advertisement("aetheria.daemon.game"));
var sink = new FakePlayableWorldSceneSink();
var presenter = new EveUnityPlayableWorldPresenter(
sink,
new EveUnityAssetRefResolver(),
liveEntityBodyOwner: true);
var presentation = presenter.Apply(projection);
Assert.That(projection.PlayableWorld!.EntityBodyId, Is.Not.Empty);
Assert.That(sink.ConfiguredWorlds.Count, Is.EqualTo(1));
Assert.That(sink.Upserts, Is.Empty);
Assert.That(sink.RemovedEntityIds, Is.Empty);
Assert.That(presentation.UpsertedEntities, Is.Zero);
Assert.That(presentation.RemovedEntities, Is.Zero);
}
[Test]
public void LivePlayableWorldClientPresentsProviderSnapshotsAndKeepsCommandsProviderOwned()
{
@ -2950,6 +2971,21 @@ namespace GameCult.Eve.UnityScene.Tests
}
}
private static EveUnityPlayableWorldProjection SnapshotWorld(EveUnityPlayableWorldProjection source) =>
new EveUnityPlayableWorldProjection(
source.WorldRootId,
source.StatePointerId,
source.AssetManifest,
source.InputProfile,
source.CameraRig,
source.ViewId,
source.PlayerEntityId,
source.MovementCommand,
source.FocusCommand,
source.TargetCommand,
source.ActionCommand,
source.Entities);
private static EveUnityPlayableWorldEntity PlayableEntityModel(
string entityId,
string assetRef,

View file

@ -1,7 +1,7 @@
{
"name": "org.gamecult.eve.unity-scene",
"displayName": "Eve Unity Scene Lowering",
"version": "0.3.103",
"version": "0.3.104",
"unity": "6000.0",
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
"dependencies": {