Lower generic docked world presentation

This commit is contained in:
Meta Vens 2026-07-13 05:33:45 +01:00
commit e44fcb0efa
6 changed files with 136 additions and 24 deletions

View file

@ -190,6 +190,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
string combatActionId = "";
string collectedPickupId = "";
bool pickupCollected = false;
bool docked = false;
bool undocked = false;
string stationObservedHostileId = "";
long initialVersion = 0;
float movementDistance = 0f;
var commandReceipts = new Dictionary<string, EveUnitySceneCommandReceipt>(StringComparer.Ordinal);
@ -251,6 +254,11 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
seededPickup.GetComponentsInChildren<Transform>(includeInactive: true).Length,
Is.GreaterThan(1),
"The generic client did not instantiate the provider-authored pickup asset.");
var dockAction = provider.CurrentInputCapability.Actions
.Single(candidate => string.Equals(candidate.ActionId, "pilot.dock", StringComparison.Ordinal));
var undockAction = provider.CurrentInputCapability.Actions
.Single(candidate => string.Equals(candidate.ActionId, "pilot.undock", StringComparison.Ordinal));
var dock = runtime.SubmitActionIntent(playerId, dockAction.ActionId);
var realtimeAction = provider.CurrentInputCapability?.Actions
.FirstOrDefault(action => string.Equals(action.ActionId, "simulation.rate.realtime", StringComparison.Ordinal));
Assert.That(realtimeAction, Is.Not.Null, "Terminus did not advertise its user-controlled simulation clock.");
@ -268,6 +276,49 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
marker.GetComponentsInChildren<Transform>(includeInactive: true).Length,
Is.GreaterThan(1),
"The generic client substituted its primitive fallback instead of the provider-authored AssetBundle prefab.");
var dockDeadline = Time.realtimeSinceStartup + 12f;
while (Time.realtimeSinceStartup < dockDeadline &&
(!commandReceipts.TryGetValue(dock.CommandId, out var observedDock) ||
runtime.ActiveVersion < observedDock.SourceVersion ||
!string.Equals(runtime.ActiveWorld.PresentationMode, "docked", StringComparison.Ordinal)))
{
yield return new WaitForSecondsRealtime(0.1f);
runtime.Refresh();
}
Assert.That(commandReceipts.TryGetValue(dock.CommandId, out var dockReceipt), Is.True);
Assert.That(dockReceipt.State, Is.EqualTo("accepted").Or.EqualTo("reconciled"), dockReceipt.Message);
Assert.That(runtime.ActiveWorld.PresentationMode, Is.EqualTo("docked"));
Assert.That(runtime.ActiveWorld.CameraTargetEntityId, Is.Not.Empty.And.Not.EqualTo(playerId));
Assert.That(FindMarker(root, runtime.ActiveWorld.CameraTargetEntityId), Is.Not.Null,
"The daemon did not include the authoritative dock parent in the docked SoA view.");
stationObservedHostileId = provider.CurrentEntityView.Identities
.FirstOrDefault(identity => string.Equals(identity.Faction, "raider", StringComparison.Ordinal))?.EntityId ?? "";
Assert.That(stationObservedHostileId, Is.Not.Empty,
"The docked pilot did not inherit the station's sensor-derived hostile contacts.");
Assert.That(runtime.ActiveWorld.SubjectVisible, Is.False);
Assert.That(runtime.ActiveWorld.MovementEnabled, Is.False);
Assert.That(FindMarker(root, playerId, includeInactive: true).gameObject.activeSelf, Is.False);
docked = true;
var undock = runtime.SubmitActionIntent(playerId, undockAction.ActionId);
var undockDeadline = Time.realtimeSinceStartup + 12f;
while (Time.realtimeSinceStartup < undockDeadline &&
(!commandReceipts.TryGetValue(undock.CommandId, out var observedUndock) ||
runtime.ActiveVersion < observedUndock.SourceVersion ||
!string.Equals(runtime.ActiveWorld.PresentationMode, "world", StringComparison.Ordinal)))
{
yield return new WaitForSecondsRealtime(0.1f);
runtime.Refresh();
}
Assert.That(commandReceipts.TryGetValue(undock.CommandId, out var undockReceipt), Is.True);
Assert.That(undockReceipt.State, Is.EqualTo("accepted").Or.EqualTo("reconciled"), undockReceipt.Message);
Assert.That(runtime.ActiveWorld.PresentationMode, Is.EqualTo("world"));
Assert.That(runtime.ActiveWorld.CameraTargetEntityId, Is.EqualTo(playerId));
Assert.That(runtime.ActiveWorld.SubjectVisible, Is.True);
Assert.That(runtime.ActiveWorld.MovementEnabled, Is.True);
Assert.That(FindMarker(root, playerId).gameObject.activeSelf, Is.True);
undocked = true;
var initialPosition = marker.transform.position;
initialVersion = runtime.ActiveVersion;
var request = runtime.SubmitMoveVectorIntent(playerId, 1f, 0f, 1f);
@ -337,17 +388,18 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
var visibleMarkers = root.GetComponentsInChildren<EveUnityPlayableWorldEntityMarker>();
var hostileTargets = visibleMarkers
.Where(candidate => candidate.Selectable && !candidate.Controllable &&
string.Equals(candidate.EntityKind, "ship", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(candidate.Faction, marker.Faction, StringComparison.Ordinal))
.OrderBy(candidate => Vector3.Distance(candidate.transform.position, marker.transform.position))
.ToArray();
Assert.That(
hostileTargets,
Is.Not.Empty,
"No selectable hostile contact was lowered. Visible entities: " +
var targetEntityId = hostileTargets.Length > 0
? hostileTargets[0].EntityId
: stationObservedHostileId;
Assert.That(targetEntityId, Is.Not.Empty,
"No hostile contact was available from pilot or shared station sensors. Visible entities: " +
string.Join(", ", visibleMarkers.Select(candidate =>
$"{candidate.EntityId}[kind={candidate.EntityKind},faction={candidate.Faction},selectable={candidate.Selectable}]")));
var targetMarker = hostileTargets[0];
var focus = runtime.SubmitTargetIntent(playerId, targetMarker.EntityId);
var focus = runtime.SubmitTargetIntent(playerId, targetEntityId);
var focusVersion = runtime.ActiveVersion;
var trajectoryRenderer = root.AddComponent<EveUnityShotTrajectoryRenderer>();
runtime.ShotAvailable += shot =>
@ -464,7 +516,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
combatActionId,
combatShot,
collectedPickupId,
pickupCollected);
pickupCollected,
docked,
undocked);
}
finally
{
@ -490,7 +544,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
string combatActionId,
EveUnityShotReceipt combatShot,
string collectedPickupId,
bool pickupCollected)
bool pickupCollected,
bool docked,
bool undocked)
{
var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH");
if (string.IsNullOrWhiteSpace(path)) return;
@ -509,6 +565,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
shotHit = combatShot?.Hit ?? false,
pickupVisible = !string.IsNullOrWhiteSpace(collectedPickupId),
pickupCollected = pickupCollected,
docked = docked,
undocked = undocked,
pickupId = collectedPickupId ?? "",
initialVersion = initialVersion,
finalVersion = finalVersion,
@ -535,6 +593,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
public bool shotHit;
public bool pickupVisible;
public bool pickupCollected;
public bool docked;
public bool undocked;
public string pickupId;
public long initialVersion;
public long finalVersion;
@ -562,9 +622,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
}
}
private static EveUnityPlayableWorldEntityMarker FindMarker(GameObject root, string entityId)
private static EveUnityPlayableWorldEntityMarker FindMarker(
GameObject root,
string entityId,
bool includeInactive = false)
{
foreach (var marker in root.GetComponentsInChildren<EveUnityPlayableWorldEntityMarker>())
foreach (var marker in root.GetComponentsInChildren<EveUnityPlayableWorldEntityMarker>(includeInactive))
{
if (string.Equals(marker.EntityId, entityId, StringComparison.Ordinal))
return marker;

View file

@ -21,6 +21,8 @@ namespace GameCult.Eve.UnityScene
private readonly Transform _root;
private readonly IEveUnityGameObjectAssetProvider _assetProvider;
private readonly Dictionary<string, GameObject> _instances = new Dictionary<string, GameObject>(StringComparer.Ordinal);
private string _subjectEntityId = "";
private bool _subjectVisible = true;
public EveUnityGameObjectPlayableWorldSceneSink(
Transform root,
@ -37,6 +39,9 @@ namespace GameCult.Eve.UnityScene
if (world == null) throw new ArgumentNullException(nameof(world));
if (!string.IsNullOrWhiteSpace(world.WorldRootId))
_root.name = world.WorldRootId;
_subjectEntityId = world.PlayerEntityId;
_subjectVisible = world.SubjectVisible;
ApplySubjectVisibility();
}
public void UpsertEntity(EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset)
@ -64,6 +69,7 @@ namespace GameCult.Eve.UnityScene
}
ApplyEntity(instance, entity, asset);
ApplySubjectVisibility(instance, entity.EntityId);
}
public void RemoveEntity(string entityId)
@ -140,6 +146,19 @@ namespace GameCult.Eve.UnityScene
else
UnityEngine.Object.DestroyImmediate(instance);
}
private void ApplySubjectVisibility()
{
foreach (var pair in _instances)
ApplySubjectVisibility(pair.Value, pair.Key);
}
private void ApplySubjectVisibility(GameObject instance, string entityId)
{
if (instance == null || !string.Equals(entityId, _subjectEntityId, StringComparison.Ordinal))
return;
instance.SetActive(_subjectVisible);
}
}
public sealed class EveUnitySemanticEntityPresentation : MonoBehaviour

