Add Ehwrj clean-room live map
Some checks failed
build / build-test-publish (push) Has been cancelled

This commit is contained in:
2026-06-02 22:49:24 +09:00
parent c93ab38cbd
commit cba5243ce4
71 changed files with 5990 additions and 9 deletions

View File

@@ -0,0 +1,296 @@
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;
}
}