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,9 @@
namespace Ehwrj.App.Models;
public sealed class AppSettings
{
public MapSettings Map { get; set; } = new();
public OverlaySettings Overlay { get; set; } = new();
public string Language { get; set; } = "en-US";
public int PollIntervalMs { get; set; } = 500;
}

View File

@@ -0,0 +1,15 @@
namespace Ehwrj.App.Models;
public enum EndpointHealthState
{
Ok,
Warning,
Error,
NotChecked
}
public sealed record EndpointHealth(
string Path,
bool Required,
EndpointHealthState State,
string Detail);

View File

@@ -0,0 +1,3 @@
namespace Ehwrj.App.Models;
public sealed record LanguageOption(string Code, string DisplayName);

View File

@@ -0,0 +1,61 @@
using Avalonia.Media.Imaging;
using Ehwrj.Core.Models;
namespace Ehwrj.App.Models;
public sealed class LiveSnapshot
{
private static readonly IReadOnlyList<EndpointHealth> EmptyEndpointHealth =
[
new("map_info.json", Required: true, EndpointHealthState.NotChecked, "not checked"),
new("map_obj.json", Required: true, EndpointHealthState.NotChecked, "not checked"),
new("map.img", Required: true, EndpointHealthState.NotChecked, "not checked"),
new("state", Required: false, EndpointHealthState.NotChecked, "not checked"),
new("hudmsg", Required: false, EndpointHealthState.NotChecked, "not checked"),
new("gamechat", Required: false, EndpointHealthState.NotChecked, "not checked")
];
public static LiveSnapshot Empty { get; } = new(
MapInfo.Empty,
Array.Empty<MapObject>(),
FlightState.Empty,
Array.Empty<BattleMessage>(),
null,
DateTimeOffset.MinValue,
false,
"Waiting for War Thunder",
EmptyEndpointHealth);
public LiveSnapshot(
MapInfo mapInfo,
IReadOnlyList<MapObject> objects,
FlightState flightState,
IReadOnlyList<BattleMessage> messages,
Bitmap? mapImage,
DateTimeOffset updatedAt,
bool isGameRunning,
string status,
IReadOnlyList<EndpointHealth>? endpointHealth = null)
{
MapInfo = mapInfo;
Objects = objects;
FlightState = flightState;
Messages = messages;
MapImage = mapImage;
UpdatedAt = updatedAt;
IsGameRunning = isGameRunning;
Status = status;
EndpointHealth = endpointHealth ?? EmptyEndpointHealth;
}
public MapInfo MapInfo { get; }
public IReadOnlyList<MapObject> Objects { get; }
public FlightState FlightState { get; }
public IReadOnlyList<BattleMessage> Messages { get; }
public Bitmap? MapImage { get; }
public DateTimeOffset UpdatedAt { get; }
public bool IsGameRunning { get; }
public string Status { get; }
public IReadOnlyList<EndpointHealth> EndpointHealth { get; }
public MapObject? Player => Objects.FirstOrDefault(static o => o.IsPlayer) ?? Objects.FirstOrDefault(static o => o.IsAircraft);
}

View File

@@ -0,0 +1,76 @@
using Ehwrj.App.ViewModels;
namespace Ehwrj.App.Models;
public sealed class MapSettings : ObservableObject
{
private bool _showLabels = true;
private bool _showAircraftMach = true;
private bool _followPlayer = true;
private bool _rotateWithPlayer;
private bool _showRangeRings = true;
private bool _showBattleLog = true;
private bool _showDeliveryTracker = true;
private double _deliveryTrackerAngle = 0.8;
public bool ShowLabels
{
get => _showLabels;
set => SetProperty(ref _showLabels, value);
}
public bool ShowAircraftMach
{
get => _showAircraftMach;
set => SetProperty(ref _showAircraftMach, value);
}
public bool FollowPlayer
{
get => _followPlayer;
set => SetProperty(ref _followPlayer, value);
}
public bool RotateWithPlayer
{
get => _rotateWithPlayer;
set => SetProperty(ref _rotateWithPlayer, value);
}
public bool ShowRangeRings
{
get => _showRangeRings;
set => SetProperty(ref _showRangeRings, value);
}
public bool ShowBattleLog
{
get => _showBattleLog;
set => SetProperty(ref _showBattleLog, value);
}
public bool ShowDeliveryTracker
{
get => _showDeliveryTracker;
set => SetProperty(ref _showDeliveryTracker, value);
}
public double DeliveryTrackerAngle
{
get => _deliveryTrackerAngle;
set => SetProperty(ref _deliveryTrackerAngle, Math.Clamp(value, 0.1, 1.0));
}
public void Reset()
{
ShowLabels = true;
ShowAircraftMach = true;
FollowPlayer = true;
RotateWithPlayer = false;
ShowRangeRings = true;
ShowBattleLog = true;
ShowDeliveryTracker = true;
DeliveryTrackerAngle = 0.8;
}
}

