mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Add beacon list importer.
This commit is contained in:
parent
af596c58c3
commit
d051859371
@ -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
74
gen/beacons.py
Normal 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)
|
||||
183
resources/tools/import_beacons.py
Normal file
183
resources/tools/import_beacons.py
Normal 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()
|
||||
Loading…
x
Reference in New Issue
Block a user