Lower generic scalar actions and action bar

This commit is contained in:
Meta Vens 2026-07-17 05:24:08 +01:00
commit e08fa08335
11 changed files with 583 additions and 14 deletions

View file

@ -30,6 +30,8 @@ namespace GameCult.Eve.Surface
[Key(6)] public string SourceRef { get; set; } = "";
[Key(7)] public Dictionary<string, string> Payload { get; set; } = new Dictionary<string, string>(StringComparer.Ordinal);
[Key(8)] public EveInputValueDocument? InputValue { get; set; }
[Key(9)] public string IconRef { get; set; } = "";
[Key(10)] public bool ActionBar { get; set; }
}
[MessagePackObject]
@ -38,6 +40,11 @@ namespace GameCult.Eve.Surface
[Key(0)] public string Model { get; set; } = "";
[Key(1)] public string PayloadKey { get; set; } = "";
[Key(2)] public string[] PayloadKeys { get; set; } = Array.Empty<string>();
[Key(3)] public double? CurrentValue { get; set; }
[Key(4)] public double? MinimumValue { get; set; }
[Key(5)] public double? MaximumValue { get; set; }
[Key(6)] public double? StepValue { get; set; }
[Key(7)] public string Unit { get; set; } = "";
}
[MessagePackObject]

View file

