Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion dcs/mission.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,9 @@ def loaddict(fname: str, mizfile: zipfile.ZipFile, reserved_files: List[str]) ->
# print(self.translation)

# setup terrain_
if imp_mission["theatre"] == 'Caucasus':
if imp_mission["theatre"] == 'Afghanistan':
self.terrain = terrain_.Afghanistan()
elif imp_mission["theatre"] == 'Caucasus':
self.terrain = terrain_.Caucasus()
elif imp_mission["theatre"] == 'Nevada':
self.terrain = terrain_.Nevada()
Expand Down
1 change: 1 addition & 0 deletions dcs/terrain/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dcs.terrain.terrain import ParkingSlot, Airport, Runway, RunwayApproach, Terrain
from dcs.terrain.terrain import RunwayOccupiedError, NoParkingSlotError, Graph, Node, MapView
from dcs.terrain.afghanistan import Afghanistan
from dcs.terrain.caucasus.caucasus import Caucasus
from dcs.terrain.falklands import Falklands
from dcs.terrain.germany import Germany
Expand Down
5 changes: 5 additions & 0 deletions dcs/terrain/afghanistan/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""DCS: Afghanistan terrain support."""

from .afghanistan import Afghanistan

__all__ = ["Afghanistan"]
43 changes: 43 additions & 0 deletions dcs/terrain/afghanistan/afghanistan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""DCS: Afghanistan terrain definition."""

import datetime

from dcs import mapping
from dcs.terrain import MapView, Terrain

from .airports import all_airports
from .projection import PARAMETERS


class Afghanistan(Terrain):
"""The DCS Afghanistan theatre (``Afghanistan`` in a .miz file)."""

temperature = [
(-10, 4),
(-7, 7),
(0, 14),
(6, 22),
(11, 29),
(16, 35),
(20, 37),
(18, 35),
(12, 30),
(5, 23),
(-1, 15),
(-7, 7),
]
assert len(temperature) == 12

def __init__(self) -> None:
# Covers the full map and every currently shipped Afghanistan airfield.
bounds = mapping.Rectangle(-500000, -500000, 500000, 500000, self)
super().__init__(
"Afghanistan",
PARAMETERS,
bounds=bounds,
map_view_default=MapView(bounds.center(), self, 1000000),
utc_offset=datetime.timezone(datetime.timedelta(hours=4, minutes=30)),
)
self.bullseye_blue = {"x": bounds.center().x, "y": bounds.center().y}
self.bullseye_red = {"x": bounds.center().x, "y": bounds.center().y}
self.airports = {airport.name: airport for airport in all_airports(self)}
1 change: 1 addition & 0 deletions dcs/terrain/afghanistan/airports.json

Large diffs are not rendered by default.

104 changes: 104 additions & 0 deletions dcs/terrain/afghanistan/airports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""Airport and parking data for the DCS Afghanistan terrain.

The bundled ``airports.json`` was exported from DCS 2.9.19.13478 by the
DCS Web Editor project. It contains runway, ATC, and parking-stand data for
the 25 usable airfields available in that build. Keeping the source export
alongside this loader preserves every stand instead of maintaining a large,
lossy generated Python module.
"""

from __future__ import annotations

import json
import math
from pathlib import Path
from typing import Any, Iterator

from dcs import mapping
from dcs.atcradio import AtcRadio
from dcs.terrain import Airport, ParkingSlot, Runway, RunwayApproach, Terrain


_DATA_FILE = Path(__file__).with_name("airports.json")


def _opposite_runway(designator: int) -> str:
"""Return the reciprocal two-digit runway designator."""
return f"{(designator + 17) % 36 + 1:02d}"


class AfghanistanAirport(Airport):
"""An airport hydrated from DCS's exported Afghanistan terrain data."""

unit_zones: list[mapping.Rectangle] = []
slot_version = 2

def __init__(self, data: dict[str, Any], terrain: Terrain) -> None:
self.id = int(data["ID"])
self.name = str(data["displayName"])
self.tacan = None
self.civilian = bool(data["airdromeData"].get("civilian", True))

atc_frequencies = data["airdromeData"].get("ATC", [])
self.atc_radio = (
AtcRadio(*sorted(atc_frequencies)) if len(atc_frequencies) == 4 else None
)
self.frequencies = atc_frequencies

reference_point = data["pos"]["DCS"]
super().__init__(
mapping.Point(reference_point["x"], reference_point["z"], terrain), terrain
)