View file

@ -53,8 +53,8 @@ namespace GameCult.Eve.UnityScene
}
var camera = cameraTransform != null ? cameraTransform : transform;
var player = FindPlayerMarker(resolvedHost, activeWorld.PlayerEntityId);
if (player == null)
var targetMarker = FindEntityMarker(resolvedHost, activeWorld.CameraTargetEntityId);
if (targetMarker == null)
return false;
var cameraComponent = camera.GetComponent<Camera>();
@ -64,8 +64,8 @@ namespace GameCult.Eve.UnityScene
{
cameraComponent.cullingMask = cullingMask;
}
var bounds = CalculateVisualBounds(player);
var target = bounds?.center ?? player.transform.position;
var bounds = CalculateVisualBounds(targetMarker);
var target = bounds?.center ?? targetMarker.transform.position;
var radius = bounds?.extents.magnitude ?? 0f;
var verticalFov = cameraComponent != null ? cameraComponent.fieldOfView : 60f;
var framingDistance = radius > 0f
@ -84,9 +84,9 @@ namespace GameCult.Eve.UnityScene
return true;
}
private static Bounds? CalculateVisualBounds(EveUnityPlayableWorldEntityMarker player)
private static Bounds? CalculateVisualBounds(EveUnityPlayableWorldEntityMarker target)
{
var renderers = player.GetComponentsInChildren<Renderer>(includeInactive: false);
var renderers = target.GetComponentsInChildren<Renderer>(includeInactive: false);
Bounds? bounds = null;
foreach (var visual in renderers)
{
@ -121,17 +121,17 @@ namespace GameCult.Eve.UnityScene
return host;
}
private static EveUnityPlayableWorldEntityMarker? FindPlayerMarker(
private static EveUnityPlayableWorldEntityMarker? FindEntityMarker(
EveUnityPlayableWorldClientHost host,
string playerEntityId)
string entityId)
{
if (string.IsNullOrWhiteSpace(playerEntityId))
if (string.IsNullOrWhiteSpace(entityId))
return null;
var markers = host.SceneRoot.GetComponentsInChildren<EveUnityPlayableWorldEntityMarker>();
var markers = host.SceneRoot.GetComponentsInChildren<EveUnityPlayableWorldEntityMarker>(includeInactive: true);
foreach (var marker in markers)
{
if (marker != null && marker.EntityId == playerEntityId)
if (marker != null && marker.EntityId == entityId)
return marker;
}

View file

@ -39,7 +39,7 @@ namespace GameCult.Eve.UnityScene
DateTimeOffset? issuedAt = null)
{
var resolvedHost = ResolveHost();
if (resolvedHost == null || resolvedHost.ActiveWorld == null)
if (resolvedHost == null || resolvedHost.ActiveWorld == null || !resolvedHost.ActiveWorld.MovementEnabled)
return null;
var movement = EveUnityPlayableWorldMoveVector.FromCameraRelativeInput(

View file

@ -125,7 +125,11 @@ namespace GameCult.Eve.UnityScene
worldRoot.GetProp("entityViewSchema"),
worldRoot.GetProp("zoneRenderPointerId"),
worldRoot.GetProp("zoneRenderSchema"),
worldRoot.GetProp("inputCapability"));
worldRoot.GetProp("inputCapability"),
worldRoot.GetProp("cameraTargetEntityId"),
ParseBoolean(worldRoot.GetProp("subjectVisible", "true")),
ParseBoolean(worldRoot.GetProp("movementEnabled", "true")),
worldRoot.GetProp("presentationMode", "world"));
}
private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component)
@ -326,7 +330,11 @@ namespace GameCult.Eve.UnityScene
string entityViewSchema = "",
string zoneRenderPointerId = "",
string zoneRenderSchema = "",
string inputCapabilityPointerId = "")
string inputCapabilityPointerId = "",
string cameraTargetEntityId = "",
bool subjectVisible = true,
bool movementEnabled = true,
string presentationMode = "world")
{
WorldRootId = worldRootId ?? "";
StatePointerId = statePointerId ?? "";
@ -345,6 +353,12 @@ namespace GameCult.Eve.UnityScene
ZoneRenderPointerId = zoneRenderPointerId ?? "";
ZoneRenderSchema = zoneRenderSchema ?? "";
InputCapabilityPointerId = inputCapabilityPointerId ?? "";
CameraTargetEntityId = string.IsNullOrWhiteSpace(cameraTargetEntityId)
? PlayerEntityId
: cameraTargetEntityId;
SubjectVisible = subjectVisible;
MovementEnabled = movementEnabled;
PresentationMode = presentationMode ?? "";
}
public string WorldRootId { get; }
@ -371,6 +385,14 @@ namespace GameCult.Eve.UnityScene
public string PlayerEntityId { get; }
public string CameraTargetEntityId { get; }
public bool SubjectVisible { get; }
public bool MovementEnabled { get; }
public string PresentationMode { get; }
public string MovementCommand { get; }
public string FocusCommand { get; }

