using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using GameCult.Eve.Surface; #nullable enable namespace GameCult.Eve.UnityScene { public sealed class EveUnityAdvertisedInputAction { 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; private EveUnityAdvertisedInputAction(EveInputActionDocument action) { _action = action; } public string ActionId => _action.ActionId ?? ""; public string Operation => _action.Operation ?? ""; public string Availability => _action.Availability ?? ""; public string InputValueModel => _action.InputValue?.Model ?? ""; public string InputValuePayloadKey => _action.InputValue?.PayloadKey ?? ""; public IReadOnlyList InputValuePayloadKeys => _action.InputValue?.PayloadKeys ?? Array.Empty(); 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, bool requireAvailable = true) { if (capability == null) throw new InvalidOperationException("The provider did not advertise an input capability."); var action = (capability.Actions ?? Array.Empty()).FirstOrDefault(candidate => 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 (requireAvailable && !string.Equals(action.Availability, "available", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException($"Input action '{actionId}' is not available."); return new EveUnityAdvertisedInputAction(action); } public IReadOnlyDictionary BuildPayload(string entityId, float? inputValue = null) { var payload = _action.Payload == null ? new Dictionary(StringComparer.Ordinal) : new Dictionary(_action.Payload, StringComparer.Ordinal); payload["entityId"] = entityId ?? ""; payload["actionId"] = ActionId; if (inputValue.HasValue) { if (float.IsNaN(inputValue.Value) || float.IsInfinity(inputValue.Value)) throw new ArgumentOutOfRangeException(nameof(inputValue), "Input action value must be finite."); if (string.IsNullOrWhiteSpace(InputValueModel) || string.IsNullOrWhiteSpace(InputValuePayloadKey)) throw new InvalidOperationException($"Input action '{ActionId}' does not advertise a value payload."); payload[InputValuePayloadKey] = inputValue.Value.ToString("R", CultureInfo.InvariantCulture); } return payload; } public IReadOnlyDictionary BuildViewDirectionPayload( string entityId, float directionX, float directionY, float directionZ) { if (!IsViewDirection) throw new InvalidOperationException($"Input action '{ActionId}' does not advertise a view-direction value."); if (InputValuePayloadKeys.Count != 3 || InputValuePayloadKeys.Any(string.IsNullOrWhiteSpace) || InputValuePayloadKeys.Distinct(StringComparer.Ordinal).Count() != 3) throw new InvalidOperationException($"Input action '{ActionId}' does not advertise three distinct view-direction payload keys."); if (!IsFinite(directionX) || !IsFinite(directionY) || !IsFinite(directionZ)) throw new ArgumentOutOfRangeException(nameof(directionX), "View direction must be finite."); var length = Math.Sqrt( (directionX * directionX) + (directionY * directionY) + (directionZ * directionZ)); if (length <= 0.000001) throw new ArgumentOutOfRangeException(nameof(directionX), "View direction must be non-zero."); var payload = BuildPayload(entityId).ToDictionary(value => value.Key, value => value.Value, StringComparer.Ordinal); payload[InputValuePayloadKeys[0]] = ((float)(directionX / length)).ToString("R", CultureInfo.InvariantCulture); payload[InputValuePayloadKeys[1]] = ((float)(directionY / length)).ToString("R", CultureInfo.InvariantCulture); payload[InputValuePayloadKeys[2]] = ((float)(directionZ / length)).ToString("R", CultureInfo.InvariantCulture); return payload; } public IReadOnlyDictionary 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); } }