Refactor Python API structure and enhance backend command handling

Major refactor of the Python API: moved modules into subdirectories, replaced app.py with api.py, and added new audio and utility modules. Backend C++ code now tracks command execution results, exposes them via the API, and improves command result handling. Also includes updates to the SRS audio handler, random string generation, and VSCode launch configurations.
This commit is contained in:
Pax1601
2025-08-07 17:01:30 +02:00
parent 4bcb5936b4
commit c66c9242b3
32 changed files with 2535 additions and 554 deletions

View File

@@ -0,0 +1,139 @@
import struct
from typing import List
from data.data_types import LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset
class DataExtractor:
def __init__(self, buffer: bytes):
self._seek_position = 0
self._buffer = buffer
self._length = len(buffer)
def set_seek_position(self, seek_position: int):
self._seek_position = seek_position
def get_seek_position(self) -> int:
return self._seek_position
def extract_bool(self) -> bool:
value = struct.unpack_from('<B', self._buffer, self._seek_position)[0]
self._seek_position += 1
return value > 0
def extract_uint8(self) -> int:
value = struct.unpack_from('<B', self._buffer, self._seek_position)[0]
self._seek_position += 1
return value
def extract_uint16(self) -> int:
value = struct.unpack_from('<H', self._buffer, self._seek_position)[0]
self._seek_position += 2
return value
def extract_uint32(self) -> int:
value = struct.unpack_from('<I', self._buffer, self._seek_position)[0]
self._seek_position += 4
return value
def extract_uint64(self) -> int:
value = struct.unpack_from('<Q', self._buffer, self._seek_position)[0]
self._seek_position += 8
return value
def extract_float64(self) -> float:
value = struct.unpack_from('<d', self._buffer, self._seek_position)[0]
self._seek_position += 8
return value
def extract_lat_lng(self) -> LatLng:
lat = self.extract_float64()
lng = self.extract_float64()
alt = self.extract_float64()
return LatLng(lat, lng, alt)
def extract_from_bitmask(self, bitmask: int, position: int) -> bool:
return ((bitmask >> position) & 1) > 0
def extract_string(self, length: int = None) -> str:
if length is None:
length = self.extract_uint16()
string_buffer = self._buffer[self._seek_position:self._seek_position + length]
# Find null terminator
string_length = length
for idx, byte_val in enumerate(string_buffer):
if byte_val == 0:
string_length = idx
break
try:
value = string_buffer[:string_length].decode('utf-8').strip()
except UnicodeDecodeError:
value = string_buffer[:string_length].decode('utf-8', errors='ignore').strip()
self._seek_position += length
return value
def extract_char(self) -> str:
return self.extract_string(1)
def extract_tacan(self) -> TACAN:
return TACAN(
is_on=self.extract_bool(),
channel=self.extract_uint8(),
xy=self.extract_char(),
callsign=self.extract_string(4)
)
def extract_radio(self) -> Radio:
return Radio(
frequency=self.extract_uint32(),
callsign=self.extract_uint8(),
callsign_number=self.extract_uint8()
)
def extract_general_settings(self) -> GeneralSettings:
return GeneralSettings(
prohibit_jettison=self.extract_bool(),
prohibit_aa=self.extract_bool(),
prohibit_ag=self.extract_bool(),
prohibit_afterburner=self.extract_bool(),
prohibit_air_wpn=self.extract_bool()
)
def extract_ammo(self) -> List[Ammo]:
value = []
size = self.extract_uint16()
for _ in range(size):
value.append(Ammo(
quantity=self.extract_uint16(),
name=self.extract_string(33),
guidance=self.extract_uint8(),
category=self.extract_uint8(),
missile_category=self.extract_uint8()
))
return value
def extract_contacts(self) -> List[Contact]:
value = []
size = self.extract_uint16()
for _ in range(size):
value.append(Contact(
id=self.extract_uint32(),
detection_method=self.extract_uint8()
))
return value
def extract_active_path(self) -> List[LatLng]:
value = []
size = self.extract_uint16()
for _ in range(size):
value.append(self.extract_lat_lng())
return value
def extract_offset(self) -> Offset:
return Offset(
x=self.extract_float64(),
y=self.extract_float64(),
z=self.extract_float64()
)

View File

