Add beacon list importer.

This commit is contained in:
Dan Albert 2020-08-31 14:21:00 -07:00
parent af596c58c3
commit d051859371
3 changed files with 257 additions and 8 deletions

View File

@ -637,11 +637,3 @@ AIRFIELD_DATA = {
atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)),
),
}
# TODO: Add list of all beacons on the map so we can reserve those frequencies.
# This list could be generated from the beasons.lua file in the terrain mod
# directory. As-is, we're allocating channels that might include VOR beacons,
# and those will broadcast their callsign consistently (probably with a lot of
# static, depending on how far away the beacon is. The F-16's VHF radio starts
# at 116 MHz, which happens to be the Damascus VOR beacon, so this is more or
# less guaranteed to happen.

74
gen/beacons.py Normal file
View File

@ -0,0 +1,74 @@
from dataclasses import dataclass
from enum import auto, IntEnum
import json
from pathlib import Path
from typing import Iterable, Optional
from gen.radios import RadioFrequency
from gen.tacan import TacanBand, TacanChannel
BEACONS_RESOURCE_PATH = Path("resources/dcs/beacons")
class BeaconType(IntEnum):
BEACON_TYPE_NULL = auto()
BEACON_TYPE_VOR = auto()
BEACON_TYPE_DME = auto()
BEACON_TYPE_VOR_DME = auto()
BEACON_TYPE_TACAN = auto()
BEACON_TYPE_VORTAC = auto()
BEACON_TYPE_RSBN = auto()
BEACON_TYPE_BROADCAST_STATION = auto()
BEACON_TYPE_HOMER = auto()
BEACON_TYPE_AIRPORT_HOMER = auto()
BEACON_TYPE_AIRPORT_HOMER_WITH_MARKER = auto()
BEACON_TYPE_ILS_FAR_HOMER = auto()
BEACON_TYPE_ILS_NEAR_HOMER = auto()
BEACON_TYPE_ILS_LOCALIZER = auto()
BEACON_TYPE_ILS_GLIDESLOPE = auto()
BEACON_TYPE_PRMG_LOCALIZER = auto()
BEACON_TYPE_PRMG_GLIDESLOPE = auto()
BEACON_TYPE_ICLS_LOCALIZER = auto()
BEACON_TYPE_ICLS_GLIDESLOPE = auto()
BEACON_TYPE_NAUTICAL_HOMER = auto()
@dataclass(frozen=True)
class Beacon:
name: str
callsign: str
beacon_type: BeaconType
hertz: int
channel: Optional[int]
@property
def frequency(self) -> RadioFrequency:
return RadioFrequency(self.hertz)
@property
def is_tacan(self) -> bool:
return self.beacon_type in (
BeaconType.BEACON_TYPE_VORTAC,
BeaconType.BEACON_TYPE_TACAN,
)
@property
def tacan_channel(self) -> TacanChannel:
assert self.is_tacan
assert self.channel is not None
return TacanChannel(self.channel, TacanBand.X)
def load_beacons_for_terrain(name: str) -> Iterable[Beacon]:
beacons_file = BEACONS_RESOURCE_PATH / f"{name.lower()}.json"
if not beacons_file.exists():
raise RuntimeError(f"Beacon file {beacons_file.resolve()} is missing")
for beacon in json.loads(beacons_file.read_text()):
yield Beacon(**beacon)

View File