for runway in data.get("runways", []):
designator = int(runway["Name"])
main = f"{designator:02d}"
opposite = _opposite_runway(designator)
# DCS reports the course counter-clockwise from north; pydcs stores
# ordinary clockwise headings.
heading = round((-math.degrees(runway["course"])) % 360)
self.runways.append(
Runway(
id=int(runway["id"]),
name=f"{main}-{opposite}",
main=RunwayApproach(name=main, heading=heading, beacons=[]),
opposite=RunwayApproach(
name=opposite, heading=(heading + 180) % 360, beacons=[]
),
)
)

for stand in data.get("stands", []):
params = stand["params"]
flags = int(stand["flag"])
height = params["HEIGHT"]
self.parking_slots.append(
ParkingSlot(
crossroad_idx=int(stand["crossroad_index"]),
position=mapping.Point(stand["x"], stand["y"], terrain),
large=bool(flags & (1 << 3)),
heli=params["FOR_HELICOPTERS"] == "1",
airplanes=params["FOR_AIRPLANES"] == "1",
slot_name=stand["name"],
length=float(params["LENGTH"]),
width=float(params["WIDTH"]),
height=float(height) if height else None,
shelter=params["SHELTER"] == "1",
)
)


def all_airports(terrain: Terrain) -> Iterator[AfghanistanAirport]:
"""Construct each usable DCS Afghanistan airfield exactly once.

The export includes a duplicate FOB Salerno record and three editor-only
FOB placeholders with no position, runway, or stands. They cannot be
represented as pydcs airports, so deliberately exclude them.
"""
seen_ids: set[int] = set()
for data in json.loads(_DATA_FILE.read_text(encoding="utf-8")):
airport_id = int(data["ID"])
if airport_id in seen_ids or not data.get("stands"):
continue
seen_ids.add(airport_id)
yield AfghanistanAirport(data, terrain)
10 changes: 10 additions & 0 deletions dcs/terrain/afghanistan/projection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# DO NOT EDIT:
# This projection was calibrated from DCS Afghanistan coordinates.
from dcs.terrain.projections import TransverseMercator

PARAMETERS = TransverseMercator(
central_meridian=63,
false_easting=-300150,
false_northing=-3759657,
scale_factor=0.9996,
)
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
'dcs/lua',
'dcs/scripts',
'dcs/terrain',
'dcs/terrain/afghanistan',
'dcs/terrain/caucasus',
'dcs/terrain/falklands',
'dcs/terrain/germany',
Expand All @@ -57,7 +58,7 @@
'dcs/terrain/thechannel',
],
package_data={
'dcs': ['py.typed'],
'dcs': ['py.typed', 'terrain/afghanistan/airports.json'],
'dcs/terrain': ['caucasus.p', 'nevada.p'],
},
entry_points={
Expand Down
27 changes: 26 additions & 1 deletion tests/test_terrain.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,33 @@
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory

import dcs


class AfghanistanTest(unittest.TestCase):
def test_airports_and_parking_stands(self):
terrain = dcs.terrain.Afghanistan()

self.assertEqual(len(terrain.airports), 25)
self.assertEqual(len(terrain.airports["Bagram"].parking_slots), 187)
self.assertEqual(len(terrain.airports["Kandahar"].parking_slots), 316)

def test_mission_uses_the_afghanistan_theatre_name(self):
mission = dcs.mission.Mission(terrain=dcs.terrain.Afghanistan())

self.assertEqual(mission.terrain.name, "Afghanistan")

def test_saved_afghanistan_mission_loads_its_terrain(self):
with TemporaryDirectory() as directory:
path = Path(directory) / "afghanistan.miz"
dcs.mission.Mission(dcs.terrain.Afghanistan()).save(path)

loaded = dcs.mission.Mission()
self.assertEqual(loaded.load_file(path), [])
self.assertIsInstance(loaded.terrain, dcs.terrain.Afghanistan)


class CaucasusTest(unittest.TestCase):
def test_parking_slots(self):
m = dcs.mission.Mission(terrain=dcs.terrain.Caucasus())
Expand Down Expand Up @@ -168,4 +194,3 @@ def test_airplane_parking_used(self):

hslots = m.terrain.airports["Aleppo"].free_parking_slots(dcs.helicopters.UH_1H)
self.assertEqual(len(hslots), 8)

2 changes: 2 additions & 0 deletions tools/export_map_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from typing import Any, Dict

from dcs import Mission
from dcs.terrain.afghanistan import Afghanistan
from dcs.action import DoScriptFile
from dcs.terrain.caucasus import Caucasus
from dcs.terrain.falklands import Falklands
Expand All @@ -62,6 +63,7 @@


ARG_TO_TERRAIN_MAP = {
"afghanistan": Afghanistan(),
"caucasus": Caucasus(),
"falklands": Falklands(),
"nevada": Nevada(),
Expand Down