@@ -0,0 +1,70 @@
from enum import Enum
class DataIndexes(Enum):
START_OF_DATA = 0
CATEGORY = 1
ALIVE = 2
ALARM_STATE = 3
RADAR_STATE = 4
HUMAN = 5
CONTROLLED = 6
COALITION = 7
COUNTRY = 8
NAME = 9
UNIT_NAME = 10
CALLSIGN = 11
UNIT_ID = 12
GROUP_ID = 13
GROUP_NAME = 14
STATE = 15
TASK = 16
HAS_TASK = 17
POSITION = 18
SPEED = 19
HORIZONTAL_VELOCITY = 20
VERTICAL_VELOCITY = 21
HEADING = 22
TRACK = 23
IS_ACTIVE_TANKER = 24
IS_ACTIVE_AWACS = 25
ON_OFF = 26
FOLLOW_ROADS = 27
FUEL = 28
DESIRED_SPEED = 29
DESIRED_SPEED_TYPE = 30
DESIRED_ALTITUDE = 31
DESIRED_ALTITUDE_TYPE = 32
LEADER_ID = 33
FORMATION_OFFSET = 34
TARGET_ID = 35
TARGET_POSITION = 36
ROE = 37
REACTION_TO_THREAT = 38
EMISSIONS_COUNTERMEASURES = 39
TACAN = 40
RADIO = 41
GENERAL_SETTINGS = 42
AMMO = 43
CONTACTS = 44
ACTIVE_PATH = 45
IS_LEADER = 46
OPERATE_AS = 47
SHOTS_SCATTER = 48
SHOTS_INTENSITY = 49
HEALTH = 50
RACETRACK_LENGTH = 51
RACETRACK_ANCHOR = 52
RACETRACK_BEARING = 53
TIME_TO_NEXT_TASKING = 54
BARREL_HEIGHT = 55
MUZZLE_VELOCITY = 56
AIM_TIME = 57
SHOTS_TO_FIRE = 58
SHOTS_BASE_INTERVAL = 59
SHOTS_BASE_SCATTER = 60
ENGAGEMENT_RANGE = 61
TARGETING_RANGE = 62
AIM_METHOD_RANGE = 63
ACQUISITION_RANGE = 64
AIRBORNE = 65
END_OF_DATA = 255

View File

@@ -0,0 +1,91 @@
from dataclasses import dataclass
from typing import List, Optional
from utils.utils import bearing_to, distance, project_with_bearing_and_distance
@dataclass
class LatLng:
lat: float
lng: float
alt: float
def toJSON(self):
"""Convert LatLng to a JSON serializable dictionary."""
return {
"lat": self.lat,
"lng": self.lng,
"alt": self.alt
}
def project_with_bearing_and_distance(self, d, bearing):
"""
Project this LatLng point with a bearing and distance.
Args:
d: Distance in meters to project.
bearing: Bearing in radians.
Returns:
A new LatLng point projected from this point.
"""
(new_lat, new_lng) = project_with_bearing_and_distance(self.lat, self.lng, d, bearing)
return LatLng(new_lat, new_lng, self.alt)
def distance_to(self, other):
"""
Calculate the distance to another LatLng point.
Args:
other: Another LatLng point.
Returns:
Distance in meters to the other point.
"""
return distance(self.lat, self.lng, other.lat, other.lng)
def bearing_to(self, other):
"""
Calculate the bearing to another LatLng point.
Args:
other: Another LatLng point.
Returns:
Bearing in radians to the other point.
"""
return bearing_to(self.lat, self.lng, other.lat, other.lng)
@dataclass
class TACAN:
is_on: bool
channel: int
xy: str
callsign: str
@dataclass
class Radio:
frequency: int
callsign: int
callsign_number: int
@dataclass
class GeneralSettings:
prohibit_jettison: bool
prohibit_aa: bool
prohibit_ag: bool
prohibit_afterburner: bool
prohibit_air_wpn: bool
@dataclass
class Ammo:
quantity: int
name: str
guidance: int
category: int
missile_category: int
@dataclass
class Contact:
id: int
detection_method: int
@dataclass
class Offset:
x: float
y: float
z: float

View File

@@ -0,0 +1 @@
ROES = ["", "free", "designated", "return", "hold"]

View File

@@ -0,0 +1,19 @@
states = [
"none",
"idle",
"reach-destination",
"attack",
"follow",
"land",
"refuel",
"AWACS",
"tanker",
"bomb-point",
"carpet-bomb",
"bomb-building",
"fire-at-area",
"simulate-fire-fight",
"scenic-aaa",
"miss-on-purpose",
"land-at-point"
]

View File

@@ -0,0 +1,30 @@
from dataclasses import dataclass
from typing import Optional
from data.data_types import LatLng
@dataclass
class UnitSpawnTable:
"""Unit spawn table data structure for spawning units."""
unit_type: str
location: LatLng
skill: str
livery_id: str
altitude: Optional[int] = None
loadout: Optional[str] = None
heading: Optional[int] = None
def toJSON(self):
"""Convert the unit spawn table to a JSON serializable dictionary."""
return {
"unitType": self.unit_type,
"location": {
"lat": self.location.lat,
"lng": self.location.lng,
"alt": self.location.alt
},
"skill": self.skill,
"liveryID": self.livery_id,
"altitude": self.altitude,
"loadout": self.loadout,
"heading": self.heading
}