View File

@@ -0,0 +1,283 @@
using Ehwrj.App.ViewModels;
namespace Ehwrj.App.Models;
public sealed class OverlaySettings : ObservableObject
{
private bool _showMiniMap = true;
private bool _showMach = true;
private bool _showSpotRadar = true;
private bool _spotShowDistance = true;
private bool _spotShowMach;
private bool _spotShowRelativeSpeed = true;
private double _size = 560;
private double _top = 24;
private double _right = 24;
private double _zoomPercent = 92;
private double _miniMapMinimumMach;
private double _miniMapAircraftScale = 125;
private double _spotOpacity = 70;
private double _spotDistance = 529;
private double _spotVerticalScale = 68;
private double _spotVerticalOffset = 224;
private double _spotArrowScale = 140;
private double _spotOutlineWidth = 2;
private double _spotDetectDistanceKm = 15;
private double _spotMinimumMach = 0.5;
private double _spotFontOpacity = 70;
private double _distanceFontSize = 31;
private double _distanceOutlineWidth = 2;
private double _machFontSize = 27;
private double _machOutlineWidth = 2;
private double _relativeFontSize = 24;
private double _relativeOutlineWidth = 2;
private string _spotArrowColor = "#ff1e1e";
private string _spotTextColor = "#ffffff";
private string _distanceTextColor = "#ff1e1e";
private string _machTextColor = "#57c7f2";
private string _relativeTextColor = "#19f24f";
public bool ShowMiniMap
{
get => _showMiniMap;
set => SetProperty(ref _showMiniMap, value);
}
public bool ShowMach
{
get => _showMach;
set => SetProperty(ref _showMach, value);
}
public bool ShowSpotRadar
{
get => _showSpotRadar;
set => SetProperty(ref _showSpotRadar, value);
}
public bool SpotShowDistance
{
get => _spotShowDistance;
set => SetProperty(ref _spotShowDistance, value);
}
public bool SpotShowMach
{
get => _spotShowMach;
set => SetProperty(ref _spotShowMach, value);
}
public bool SpotShowRelativeSpeed
{
get => _spotShowRelativeSpeed;
set => SetProperty(ref _spotShowRelativeSpeed, value);
}
public double Size
{
get => _size;
set => SetProperty(ref _size, Math.Clamp(value, 160, 2000));
}
public double Top
{
get => _top;
set => SetProperty(ref _top, Math.Clamp(value, 0, 500));
}
public double Right
{
get => _right;
set => SetProperty(ref _right, Math.Clamp(value, 0, 2000));
}
public double ZoomPercent
{
get => _zoomPercent;
set => SetProperty(ref _zoomPercent, Math.Clamp(value, 25, 400));
}
public double MiniMapMinimumMach
{
get => _miniMapMinimumMach;
set => SetProperty(ref _miniMapMinimumMach, Math.Clamp(value, 0, 2));
}
public double MiniMapAircraftScale
{
get => _miniMapAircraftScale;
set => SetProperty(ref _miniMapAircraftScale, Math.Clamp(value, 50, 200));
}
public double SpotOpacity
{
get => _spotOpacity;
set => SetProperty(ref _spotOpacity, Math.Clamp(value, 0, 100));
}
public double SpotDistance
{
get => _spotDistance;
set => SetProperty(ref _spotDistance, Math.Clamp(value, 40, 600));
}
public double SpotVerticalScale
{
get => _spotVerticalScale;
set => SetProperty(ref _spotVerticalScale, Math.Clamp(value, 40, 140));
}
public double SpotVerticalOffset
{
get => _spotVerticalOffset;
set => SetProperty(ref _spotVerticalOffset, Math.Clamp(value, -500, 500));
}
public double SpotArrowScale
{
get => _spotArrowScale;
set => SetProperty(ref _spotArrowScale, Math.Clamp(value, 50, 200));
}
public double SpotOutlineWidth
{
get => _spotOutlineWidth;
set => SetProperty(ref _spotOutlineWidth, Math.Clamp(value, 0, 10));
}
public double SpotDetectDistanceKm
{
get => _spotDetectDistanceKm;
set => SetProperty(ref _spotDetectDistanceKm, Math.Clamp(value, 0, 30));
}
public double SpotMinimumMach
{
get => _spotMinimumMach;
set => SetProperty(ref _spotMinimumMach, Math.Clamp(value, 0, 2));
}
public double SpotFontOpacity
{
get => _spotFontOpacity;
set => SetProperty(ref _spotFontOpacity, Math.Clamp(value, 0, 100));
}
public double DistanceFontSize
{
get => _distanceFontSize;
set => SetProperty(ref _distanceFontSize, Math.Clamp(value, 10, 60));
}
public double DistanceOutlineWidth
{
get => _distanceOutlineWidth;
set => SetProperty(ref _distanceOutlineWidth, Math.Clamp(value, 0, 8));
}
public double MachFontSize
{
get => _machFontSize;
set => SetProperty(ref _machFontSize, Math.Clamp(value, 10, 60));
}
public double MachOutlineWidth
{
get => _machOutlineWidth;
set => SetProperty(ref _machOutlineWidth, Math.Clamp(value, 0, 8));
}
public double RelativeFontSize
{
get => _relativeFontSize;
set => SetProperty(ref _relativeFontSize, Math.Clamp(value, 10, 60));
}
public double RelativeOutlineWidth
{
get => _relativeOutlineWidth;
set => SetProperty(ref _relativeOutlineWidth, Math.Clamp(value, 0, 8));
}
public string SpotArrowColor
{
get => _spotArrowColor;
set => SetProperty(ref _spotArrowColor, NormalizeColor(value, "#ff1e1e"));
}
public string SpotTextColor
{
get => _spotTextColor;
set => SetProperty(ref _spotTextColor, NormalizeColor(value, "#ffffff"));
}
public string DistanceTextColor
{
get => _distanceTextColor;
set => SetProperty(ref _distanceTextColor, NormalizeColor(value, "#ff1e1e"));
}
public string MachTextColor
{
get => _machTextColor;
set => SetProperty(ref _machTextColor, NormalizeColor(value, "#57c7f2"));
}
public string RelativeTextColor
{
get => _relativeTextColor;
set => SetProperty(ref _relativeTextColor, NormalizeColor(value, "#19f24f"));
}
public void Reset()
{
ShowMiniMap = true;
ShowMach = true;
ShowSpotRadar = true;
SpotShowDistance = true;
SpotShowMach = false;
SpotShowRelativeSpeed = true;
Size = 560;
Top = 24;
Right = 24;
ZoomPercent = 92;
MiniMapMinimumMach = 0;
MiniMapAircraftScale = 125;
SpotOpacity = 70;
SpotDistance = 529;
SpotVerticalScale = 68;
SpotVerticalOffset = 224;
SpotArrowScale = 140;
SpotOutlineWidth = 2;
SpotDetectDistanceKm = 15;
SpotMinimumMach = 0.5;
SpotFontOpacity = 70;
DistanceFontSize = 31;
DistanceOutlineWidth = 2;
MachFontSize = 27;
MachOutlineWidth = 2;
RelativeFontSize = 24;
RelativeOutlineWidth = 2;
SpotArrowColor = "#ff1e1e";
SpotTextColor = "#ffffff";
DistanceTextColor = "#ff1e1e";
MachTextColor = "#57c7f2";
RelativeTextColor = "#19f24f";
}
private static string NormalizeColor(string? value, string fallback)
{
if (string.IsNullOrWhiteSpace(value))
{
return fallback;
}
var trimmed = value.Trim();
if (!trimmed.StartsWith('#'))
{
trimmed = $"#{trimmed}";
}
return trimmed.Length is 7 or 9 ? trimmed : fallback;
}
}