@ -1,7 +1,7 @@
{
"name": "org.gamecult.eve.surface",
"displayName": "Eve Surface Contract",
"version": "0.2.3",
"version": "0.2.4",
"unity": "2021.3",
"description": "Unity-compatible DTOs for gamecult.eve.surface.v1 documents.",
"dependencies": {

View file

@ -22,6 +22,14 @@ active world camera's normalized world-space forward direction into the three
advertised payload keys. Providers own what that ray means; EveUnity never
selects targets or imports provider gameplay policy.
`EveUnityInputActionBar` lowers provider suggestions and profile-visible actions
from that same catalog. It resolves `iconRef` through the provider asset
manifest, disables unavailable actions, preserves press/release behavior for
held controls, samples the active camera for view-direction controls, and
submits `scalar.v1` values from an explicit numeric field. Its named UI Toolkit
elements and classes are presentation levers; the component never imports a
provider assembly or mutates provider-owned current values optimistically.
`IEveUnityPresentedEntityRegistry` exposes each committed SoA generation as
immutable entity facts plus presentation transforms, addressable by Eve entity
id or source index. Camera, selection, and provider UI adapters consume that

View file

@ -12,6 +12,7 @@ namespace GameCult.Eve.UnityScene
{
public const string ButtonHoldValueModel = "button-hold.v1";
public const string AxisValueModel = "axis.v1";
public const string ScalarValueModel = "scalar.v1";
public const string ViewDirectionValueModel = "view-direction.v1";
private readonly EveInputActionDocument _action;
@ -27,10 +28,19 @@ namespace GameCult.Eve.UnityScene
public string InputValueModel => _action.InputValue?.Model ?? "";
public string InputValuePayloadKey => _action.InputValue?.PayloadKey ?? "";
public IReadOnlyList<string> InputValuePayloadKeys => _action.InputValue?.PayloadKeys ?? Array.Empty<string>();
public double? CurrentValue => _action.InputValue?.CurrentValue;
public double? MinimumValue => _action.InputValue?.MinimumValue;
public double? MaximumValue => _action.InputValue?.MaximumValue;
public double? StepValue => _action.InputValue?.StepValue;
public string Unit => _action.InputValue?.Unit ?? "";
public bool IsButtonHold => string.Equals(InputValueModel, ButtonHoldValueModel, StringComparison.Ordinal);
public bool IsScalar => string.Equals(InputValueModel, ScalarValueModel, StringComparison.Ordinal);
public bool IsViewDirection => string.Equals(InputValueModel, ViewDirectionValueModel, StringComparison.Ordinal);
public static EveUnityAdvertisedInputAction Resolve(EveInputCapabilityDocument? capability, string actionId)
public static EveUnityAdvertisedInputAction Resolve(
EveInputCapabilityDocument? capability,
string actionId,
bool requireAvailable = true)
{
if (capability == null)
throw new InvalidOperationException("The provider did not advertise an input capability.");
@ -38,7 +48,7 @@ namespace GameCult.Eve.UnityScene
string.Equals(candidate?.ActionId, actionId, StringComparison.Ordinal));
if (action == null || string.IsNullOrWhiteSpace(action.Operation))
throw new InvalidOperationException($"Input action '{actionId}' is not advertised with an operation.");
if (!string.Equals(action.Availability, "available", StringComparison.OrdinalIgnoreCase))
if (requireAvailable && !string.Equals(action.Availability, "available", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException($"Input action '{actionId}' is not available.");
return new EveUnityAdvertisedInputAction(action);
}
@ -90,6 +100,31 @@ namespace GameCult.Eve.UnityScene
return payload;
}
public IReadOnlyDictionary<string, string> BuildScalarPayload(string entityId, double value)
{
if (!IsScalar)
throw new InvalidOperationException($"Input action '{ActionId}' does not advertise a scalar value.");
if (double.IsNaN(value) || double.IsInfinity(value))
throw new ArgumentOutOfRangeException(nameof(value), "Scalar input value must be finite.");
if (!CurrentValue.HasValue || double.IsNaN(CurrentValue.Value) || double.IsInfinity(CurrentValue.Value))
throw new InvalidOperationException($"Input action '{ActionId}' does not advertise a finite current scalar value.");
if (MinimumValue.HasValue && value < MinimumValue.Value)
throw new ArgumentOutOfRangeException(nameof(value), $"Scalar input value is below the advertised minimum {MinimumValue.Value}.");
if (MaximumValue.HasValue && value > MaximumValue.Value)
throw new ArgumentOutOfRangeException(nameof(value), $"Scalar input value is above the advertised maximum {MaximumValue.Value}.");
if (MinimumValue.HasValue && MaximumValue.HasValue && MinimumValue.Value > MaximumValue.Value)
throw new InvalidOperationException($"Input action '{ActionId}' advertises an inverted scalar range.");
if (StepValue.HasValue && (!IsFinite(StepValue.Value) || StepValue.Value <= 0))
throw new InvalidOperationException($"Input action '{ActionId}' advertises an invalid scalar step.");
if (string.IsNullOrWhiteSpace(InputValuePayloadKey))
throw new InvalidOperationException($"Input action '{ActionId}' does not advertise a scalar payload key.");
var payload = BuildPayload(entityId).ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
payload[InputValuePayloadKey] = value.ToString("R", CultureInfo.InvariantCulture);
return payload;
}
private static bool IsFinite(float value) => !float.IsNaN(value) && !float.IsInfinity(value);
private static bool IsFinite(double value) => !double.IsNaN(value) && !double.IsInfinity(value);
}
}

View file

@ -0,0 +1,306 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using GameCult.Eve.Surface;
using UnityEngine;
using UnityEngine.UIElements;
#nullable enable
namespace GameCult.Eve.UnityScene
{
public sealed class EveUnityInputActionBar : MonoBehaviour
{
private readonly Dictionary<string, ActionRow> _rows =
new Dictionary<string, ActionRow>(StringComparer.Ordinal);
private readonly HashSet<string> _pressedHolds = new HashSet<string>(StringComparer.Ordinal);
private EveUnityPlayableWorldClientHost? _host;
private UIDocument? _document;
private VisualElement? _root;
private string _structureKey = "";
private float _nextRefreshAt;
public VisualElement? Root => _root;
public IReadOnlyCollection<string> PresentedActionIds => _rows.Keys;
private void Awake() => EnsureBuilt();
public void Bind(EveUnityPlayableWorldClientHost host)
{
_host = host != null ? host : throw new ArgumentNullException(nameof(host));
EnsureBuilt();
RefreshNow();
}
private void Update()
{
if (Time.unscaledTime < _nextRefreshAt) return;
_nextRefreshAt = Time.unscaledTime + 0.2f;
RefreshNow();
}
public void RefreshNow()
{
EnsureBuilt();
var capability = _host?.InputCapability;
var actions = SelectActions(capability).ToArray();
var structureKey = string.Join("\n", actions.Select(action =>
$"{action.ActionId}\t{action.Label}\t{action.IconRef}\t{action.InputValue?.Model}"));
if (!string.Equals(_structureKey, structureKey, StringComparison.Ordinal))
{
ReleaseAllHolds();
Rebuild(actions);
_structureKey = structureKey;
}
foreach (var action in actions)
if (_rows.TryGetValue(action.ActionId ?? "", out var row))
Apply(row, action);
if (_root != null)
_root.style.display = actions.Length == 0 ? DisplayStyle.None : DisplayStyle.Flex;
}
public static IReadOnlyList<EveInputActionDocument> SelectActions(EveInputCapabilityDocument? capability)
{
if (capability == null) return Array.Empty<EveInputActionDocument>();
var actions = (capability.Actions ?? Array.Empty<EveInputActionDocument>())
.Where(action => action != null && !string.IsNullOrWhiteSpace(action.ActionId))
.ToDictionary(action => action.ActionId, StringComparer.Ordinal);
var selected = new List<EveInputActionDocument>();
var seen = new HashSet<string>(StringComparer.Ordinal);
var profile = (capability.DefaultProfiles ?? Array.Empty<EveInputProfileDocument>())
.FirstOrDefault(candidate => string.Equals(candidate.DeviceClass, "keyboard-mouse", StringComparison.Ordinal))
?? (capability.DefaultProfiles ?? Array.Empty<EveInputProfileDocument>()).FirstOrDefault();
foreach (var binding in profile?.Bindings ?? Array.Empty<EveInputBindingDocument>())
if (binding.ActionBar && actions.TryGetValue(binding.ActionId ?? "", out var action) && seen.Add(action.ActionId))
selected.Add(action);
foreach (var action in actions.Values.OrderBy(candidate => candidate.ActionId, StringComparer.Ordinal))
if (action.ActionBar && seen.Add(action.ActionId))
selected.Add(action);
return selected;
}
public void SubmitPerformed(string actionId)
{
var host = RequireHost();
var action = EveUnityAdvertisedInputAction.Resolve(host.InputCapability, actionId);
if (action.IsViewDirection)
{
var view = host.ActiveCameraTransform;
if (view == null || view.forward.sqrMagnitude <= 0.000001f) return;
var direction = view.forward.normalized;
host.SubmitAdvertisedActionViewDirectionIntent(
RequireEntityId(host), actionId, direction.x, direction.y, direction.z);
return;
}
host.SubmitAdvertisedActionIntent(RequireEntityId(host), actionId);
}
public void SubmitHold(string actionId, bool active)
{
var host = RequireHost();
host.SubmitAdvertisedActionValueIntent(RequireEntityId(host), actionId, active ? 1f : 0f);
}
public void SubmitScalar(string actionId, string value)
{
if (!double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed) ||
double.IsNaN(parsed) || double.IsInfinity(parsed))
throw new FormatException($"'{value}' is not a finite scalar value.");
var host = RequireHost();
host.SubmitAdvertisedActionScalarIntent(RequireEntityId(host), actionId, parsed);
}
private void Rebuild(IReadOnlyList<EveInputActionDocument> actions)
{
_root!.Clear();
_rows.Clear();
_pressedHolds.Clear();
foreach (var action in actions)
{
var row = BuildRow(action);
_rows[action.ActionId] = row;
_root.Add(row.Root);
}
}
private void OnDisable() => ReleaseAllHolds();
private void ReleaseAllHolds()
{
foreach (var actionId in _pressedHolds.ToArray())
{
try { SubmitHold(actionId, false); }
catch (InvalidOperationException) { }
}
_pressedHolds.Clear();
}
private ActionRow BuildRow(EveInputActionDocument action)
{
var root = new VisualElement { name = "eve-action-slot-" + SafeName(action.ActionId) };
root.AddToClassList("eve-input-action-slot");
root.style.width = 112;
root.style.minHeight = 72;
root.style.paddingLeft = root.style.paddingRight = 6;
root.style.paddingTop = root.style.paddingBottom = 5;
root.style.marginLeft = root.style.marginRight = 3;
root.style.backgroundColor = new Color(0.025f, 0.03f, 0.035f, 0.88f);
root.style.borderLeftWidth = root.style.borderRightWidth = 1;
root.style.borderTopWidth = root.style.borderBottomWidth = 1;
root.style.borderLeftColor = root.style.borderRightColor = new Color(0.35f, 0.42f, 0.46f, 0.8f);
root.style.borderTopColor = root.style.borderBottomColor = new Color(0.35f, 0.42f, 0.46f, 0.8f);
var icon = new Image { name = "icon", scaleMode = ScaleMode.ScaleToFit };
icon.AddToClassList("eve-input-action-icon");
icon.style.height = 28;
root.Add(icon);
var label = new Label(action.Label ?? action.ActionId) { name = "label" };
label.AddToClassList("eve-input-action-label");
label.style.fontSize = 9;
label.style.color = Color.white;
label.style.unityTextAlign = TextAnchor.MiddleCenter;
label.style.whiteSpace = WhiteSpace.Normal;
root.Add(label);
TextField? scalar = null;
Button? trigger = null;
if (string.Equals(action.InputValue?.Model, EveUnityAdvertisedInputAction.ScalarValueModel, StringComparison.Ordinal))
{
var controls = new VisualElement();
controls.style.flexDirection = FlexDirection.Row;
scalar = new TextField { name = "scalar" };
scalar.style.flexGrow = 1;
scalar.RegisterCallback<KeyDownEvent>(evt =>
{
if (evt.keyCode != KeyCode.Return && evt.keyCode != KeyCode.KeypadEnter) return;
SubmitScalar(action.ActionId, scalar.value);
evt.StopPropagation();
});
controls.Add(scalar);
trigger = new Button(() => SubmitScalar(action.ActionId, scalar.value)) { text = "Set", name = "submit" };
controls.Add(trigger);
root.Add(controls);
}
else
{
trigger = new Button { text = "Use", name = "trigger" };
if (string.Equals(action.InputValue?.Model, EveUnityAdvertisedInputAction.ButtonHoldValueModel, StringComparison.Ordinal))
{
trigger.RegisterCallback<PointerDownEvent>(evt =>
{
if (!_pressedHolds.Add(action.ActionId)) return;
trigger.CapturePointer(evt.pointerId);
SubmitHold(action.ActionId, true);
});
trigger.RegisterCallback<PointerUpEvent>(evt =>
{
ReleaseHold(action.ActionId);
if (trigger.HasPointerCapture(evt.pointerId)) trigger.ReleasePointer(evt.pointerId);
});
trigger.RegisterCallback<PointerCaptureOutEvent>(_ => ReleaseHold(action.ActionId));
}
else
{
trigger.clicked += () => SubmitPerformed(action.ActionId);
}
root.Add(trigger);
}
return new ActionRow(root, icon, label, trigger, scalar);
}
private void Apply(ActionRow row, EveInputActionDocument action)
{
row.Label.text = action.Label ?? action.ActionId;
var available = string.Equals(action.Availability, "available", StringComparison.OrdinalIgnoreCase);
row.Root.SetEnabled(available);
row.Root.style.opacity = available ? 1f : 0.45f;
if (row.Scalar != null && action.InputValue?.CurrentValue is double current &&
row.Scalar.focusController?.focusedElement != row.Scalar)
{
row.Scalar.SetValueWithoutNotify(current.ToString("R", CultureInfo.InvariantCulture));
row.Scalar.tooltip = string.IsNullOrWhiteSpace(action.InputValue.Unit)
? "" : action.InputValue.Unit;
}
if (!string.IsNullOrWhiteSpace(action.IconRef) && _host?.NativeAssetProvider != null)
{
var texture = _host.NativeAssetProvider.ResolveAsset(
new EveUnityPlayableWorldAssetBinding(action.IconRef, "", "provider-asset-ref"),
typeof(Texture2D)) as Texture2D;
row.Icon.image = texture;
}
else row.Icon.image = null;
}
private void ReleaseHold(string actionId)
{
if (!_pressedHolds.Remove(actionId)) return;
SubmitHold(actionId, false);
}
private EveUnityPlayableWorldClientHost RequireHost() =>
_host ?? throw new InvalidOperationException("The generic input action bar is not bound to a playable-world host.");
private static string RequireEntityId(EveUnityPlayableWorldClientHost host) =>
host.ActiveWorld?.PlayerEntityId ?? throw new InvalidOperationException("The playable world has no active player entity.");
private void EnsureBuilt()
{
if (_root != null)
{
AttachToDocument();
return;
}
_document = GetComponent<UIDocument>();
if (_document == null)
{
var panel = ScriptableObject.CreateInstance<PanelSettings>();
panel.name = "Eve playable-world HUD panel";
panel.scaleMode = PanelScaleMode.ScaleWithScreenSize;
panel.referenceResolution = new Vector2Int(1920, 1080);
panel.match = 0.5f;
_document = gameObject.AddComponent<UIDocument>();
_document.panelSettings = panel;
_document.sortingOrder = 100;
}
_root = new VisualElement { name = "eve-input-action-bar" };
_root.AddToClassList("eve-input-action-bar");
_root.style.position = Position.Absolute;
_root.style.left = Length.Percent(50);
_root.style.bottom = 18;
_root.style.translate = new Translate(Length.Percent(-50), 0);
_root.style.flexDirection = FlexDirection.Row;
_root.style.alignItems = Align.FlexEnd;
AttachToDocument();
}
private void AttachToDocument()
{
var documentRoot = _document?.rootVisualElement;
if (documentRoot != null && _root != null && _root.parent == null)
documentRoot.Add(_root);
}
private static string SafeName(string value) =>
new string((value ?? "").Select(character => char.IsLetterOrDigit(character) ? character : '-').ToArray());
private sealed class ActionRow
{
public ActionRow(VisualElement root, Image icon, Label label, Button? trigger, TextField? scalar)
{
Root = root;
Icon = icon;
Label = label;
Trigger = trigger;
Scalar = scalar;
}
public VisualElement Root { get; }
public Image Icon { get; }
public Label Label { get; }
public Button? Trigger { get; }
public TextField? Scalar { get; }
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bb62601554a846f39e46a6969c10eff7

View file

@ -118,6 +118,9 @@ namespace GameCult.Eve.UnityScene
var fieldParticles = GetComponent<EveUnityFieldsParticleRenderer>();
if (fieldParticles == null) fieldParticles = gameObject.AddComponent<EveUnityFieldsParticleRenderer>();
fieldParticles.Bind(this, providerSurfaceDocuments as IEveUnityFieldsSplatsDocumentSource);
var actionBar = GetComponent<EveUnityInputActionBar>();
if (actionBar == null) actionBar = gameObject.AddComponent<EveUnityInputActionBar>();
actionBar.Bind(this);
var presentation = Runtime.Connect();
ConnectionEpoch++;
@ -220,7 +223,10 @@ namespace GameCult.Eve.UnityScene
float inputValue,
DateTimeOffset? issuedAt = null)
{
var action = EveUnityAdvertisedInputAction.Resolve(InputCapability, actionId);
var action = EveUnityAdvertisedInputAction.Resolve(
InputCapability,
actionId,
requireAvailable: inputValue != 0f);
return RequireRuntime().SubmitCommandIntent(
action.Operation,
action.BuildPayload(entityId, inputValue),
@ -242,6 +248,19 @@ namespace GameCult.Eve.UnityScene
issuedAt);
}
public EveSurfaceCommandRequest SubmitAdvertisedActionScalarIntent(
string entityId,
string actionId,
double value,
DateTimeOffset? issuedAt = null)
{
var action = EveUnityAdvertisedInputAction.Resolve(InputCapability, actionId);
return RequireRuntime().SubmitCommandIntent(
action.Operation,
action.BuildScalarPayload(entityId, value),
issuedAt);
}
public void Disconnect()
{
if (_cameraOwner is EveUnityPlayableWorldCameraRig rig)

View file

@ -30,14 +30,18 @@ namespace GameCult.Eve.UnityScene
AttachToDocument();
return;
}
var panel = ScriptableObject.CreateInstance<PanelSettings>();
panel.name = "Eve thermal HUD panel";
panel.scaleMode = PanelScaleMode.ScaleWithScreenSize;
panel.referenceResolution = new Vector2Int(1920, 1080);
panel.match = 0.5f;
_document = gameObject.AddComponent<UIDocument>();
_document.panelSettings = panel;
_document.sortingOrder = 100;
_document = GetComponent<UIDocument>();
if (_document == null)
{
var panel = ScriptableObject.CreateInstance<PanelSettings>();
panel.name = "Eve playable-world HUD panel";
panel.scaleMode = PanelScaleMode.ScaleWithScreenSize;
panel.referenceResolution = new Vector2Int(1920, 1080);
panel.match = 0.5f;
_document = gameObject.AddComponent<UIDocument>();
_document.panelSettings = panel;
_document.sortingOrder = 100;
}
_root = new VisualElement();
Build(_root);
AttachToDocument();

View file

@ -65,6 +65,34 @@ namespace GameCult.Eve.UnityScene.Tests
Assert.Throws<InvalidOperationException>(() => action.BuildPayload("entity.4", 1f));
}
[Test]
public void UnavailableActionCanOnlyBeResolvedForReleaseCleanup()
{
var capability = new EveInputCapabilityDocument
{
Actions = new[]
{
new EveInputActionDocument
{
ActionId = "pilot.scoop",
Operation = "provider.commands.SetPower",
Availability = "unavailable",
InputValue = new EveInputValueDocument
{
Model = EveUnityAdvertisedInputAction.ButtonHoldValueModel,
PayloadKey = "active"
}
}
}
};
Assert.Throws<InvalidOperationException>(() =>
EveUnityAdvertisedInputAction.Resolve(capability, "pilot.scoop"));
Assert.DoesNotThrow(() =>
EveUnityAdvertisedInputAction.Resolve(capability, "pilot.scoop", requireAvailable: false)
.BuildPayload("entity.4", 0));
}
[Test]
public void ViewDirectionUsesAdvertisedKeysAndNormalizesWithoutMutatingCapability()
{
@ -145,5 +173,50 @@ namespace GameCult.Eve.UnityScene.Tests
}, "pilot.target-reticle");
Assert.Throws<ArgumentOutOfRangeException>(() => action.BuildViewDirectionPayload("entity.4", 0f, 0f, 0f));
}
[Test]
public void ScalarUsesAdvertisedStateAndRangeWithoutMutatingCapability()
{
var sourcePayload = new Dictionary<string, string>(StringComparer.Ordinal)
{
["scalarValue"] = "300",
["equipmentIndex"] = "2"
};
var action = EveUnityAdvertisedInputAction.Resolve(new EveInputCapabilityDocument
{
Actions = new[]
{
new EveInputActionDocument
{
ActionId = "equipment.2.temperature",
Operation = "provider.commands.SetTemperature",
Availability = "available",
Payload = sourcePayload,
InputValue = new EveInputValueDocument
{
Model = EveUnityAdvertisedInputAction.ScalarValueModel,
PayloadKey = "scalarValue",
CurrentValue = 300,
MinimumValue = 100,
MaximumValue = 600,
StepValue = 5,
Unit = "kelvin"
}
}
}
}, "equipment.2.temperature");
var payload = action.BuildScalarPayload("entity.4", 425.5);
Assert.That(action.IsScalar, Is.True);
Assert.That(action.CurrentValue, Is.EqualTo(300));
Assert.That(action.Unit, Is.EqualTo("kelvin"));
Assert.That(payload["scalarValue"], Is.EqualTo("425.5"));
Assert.That(payload["equipmentIndex"], Is.EqualTo("2"));
Assert.That(sourcePayload["scalarValue"], Is.EqualTo("300"));
Assert.Throws<ArgumentOutOfRangeException>(() => action.BuildScalarPayload("entity.4", 99));
Assert.Throws<ArgumentOutOfRangeException>(() => action.BuildScalarPayload("entity.4", 601));
Assert.Throws<ArgumentOutOfRangeException>(() => action.BuildScalarPayload("entity.4", double.NaN));
}
}
}