View file

@ -117,6 +117,10 @@ namespace GameCult.Eve.UnityScene.Tests
Assert.That(projection.PlayableWorld.InputProfile, Is.EqualTo("arpg-third-person"));
Assert.That(projection.PlayableWorld.CameraRig, Is.EqualTo("third-person-orbit"));
Assert.That(projection.PlayableWorld.PlayerEntityId, Is.EqualTo("player-vanguard"));
Assert.That(projection.PlayableWorld.CameraTargetEntityId, Is.EqualTo("player-vanguard"));
Assert.That(projection.PlayableWorld.SubjectVisible, Is.True);
Assert.That(projection.PlayableWorld.MovementEnabled, Is.True);
Assert.That(projection.PlayableWorld.PresentationMode, Is.EqualTo("world"));
Assert.That(projection.PlayableWorld.MovementCommand, Is.EqualTo("aetheria.daemon.move_intent"));
Assert.That(projection.PlayableWorld.FocusCommand, Is.EqualTo("aetheria.daemon.focus"));
Assert.That(projection.PlayableWorld.TargetCommand, Is.EqualTo("aetheria.daemon.target"));
@ -1419,6 +1423,10 @@ namespace GameCult.Eve.UnityScene.Tests
["inputProfile"] = "arpg-third-person",
["cameraRig"] = "third-person-orbit",
["playerEntityId"] = "player-vanguard",
["cameraTargetEntityId"] = "player-vanguard",
["subjectVisible"] = "true",
["movementEnabled"] = "true",
["presentationMode"] = "world",
["movementCommand"] = "aetheria.daemon.move_intent",
["focusCommand"] = "aetheria.daemon.focus",
["targetCommand"] = "aetheria.daemon.target",