297 lines
10 KiB
C#
297 lines
10 KiB
C#
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<MapObject> 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<JsonElement>().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;
|
|
}
|
|
}
|