View file

@ -1216,6 +1216,121 @@ namespace GameCult.Eve.UnityScene.Tests
}
}
[Test]
public void GenericActionBarSelectsSuggestionsAndSubmitsScalarThroughAdvertisedOperation()
{
var rootObject = new GameObject("generic-eve-world-root");
var hostObject = new GameObject("generic-eve-client");
hostObject.SetActive(false);
try
{
var capability = new EveInputCapabilityDocument
{
ProviderId = "generic-provider",
CapabilityId = "pilot.input",
Actions = new[]
{
new EveInputActionDocument
{
ActionId = "equipment.0.temperature",
Label = "Target Temperature",
Operation = "generic.commands.SetTemperature",
Availability = "available",
ActionBar = true,
IconRef = "item.thermostat.icon",
Payload = new Dictionary<string, string>(StringComparer.Ordinal)
{
["equipmentIndex"] = "0",
["behaviorIndex"] = "3"
},
InputValue = new EveInputValueDocument
{
Model = EveUnityAdvertisedInputAction.ScalarValueModel,
PayloadKey = "scalarValue",
CurrentValue = 300,
Unit = "kelvin"
}
},
new EveInputActionDocument
{
ActionId = "pilot.interact",
Label = "Interact",
Operation = "generic.commands.Interact",
Availability = "available"
},
new EveInputActionDocument
{
ActionId = "pilot.hidden",
Label = "Hidden",
Operation = "generic.commands.Hidden",
Availability = "available"
}
},
DefaultProfiles = new[]
{
new EveInputProfileDocument
{
ProfileId = "keyboard-mouse",
DeviceClass = "keyboard-mouse",
Bindings = new[]
{
new EveInputBindingDocument
{
BindingId = "interact.f",
ActionId = "pilot.interact",
ActionBar = true,
Gesture = new EveInputGestureDocument
{
Kind = "direct",
Controls = new[] { "keyboard.f" }
}
}
}
}
}
};
Assert.That(EveUnityInputActionBar.SelectActions(capability)
.Select(action => action.ActionId),
Is.EqualTo(new[] { "pilot.interact", "equipment.0.temperature" }));
var provider = hostObject.AddComponent<FakePlayableWorldProviderComponent>();
provider.Set(
new EveUnitySceneProviderSurfaceDocument(
PlayableArpgDocument(),
Advertisement("aetheria.daemon.game"),
"cultmesh://generic/eve/surfaces/game",
1),
new EveUnityPlayableWorldAssetManifestDocument(
"cultmesh://generic/assets/manifest",
Array.Empty<EveUnityPlayableWorldAssetManifestDocumentEntry>(),
"generic-provider"),
capability);
var host = hostObject.AddComponent<EveUnityPlayableWorldClientHost>();
host.Configure(rootObject.transform, provider, provider, provider, provider);
host.Connect();
var bar = hostObject.GetComponent<EveUnityInputActionBar>();
Assert.That(bar, Is.Not.Null);
bar!.RefreshNow();
Assert.That(bar.PresentedActionIds,
Is.EquivalentTo(new[] { "pilot.interact", "equipment.0.temperature" }));
bar.SubmitScalar("equipment.0.temperature", "425.5");
var request = provider.Submitted.Single();
Assert.That(request.Payload.GetString("commandId"), Is.EqualTo("generic.commands.SetTemperature"));
Assert.That(request.Payload.GetString("entityId"), Is.EqualTo("player-vanguard"));
Assert.That(request.Payload.GetString("actionId"), Is.EqualTo("equipment.0.temperature"));
Assert.That(request.Payload.GetString("equipmentIndex"), Is.EqualTo("0"));
Assert.That(request.Payload.GetDouble("scalarValue", 0), Is.EqualTo(425.5).Within(0.000001));
}
finally
{
UnityEngine.Object.DestroyImmediate(hostObject);
UnityEngine.Object.DestroyImmediate(rootObject);
}
}
[Test]
public void PlayableWorldCameraRigFollowsAdvertisedPlayerEntityWithoutProviderTypes()
{

View file

@ -1,13 +1,13 @@
{
"name": "org.gamecult.eve.unity-scene",
"displayName": "Eve Unity Scene Lowering",
"version": "0.3.64",
"version": "0.3.65",
"unity": "6000.0",
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
"dependencies": {
"org.gamecult.cultlib": "1.0.15",
"org.gamecult.eve.plugin-fields": "0.2.3",
"org.gamecult.eve.surface": "0.2.3",
"org.gamecult.eve.surface": "0.2.4",
"com.unity.mathematics": "1.3.2",
"com.unity.render-pipelines.universal": "17.4.0",
"com.unity.modules.particlesystem": "1.0.0"