View File

@@ -0,0 +1,311 @@
namespace Ehwrj.App.Models;
public sealed class UiText
{
private static readonly UiText English = new("en-US")
{
Subtitle = "War Thunder local map companion",
Language = "Language",
Start = "Start",
Stop = "Stop",
Connection = "Connection",
Diagnostics = "Diagnostics",
RequiredEndpoint = "required",
OptionalEndpoint = "optional",
EndpointOk = "ok",
EndpointWarning = "warning",
EndpointError = "error",
EndpointNotChecked = "not checked",
Map = "Map",
ShowLabels = "Show labels",
ShowAircraftMach = "Show aircraft Mach",
FollowPlayer = "Follow player",
RotateWithPlayer = "Rotate with player",
ShowRangeRings = "Show range rings",
ShowBattleLog = "Show battle log",
DeliveryTracker = "Delivery tracker",
DeliveryTrackerAngle = "Delivery tracker angle",
ShowOverlay = "Show overlay",
ShowMinimap = "Show minimap",
ShowMachLabels = "Show Mach labels",
ShowSpotRadar = "Show spot radar",
OverlaySize = "Overlay size",
TopOffset = "Top offset",
RightOffset = "Right offset",
Zoom = "Zoom",
Minimap = "Minimap",
AircraftScale = "Aircraft scale",
MinimumMach = "Minimum Mach",
SpotRadar = "Spot radar",
ShowDistance = "Show distance",
ShowMach = "Show Mach",
ShowClosureSpeed = "Show closure speed",
RadarRangeKm = "Radar range, km",
RadarSpread = "Radar spread",
MarkerOpacity = "Marker opacity",
FontOpacity = "Font opacity",
VerticalScale = "Vertical scale",
VerticalOffset = "Vertical offset",
ArrowScale = "Arrow scale",
ArrowOutline = "Arrow outline",
DistanceFontSize = "Distance font size",
MachFontSize = "Mach font size",
ClosureFontSize = "Closure font size",
ArrowColor = "Arrow color",
DistanceColor = "Distance color",
MachColor = "Mach color",
ClosureColor = "Closure color",
Save = "Save",
Reset = "Reset",
LiveMap = "Live Map",
LocalApiScope = "Reads only the local game API at 127.0.0.1:8111",
BattleLog = "Battle Log",
WaitingForWarThunder = "Waiting for War Thunder",
ConnectedToLocalApi = "Connected to 127.0.0.1:8111",
WaitingForLocalMapApi = "Waiting for local map API",
WarThunderProcessNotDetected = "War Thunder process not detected",
StartWarThunderToEnableLiveData = "Start War Thunder to enable live data",
WaitingForMapImage = "Waiting for map.img",
AcesDetected = "aces.exe detected",
AcesNotDetected = "aces.exe not detected",
NeverUpdated = "Never updated",
PlayerNotDetected = "Player not detected",
Player = "Player",
Allied = "allied",
Enemy = "enemy",
Objects = "objects",
Pos = "pos",
Climb = "climb",
Heading = "hdg",
Updated = "Updated",
MessageEnemy = "Enemy",
MessageAlly = "Ally",
MessageEvent = "Event",
SessionStarted = "Session started",
AircraftVisible = "aircraft visible",
AlliedAircraftChanged = "Allied aircraft count changed",
EnemyAircraftChanged = "Enemy aircraft count changed"
};
private static readonly UiText Korean = new("ko-KR")
{
Subtitle = "War Thunder 로컬 맵 도구",
Language = "언어",
Start = "시작",
Stop = "정지",
Connection = "연결",
Diagnostics = "진단",
RequiredEndpoint = "필수",
OptionalEndpoint = "선택",
EndpointOk = "정상",
EndpointWarning = "경고",
EndpointError = "오류",
EndpointNotChecked = "미확인",
Map = "지도",
ShowLabels = "라벨 표시",
ShowAircraftMach = "항공기 마하 표시",
FollowPlayer = "플레이어 따라가기",
RotateWithPlayer = "플레이어 방향 회전",
ShowRangeRings = "거리 링 표시",
ShowBattleLog = "전투 로그 표시",
DeliveryTracker = "투하 추적기",
DeliveryTrackerAngle = "투하 추적 각도",
ShowOverlay = "오버레이 표시",
ShowMinimap = "미니맵 표시",
ShowMachLabels = "마하 라벨 표시",
ShowSpotRadar = "스팟 레이더 표시",
OverlaySize = "오버레이 크기",
TopOffset = "위쪽 오프셋",
RightOffset = "오른쪽 오프셋",
Zoom = "확대",
Minimap = "미니맵",
AircraftScale = "항공기 크기",
MinimumMach = "최소 마하",
SpotRadar = "스팟 레이더",
ShowDistance = "거리 표시",
ShowMach = "마하 표시",
ShowClosureSpeed = "접근 속도 표시",
RadarRangeKm = "레이더 범위, km",
RadarSpread = "레이더 간격",
MarkerOpacity = "마커 불투명도",
FontOpacity = "글자 불투명도",
VerticalScale = "세로 배율",
VerticalOffset = "세로 오프셋",
ArrowScale = "화살표 크기",
ArrowOutline = "화살표 외곽선",
DistanceFontSize = "거리 글자 크기",
MachFontSize = "마하 글자 크기",
ClosureFontSize = "접근 속도 글자 크기",
ArrowColor = "화살표 색상",
DistanceColor = "거리 색상",
MachColor = "마하 색상",
ClosureColor = "접근 속도 색상",
Save = "저장",
Reset = "초기화",
LiveMap = "라이브 맵",
LocalApiScope = "로컬 게임 API 127.0.0.1:8111만 읽음",
BattleLog = "전투 로그",
WaitingForWarThunder = "War Thunder 대기 중",
ConnectedToLocalApi = "127.0.0.1:8111 연결됨",
WaitingForLocalMapApi = "로컬 맵 API 대기 중",
WarThunderProcessNotDetected = "War Thunder 프로세스 감지 안 됨",
StartWarThunderToEnableLiveData = "실시간 데이터를 보려면 War Thunder를 시작하세요",
WaitingForMapImage = "map.img 대기 중",
AcesDetected = "aces.exe 감지됨",
AcesNotDetected = "aces.exe 감지 안 됨",
NeverUpdated = "아직 갱신 안 됨",
PlayerNotDetected = "플레이어 감지 안 됨",
Player = "플레이어",
Allied = "아군",
Enemy = "적",
Objects = "개 오브젝트",
Pos = "좌표",
Climb = "상승각",
Heading = "방위",
Updated = "갱신",
MessageEnemy = "적",
MessageAlly = "아군",
MessageEvent = "이벤트",
SessionStarted = "세션 시작",
AircraftVisible = "항공기 표시 중",
AlliedAircraftChanged = "아군 항공기 수 변경",
EnemyAircraftChanged = "적 항공기 수 변경"
};
private UiText(string code)
{
Code = code;
}
public string Code { get; }
public string Subtitle { get; init; } = "";
public string Language { get; init; } = "";
public string Start { get; init; } = "";
public string Stop { get; init; } = "";
public string Connection { get; init; } = "";
public string Diagnostics { get; init; } = "";
public string RequiredEndpoint { get; init; } = "";
public string OptionalEndpoint { get; init; } = "";
public string EndpointOk { get; init; } = "";
public string EndpointWarning { get; init; } = "";
public string EndpointError { get; init; } = "";
public string EndpointNotChecked { get; init; } = "";
public string Map { get; init; } = "";
public string ShowLabels { get; init; } = "";
public string ShowAircraftMach { get; init; } = "";
public string FollowPlayer { get; init; } = "";
public string RotateWithPlayer { get; init; } = "";
public string ShowRangeRings { get; init; } = "";
public string ShowBattleLog { get; init; } = "";
public string DeliveryTracker { get; init; } = "";
public string DeliveryTrackerAngle { get; init; } = "";
public string ShowOverlay { get; init; } = "";
public string ShowMinimap { get; init; } = "";
public string ShowMachLabels { get; init; } = "";
public string ShowSpotRadar { get; init; } = "";
public string OverlaySize { get; init; } = "";
public string TopOffset { get; init; } = "";
public string RightOffset { get; init; } = "";
public string Zoom { get; init; } = "";
public string Minimap { get; init; } = "";
public string AircraftScale { get; init; } = "";
public string MinimumMach { get; init; } = "";
public string SpotRadar { get; init; } = "";
public string ShowDistance { get; init; } = "";
public string ShowMach { get; init; } = "";
public string ShowClosureSpeed { get; init; } = "";
public string RadarRangeKm { get; init; } = "";
public string RadarSpread { get; init; } = "";
public string MarkerOpacity { get; init; } = "";
public string FontOpacity { get; init; } = "";
public string VerticalScale { get; init; } = "";
public string VerticalOffset { get; init; } = "";
public string ArrowScale { get; init; } = "";
public string ArrowOutline { get; init; } = "";
public string DistanceFontSize { get; init; } = "";
public string MachFontSize { get; init; } = "";
public string ClosureFontSize { get; init; } = "";
public string ArrowColor { get; init; } = "";
public string DistanceColor { get; init; } = "";
public string MachColor { get; init; } = "";
public string ClosureColor { get; init; } = "";
public string Save { get; init; } = "";
public string Reset { get; init; } = "";
public string LiveMap { get; init; } = "";
public string LocalApiScope { get; init; } = "";
public string BattleLog { get; init; } = "";
public string WaitingForWarThunder { get; init; } = "";
public string ConnectedToLocalApi { get; init; } = "";
public string WaitingForLocalMapApi { get; init; } = "";
public string WarThunderProcessNotDetected { get; init; } = "";
public string StartWarThunderToEnableLiveData { get; init; } = "";
public string WaitingForMapImage { get; init; } = "";
public string AcesDetected { get; init; } = "";
public string AcesNotDetected { get; init; } = "";
public string NeverUpdated { get; init; } = "";
public string PlayerNotDetected { get; init; } = "";
public string Player { get; init; } = "";
public string Allied { get; init; } = "";
public string Enemy { get; init; } = "";
public string Objects { get; init; } = "";
public string Pos { get; init; } = "";
public string Climb { get; init; } = "";
public string Heading { get; init; } = "";
public string Updated { get; init; } = "";
public string MessageEnemy { get; init; } = "";
public string MessageAlly { get; init; } = "";
public string MessageEvent { get; init; } = "";
public string SessionStarted { get; init; } = "";
public string AircraftVisible { get; init; } = "";
public string AlliedAircraftChanged { get; init; } = "";
public string EnemyAircraftChanged { get; init; } = "";
public static IReadOnlyList<LanguageOption> LanguageOptions { get; } =
[
new("en-US", "English"),
new("ko-KR", "한국어")
];
public static UiText For(string? code)
{
return string.Equals(code, Korean.Code, StringComparison.OrdinalIgnoreCase)
? Korean
: English;
}
public string FormatAllySummary(int count) => Code == Korean.Code ? $"{Allied} {count}" : $"{count} {Allied}";
public string FormatEnemySummary(int count) => Code == Korean.Code ? $"{Enemy} {count}" : $"{count} {Enemy}";
public string FormatObjectSummary(int count) => Code == Korean.Code ? $"{count}{Objects}" : $"{count} {Objects}";
public string FormatUpdated(DateTimeOffset updatedAt) => $"{Updated} {updatedAt:HH:mm:ss}";
public string FormatSessionStarted(int ally, int enemy) => $"{SessionStarted}: {ally} {Allied}, {enemy} {Enemy} {AircraftVisible}.";
public string FormatAlliedAircraftChanged(int oldCount, int newCount) => $"{AlliedAircraftChanged}: {oldCount} -> {newCount}.";
public string FormatEnemyAircraftChanged(int oldCount, int newCount) => $"{EnemyAircraftChanged}: {oldCount} -> {newCount}.";
public string FormatEndpointHealth(EndpointHealth health)
{
var scope = health.Required ? RequiredEndpoint : OptionalEndpoint;
var state = health.State switch
{
EndpointHealthState.Ok => EndpointOk,
EndpointHealthState.Warning => EndpointWarning,
EndpointHealthState.Error => EndpointError,
EndpointHealthState.NotChecked => EndpointNotChecked,
_ => health.State.ToString()
};
return $"{health.Path} [{scope}] {state} - {health.Detail}";
}
public string FormatStatus(string status)
{
return status switch
{
"Waiting for War Thunder" => WaitingForWarThunder,
"Connected to 127.0.0.1:8111" => ConnectedToLocalApi,
"War Thunder process not detected" => WarThunderProcessNotDetected,
_ when status.StartsWith("Waiting for local map API: ", StringComparison.Ordinal) =>
$"{WaitingForLocalMapApi}: {status["Waiting for local map API: ".Length..]}",
_ => status
};
}
}