Lower provider-authored combat presentation
This commit is contained in:
parent
f0d25c5716
commit
d9b762f429
21 changed files with 1019 additions and 76 deletions
|
|
@ -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<EveUnityPlayableWorldInputDriver>() ??
|
||||
root.AddComponent<EveUnityPlayableWorldInputDriver>();
|
||||
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<EveUnityShotTrajectoryRenderer>();
|
||||
var combatRenderer = root.GetComponent<EveUnityCombatPresentationRenderer>();
|
||||
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>();
|
||||
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<EveUnityPlayableWorldEntityMarker>())
|
||||
{
|
||||
foreach (var renderer in entityMarker.GetComponentsInChildren<Renderer>())
|
||||
{
|
||||
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<Renderer>();
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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<EveUnityPlayableWorldInputDriver>() ??
|
||||
root.AddComponent<EveUnityPlayableWorldInputDriver>();
|
||||
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<EveUnityShotTrajectoryRenderer>();
|
||||
var combatRenderer = root.GetComponent<EveUnityCombatPresentationRenderer>();
|
||||
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>();
|
||||
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<EveUnityPlayableWorldEntityMarker>())
|
||||
{
|
||||
foreach (var renderer in entityMarker.GetComponentsInChildren<Renderer>())
|
||||
{
|
||||
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<Renderer>();
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<EveUnitySceneProviderSurfaceDocument>? SurfaceDocumentAvailable;
|
||||
|
||||
public event Action<EveUnityPlayableWorldAssetManifestDocument>? 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<EveInputCapabilityDocument>(
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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<EveUnitySceneProviderSurfaceDocument>? DocumentAvailable
|
||||
{
|
||||
add { Bridge.DocumentAvailable += value; }
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e15acab8e9094b18b6dbd3abea0e055d
|
||||
|
|
@ -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<EveUnityPlayableWorldEntityMarker>(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<LineRenderer>();
|
||||
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<Renderer>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5c953d99a7454322b2935b878295b357
|
||||
|
|
@ -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<Renderer>(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<Renderer>(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)
|
||||
|
|
|
|||
|
|
@ -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<EveUnityFeedbackEvent>? FeedbackAvailable;
|
||||
|
|
@ -89,6 +92,9 @@ namespace GameCult.Eve.UnityScene
|
|||
var feedbackEffects = GetComponent<EveUnityFeedbackEffectRenderer>();
|
||||
if (feedbackEffects == null) feedbackEffects = gameObject.AddComponent<EveUnityFeedbackEffectRenderer>();
|
||||
feedbackEffects.Bind(this, Runtime.GameObjectAssetProvider);
|
||||
var combatPresentation = GetComponent<EveUnityCombatPresentationRenderer>();
|
||||
if (combatPresentation == null) combatPresentation = gameObject.AddComponent<EveUnityCombatPresentationRenderer>();
|
||||
combatPresentation.Bind(this);
|
||||
var thermal = GetComponent<EveUnityThermalPresenter>();
|
||||
var thermalHud = GetComponent<EveUnityThermalHudSink>();
|
||||
if (thermalHud == null) thermalHud = gameObject.AddComponent<EveUnityThermalHudSink>();
|
||||
|
|
|
|||
|
|
@ -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<EveInputProfileDocument>())
|
||||
.FirstOrDefault(value => string.Equals(value.DeviceClass, "keyboard-mouse", StringComparison.Ordinal))
|
||||
?? (capability.DefaultProfiles ?? Array.Empty<EveInputProfileDocument>()).FirstOrDefault();
|
||||
var binding = (profile?.Bindings ?? Array.Empty<EveInputBindingDocument>())
|
||||
.FirstOrDefault(value => (value.Gesture?.Controls ?? Array.Empty<string>())
|
||||
.Any(control => string.Equals(control, "mouse.primary", StringComparison.Ordinal)))
|
||||
?? (profile?.Bindings ?? Array.Empty<EveInputBindingDocument>()).FirstOrDefault(value => value.ActionBar);
|
||||
if (binding == null)
|
||||
return "";
|
||||
var action = (capability.Actions ?? Array.Empty<EveInputActionDocument>())
|
||||
.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()
|
||||
|
|
|
|||
|
|
@ -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 ?? "" : "";
|
||||
|
|
|
|||
|
|
@ -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<string, string>(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<string, string>(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<string, string> props, params EveUnitySceneNode[] children) =>
|
||||
new EveUnitySceneNode(id, kind, "scene-node", props, Empty(), Empty(), 0, 0,
|
||||
Array.Empty<EveUnitySceneEmbeddedDocumentSlot>(), null, children);
|
||||
|
||||
private static IReadOnlyDictionary<string, string> Empty() =>
|
||||
new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 30c8f313c1ea4b43858ed212909b7ce9
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<EveUnitySceneEmbeddedDocumentSlot>(), null,
|
||||
Array.Empty<EveUnitySceneNode>());
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
121
scripts/run-interactive-aetheria.ps1
Normal file
121
scripts/run-interactive-aetheria.ps1
Normal file
|
|
@ -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
|
||||
}
|
||||
Reference in a new issue