Lower generic scalar actions and action bar
This commit is contained in:
parent
7647d37445
commit
e08fa08335
11 changed files with 583 additions and 14 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bb62601554a846f39e46a6969c10eff7
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Reference in a new issue