mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
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:
139
scripts/python/API/data/data_extractor.py
Normal file
139
scripts/python/API/data/data_extractor.py
Normal 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()
|
||||
)
|
||||
70
scripts/python/API/data/data_indexes.py
Normal file
70
scripts/python/API/data/data_indexes.py
Normal 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
|
||||
91
scripts/python/API/data/data_types.py
Normal file
91
scripts/python/API/data/data_types.py
Normal 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
|
||||
1
scripts/python/API/data/roes.py
Normal file
1
scripts/python/API/data/roes.py
Normal file
@@ -0,0 +1 @@
|
||||
ROES = ["", "free", "designated", "return", "hold"]
|
||||
19
scripts/python/API/data/states.py
Normal file
19
scripts/python/API/data/states.py
Normal 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"
|
||||
]
|
||||
30
scripts/python/API/data/unit_spawn_table.py
Normal file
30
scripts/python/API/data/unit_spawn_table.py
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user