@ -0,0 +1,183 @@
"""Generates resources/dcs/beacons.json from the DCS installation.
DCS has a beacons.lua file for each terrain mod that includes information about
the radio beacons present on the map:
beacons = {
{
display_name = _('INCIRLIC');
beaconId = 'airfield16_0';
type = BEACON_TYPE_VORTAC;
callsign = 'DAN';
frequency = 108400000.000000;
channel = 21;
position = { 222639.437500, 73.699811, -33216.257813 };
direction = 0.000000;
positionGeo = { latitude = 37.015611, longitude = 35.448194 };
sceneObjects = {'t:124814096'};
};
...
}
"""
import argparse
from contextlib import contextmanager
import dataclasses
import gettext
import os
from pathlib import Path
import textwrap
from typing import Dict, Iterable, Union
import lupa
import game # Needed to resolve cyclic import, for some reason.
from gen.beacons import Beacon, BeaconType, BEACONS_RESOURCE_PATH
THIS_DIR = Path(__file__).parent.resolve()
SRC_DIR = THIS_DIR.parents[1]
EXPORT_DIR = SRC_DIR / BEACONS_RESOURCE_PATH
@contextmanager
def cd(path: Path):
cwd = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(cwd)
def convert_lua_frequency(raw: Union[float, int]) -> int:
if isinstance(raw, float):
if not raw.is_integer():
# The values are in hertz, and everything should be a whole number.
raise ValueError(f"Unexpected non-integer frequency: {raw}")
return int(raw)
else:
return raw
def beacons_from_terrain(dcs_path: Path, path: Path) -> Iterable[Beacon]:
# TODO: Fix case-sensitive issues.
# The beacons.lua file differs by case in some terrains. Will need to be
# fixed if the tool is to be run on Linux, but presumably the server
# wouldn't be able to find these anyway.
beacons_lua = path / "beacons.lua"
with cd(dcs_path):
lua = lupa.LuaRuntime()
lua.execute(textwrap.dedent("""\
function module(name)
end
"""))
bind_gettext = lua.eval(textwrap.dedent("""\
function(py_gettext)
package.preload["i_18n"] = function()
return {
translate = py_gettext
}
end
end
"""))
translator = gettext.translation(
"messages", path / "l10n", languages=["en"])
def translate(message_name: str) -> str:
if not message_name:
return message_name
return translator.gettext(message_name)
bind_gettext(translate)
src = beacons_lua.read_text()
lua.execute(src)
beacon_types_map: Dict[int, BeaconType] = {}
for beacon_type in BeaconType:
beacon_value = lua.eval(beacon_type.name)
beacon_types_map[beacon_value] = beacon_type
beacons = lua.eval("beacons")
for beacon in beacons.values():
beacon_type_lua = beacon["type"]
if beacon_type_lua not in beacon_types_map:
raise KeyError(
f"Unknown beacon type {beacon_type_lua}. Check that all "
f"beacon types in {beacon_types_path} are present in "
f"{BeaconType.__class__.__name__}"
)
beacon_type = beacon_types_map[beacon_type_lua]
yield Beacon(
beacon["display_name"],
beacon["callsign"],
beacon_type,
convert_lua_frequency(beacon["frequency"]),
getattr(beacon, "channel", None)
)
class Importer:
"""Imports beacon definitions from each available terrain mod.
Only beacons for maps owned by the user can be imported. Other maps that
have been previously imported will not be disturbed.
"""
def __init__(self, dcs_path: Path, export_dir: Path) -> None:
self.dcs_path = dcs_path
self.export_dir = export_dir
def run(self) -> None:
"""Exports the beacons for each available terrain mod."""
terrains_path = self.dcs_path / "Mods" / "terrains"
self.export_dir.mkdir(parents=True, exist_ok=True)
for terrain in terrains_path.iterdir():
beacons = beacons_from_terrain(self.dcs_path, terrain)
self.export_beacons(terrain.name, beacons)
def export_beacons(self, terrain: str, beacons: Iterable[Beacon]) -> None:
terrain_py_path = self.export_dir / f"{terrain.lower()}.json"
import json
terrain_py_path.write_text(json.dumps([
dataclasses.asdict(b) for b in beacons
], indent=True))
def parse_args() -> argparse.Namespace:
"""Parses and returns command line arguments."""
parser = argparse.ArgumentParser()
def resolved_path(val: str) -> Path:
"""Returns the given string as a fully resolved Path."""
return Path(val).resolve()
parser.add_argument(
"--export-to",
type=resolved_path,
default=EXPORT_DIR,
help="Output directory for generated JSON files.")
parser.add_argument(
"dcs_path",
metavar="DCS_PATH",
type=resolved_path,
help="Path to DCS installation."
)
return parser.parse_args()
def main() -> None:
"""Program entry point."""
args = parse_args()
Importer(args.dcs_path, args.export_to).run()
if __name__ == "__main__":
main()