using System.Globalization; using System.Text.Json; namespace Ehwrj.Core.Models; public sealed class MapObject { public required string Id { get; init; } public string? Name { get; init; } public string? Type { get; init; } public string? Icon { get; init; } public string? Team { get; init; } public string? Color { get; init; } public double? X { get; init; } public double? Y { get; init; } public double? DirectionX { get; init; } public double? DirectionY { get; init; } public double? HeadingRadians { get; init; } public double? Mach { get; init; } public double? ClosureSpeed { get; init; } public double? ClimbAngleDegrees { get; init; } public MapObjectKind Kind { get; init; } public bool IsPlayer { get; init; } public bool IsAircraft { get; init; } public bool IsObjective { get; init; } public bool IsEnemyBombingPoint { get; init; } public string RawJson { get; init; } = "{}"; public static IReadOnlyList FromJson(string json, ObjectTracker tracker, FlightState? flightState = null) { using var document = JsonDocument.Parse(json); var root = document.RootElement; var source = root.ValueKind == JsonValueKind.Array ? root.EnumerateArray() : root.ValueKind == JsonValueKind.Object && root.TryGetProperty("objects", out var objects) && objects.ValueKind == JsonValueKind.Array ? objects.EnumerateArray() : Array.Empty().AsEnumerable(); return source.Select((item, index) => FromElement(item, index, tracker, flightState ?? FlightState.Empty)).ToArray(); } private static MapObject FromElement(JsonElement element, int index, ObjectTracker tracker, FlightState flightState) { var name = ReadString(element, "name", "title", "label"); var type = ReadString(element, "type", "object_type"); var icon = ReadString(element, "icon", "icon_type"); var team = ReadString(element, "team", "army", "side"); var color = ReadString(element, "color", "colour"); var id = ReadString(element, "id", "uid") ?? StableId(name, type, icon, index); var x = ReadNumber(element, "x", "sx", "lon"); var y = ReadNumber(element, "y", "sy", "lat"); ReadVector(element, ref x, ref y, "pos", "position", "coord", "coords", "location"); var dx = ReadNumber(element, "dx", "dir_x", "vx"); var dy = ReadNumber(element, "dy", "dir_y", "vy"); ReadVector(element, ref dx, ref dy, "dir", "direction", "velocity", "vector"); var heading = NormalizeHeading(ReadNumber(element, "angle", "rotation", "dir", "heading")); var classifier = $"{name} {type} {icon} {team}".ToLowerInvariant(); var kind = Classify(classifier, color); var isPlayer = kind == MapObjectKind.Player; var isAircraft = classifier.Contains("air", StringComparison.Ordinal) || classifier.Contains("plane", StringComparison.Ordinal) || classifier.Contains("fighter", StringComparison.Ordinal) || classifier.Contains("bomber", StringComparison.Ordinal) || classifier.Contains("attacker", StringComparison.Ordinal) || classifier.Contains("interceptor", StringComparison.Ordinal) || classifier.Contains("player", StringComparison.Ordinal); var isEnemyBombingPoint = classifier.Contains("bombing_point", StringComparison.Ordinal) && kind == MapObjectKind.Enemy; var isObjective = kind == MapObjectKind.Objective || isEnemyBombingPoint || classifier.Contains("capture", StringComparison.Ordinal) || classifier.Contains("zone", StringComparison.Ordinal) || classifier.Contains("objective", StringComparison.Ordinal); var motion = tracker.Update(id, x, y); var mach = isPlayer && flightState.Mach.HasValue ? flightState.Mach : motion.Mach; var climbAngle = isPlayer ? flightState.ClimbAngleDegrees : null; return new MapObject { Id = id, Name = name, Type = type, Icon = icon, Team = team, Color = color, X = x, Y = y, DirectionX = dx, DirectionY = dy, HeadingRadians = heading, Mach = mach, ClosureSpeed = motion.ClosureSpeed, ClimbAngleDegrees = climbAngle, Kind = kind, IsPlayer = isPlayer, IsAircraft = isAircraft, IsObjective = isObjective, IsEnemyBombingPoint = isEnemyBombingPoint, RawJson = element.GetRawText() }; } private static MapObjectKind Classify(string classifier, string? color) { if (classifier.Contains("player", StringComparison.Ordinal) || classifier.Contains("own", StringComparison.Ordinal) || classifier.Contains("self", StringComparison.Ordinal)) { return MapObjectKind.Player; } if (classifier.Contains("squad", StringComparison.Ordinal)) { return MapObjectKind.Squad; } if (classifier.Contains("enemy", StringComparison.Ordinal) || classifier.Contains("hostile", StringComparison.Ordinal) || classifier.Contains("bombing_point", StringComparison.Ordinal)) { return MapObjectKind.Enemy; } if (classifier.Contains("ally", StringComparison.Ordinal) || classifier.Contains("friendly", StringComparison.Ordinal) || classifier.Contains("defending_point", StringComparison.Ordinal)) { return MapObjectKind.Ally; } if (classifier.Contains("capture", StringComparison.Ordinal) || classifier.Contains("objective", StringComparison.Ordinal) || classifier.Contains("zone", StringComparison.Ordinal)) { return MapObjectKind.Objective; } var colorKind = ClassifyColor(color); if (colorKind != MapObjectKind.Unknown) { return colorKind; } return MapObjectKind.Unknown; } private static MapObjectKind ClassifyColor(string? color) { if (string.IsNullOrWhiteSpace(color)) { return MapObjectKind.Unknown; } var text = color.Trim(); if (text.StartsWith('#') && text.Length == 7) { var r = Convert.ToInt32(text[1..3], 16); var g = Convert.ToInt32(text[3..5], 16); var b = Convert.ToInt32(text[5..7], 16); return ClassifyRgb(r, g, b); } return MapObjectKind.Unknown; } private static MapObjectKind ClassifyRgb(int r, int g, int b) { if (r > 210 && g > 210 && b > 210) { return MapObjectKind.Player; } if (r > 160 && r > g * 1.25 && r > b * 1.25) { return MapObjectKind.Enemy; } if (g > 120 && b > 120 && r < 120) { return MapObjectKind.Ally; } if (r > 185 && g > 145 && b < 130) { return MapObjectKind.Player; } return MapObjectKind.Unknown; } private static double? NormalizeHeading(double? heading) { if (!heading.HasValue || !double.IsFinite(heading.Value)) { return null; } var value = heading.Value; if (Math.Abs(value) > Math.Tau) { value *= Math.PI / 180.0; } while (value < -Math.PI) value += Math.Tau; while (value > Math.PI) value -= Math.Tau; return value; } private static string StableId(string? name, string? type, string? icon, int index) { return $"{name ?? "object"}:{type ?? "unknown"}:{icon ?? "none"}:{index}"; } private static string? ReadString(JsonElement element, params string[] names) { foreach (var name in names) { if (!element.TryGetProperty(name, out var value)) continue; if (value.ValueKind == JsonValueKind.String) return value.GetString(); if (value.ValueKind is JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False) return value.ToString(); } return null; } private static double? ReadNumber(JsonElement element, params string[] names) { foreach (var name in names) { if (!element.TryGetProperty(name, out var value)) continue; if (value.ValueKind == JsonValueKind.Number && value.TryGetDouble(out var number)) return number; if (value.ValueKind == JsonValueKind.String) { var parsed = ParseNumber(value.GetString()); if (parsed.HasValue) return parsed; } } return null; } private static void ReadVector(JsonElement element, ref double? x, ref double? y, params string[] names) { if (x.HasValue && y.HasValue) { return; } foreach (var name in names) { if (!element.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.Array || value.GetArrayLength() < 2) { continue; } x ??= ReadNumberValue(value[0]); y ??= ReadNumberValue(value[1]); return; } } private static double? ReadNumberValue(JsonElement value) { return value.ValueKind switch { JsonValueKind.Number when value.TryGetDouble(out var number) => number, JsonValueKind.String => ParseNumber(value.GetString()), _ => null }; } private static double? ParseNumber(string? text) { if (string.IsNullOrWhiteSpace(text)) { return null; } if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)) { return value; } var normalized = text.Replace(',', '.'); return double.TryParse(normalized, NumberStyles.Float, CultureInfo.InvariantCulture, out value) ? value : null; } }