Skip to content

Commit

Permalink
Add generation-time allocation for STNs.
Browse files Browse the repository at this point in the history
This is a stop-gap solution so we can get a release out that supports
datalink as quickly as possible. It would be better to assign these
early so the player can select which flights should be their
team/donors, but for that we need to keep a registry in Game, plumb it
through to each Flight creation (and there are many callsites for that),
subscribe each flight to its datalink contacts (so disbanded flights do
not remain part of a network), and of course the UI itself. That'll come
later.

Follow up work:

* Filtering to only apply for the airframes that can use it
* Selection of team/donors
  • Loading branch information
DanAlbert committed Oct 4, 2023
1 parent c83d8be commit ef7d7ba
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 3 deletions.
23 changes: 23 additions & 0 deletions game/datalink/sourcetracknumber.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from dataclasses import dataclass

from game.datalink.sourcetracknumberprefix import SourceTrackNumberPrefix


@dataclass(frozen=True)
class SourceTrackNumber:
"""Source track number (STN) for a flight member."""

prefix: SourceTrackNumberPrefix
index: int

def __post_init__(self) -> None:
if self.index < 0:
raise ValueError("STN indexes cannot be negative")
if self.index >= 8:
raise ValueError("STN indexes must be < 8")

def __str__(self) -> str:
return f"{self.prefix}{self.index}"

def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.prefix!r}, {self.index})"
42 changes: 42 additions & 0 deletions game/datalink/sourcetracknumberprefix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from dataclasses import dataclass


@dataclass(frozen=True)
class SourceTrackNumberPrefix:
"""The prefix of a source track number (STN) for a flight.
STNs are 5 octal digits. To make it easier for players to guess the codes for their
flight members, these are segmented so that the least significant digit is always
the flight member index. This wastes of the address space, but even at the lowest
density (single-member flights, ~88% waste), there are still enough prefixes for
4096 aircraft.
There is no per-package segmenting, however. DCS imposes a flight-size limitation on
us (usually four, sometimes fewer), but we do not restrict the number of flights
that can be in a package. If we were to carve out a digit for the package, we'd be
limiting the package to a max of eight flights. It's larger than a typical package,
but it's not unreasonable for a large OCA strike. If we carved out two digits, the
limit would be more than enough (64), but it would also limit the game to 64
packages. That's also quite high, but it's low enough that it could be hit. There's
some wiggle room here, since for now the only aircraft that need STNs are the F-16C,
F/A-18C, and A-10C II, but we shouldn't break in the unlikely case where the game is
composed entirely of those airframes.
Carving up the address space in different ways (such as two bits for the flight and
six for the package) would defeat the purpose of doing so, since they wouldn't be
recognizable prefixes for players, since these are expressed as octal in the jet.
"""

value: int

def __post_init__(self) -> None:
if self.value < 0:
raise ValueError("STN prefixes cannot be negative")
if self.value >= 0o10000:
raise ValueError("STN prefixes must be < 0o10000")

def __str__(self) -> str:
return f"{oct(self.value)[2:]:0>4}"

def __repr__(self) -> str:
return f"{self.__class__.__name__}({oct(self.value)})"
5 changes: 5 additions & 0 deletions game/missiongenerator/aircraft/aircraftgenerator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import logging
from collections.abc import Iterator
from datetime import datetime
from functools import cached_property
from typing import Any, Dict, TYPE_CHECKING
Expand All @@ -16,6 +17,7 @@
from game.ato.flighttype import FlightType
from game.ato.package import Package
from game.ato.starttype import StartType
from game.datalink.sourcetracknumberprefix import SourceTrackNumberPrefix
from game.factions.faction import Faction
from game.missiongenerator.missiondata import MissionData
from game.radio.radios import RadioRegistry
Expand Down Expand Up @@ -47,6 +49,7 @@ def __init__(
time: datetime,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
stn_prefix_allocator: Iterator[int],
unit_map: UnitMap,
mission_data: MissionData,
helipads: dict[ControlPoint, StaticGroup],
Expand All @@ -57,6 +60,7 @@ def __init__(
self.time = time
self.radio_registry = radio_registry
self.tacan_registy = tacan_registry
self.stn_prefix_allocator = stn_prefix_allocator
self.unit_map = unit_map
# A list of per-package briefing data, which is in turn a list of per-flight
# briefing data.
Expand Down Expand Up @@ -176,6 +180,7 @@ def create_and_configure_flight(
self.time,
self.radio_registry,
self.tacan_registy,
SourceTrackNumberPrefix(next(self.stn_prefix_allocator)),
self.mission_data,
dynamic_runways,
self.use_client,
Expand Down
3 changes: 3 additions & 0 deletions game/missiongenerator/aircraft/flightdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from dcs.flyingunit import FlyingUnit

from game.callsigns import create_group_callsign_from_unit
from game.datalink.sourcetracknumber import SourceTrackNumber

if TYPE_CHECKING:
from game.ato import FlightType, FlightWaypoint, Package
Expand Down Expand Up @@ -66,6 +67,8 @@ class FlightData:

laser_codes: list[Optional[int]]

source_track_numbers: list[SourceTrackNumber]

custom_name: Optional[str]

callsign: str = field(init=False)
Expand Down
11 changes: 10 additions & 1 deletion game/missiongenerator/aircraft/flightgroupconfigurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from game.ato import Flight, FlightType
from game.callsigns import callsign_for_support_unit
from game.data.weapons import Pylon
from game.datalink.sourcetracknumber import SourceTrackNumber
from game.datalink.sourcetracknumberprefix import SourceTrackNumberPrefix
from game.missiongenerator.logisticsgenerator import LogisticsGenerator
from game.missiongenerator.missiondata import AwacsInfo, MissionData, TankerInfo
from game.radio.radios import RadioFrequency, RadioRegistry
Expand Down Expand Up @@ -40,6 +42,7 @@ def __init__(
time: datetime,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
stn_prefix: SourceTrackNumberPrefix,
mission_data: MissionData,
dynamic_runways: dict[str, RunwayData],
use_client: bool,
Expand All @@ -52,6 +55,7 @@ def __init__(
self.time = time
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.stn_prefix = stn_prefix
self.mission_data = mission_data
self.dynamic_runways = dynamic_runways
self.use_client = use_client
Expand All @@ -66,8 +70,12 @@ def configure(self) -> FlightData:
flight_channel = self.setup_radios()

laser_codes: list[Optional[int]] = []
for unit, member in zip(self.group.units, self.flight.iter_members()):
stns = []
for idx, (unit, member) in enumerate(
zip(self.group.units, self.flight.iter_members())
):
self.configure_flight_member(unit, member, laser_codes)
stns.append(SourceTrackNumber(self.stn_prefix, idx))

divert = None
if self.flight.divert is not None:
Expand Down Expand Up @@ -134,6 +142,7 @@ def configure(self) -> FlightData:
joker_fuel=bingo_estimator.estimate_joker(),
custom_name=self.flight.custom_name,
laser_codes=laser_codes,
source_track_numbers=stns,
)

def configure_flight_member(
Expand Down
7 changes: 5 additions & 2 deletions game/missiongenerator/kneeboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,9 @@ def write(self, path: Path) -> None:

table = []
for flight in self.flights:
for idx, laser_code in enumerate(flight.laser_codes, 1):
for idx, (laser_code, stn) in enumerate(
zip(flight.laser_codes, flight.source_track_numbers), 1
):
# Blank the flight-wide properties to make the table easier to scan.
if idx > 1:
task = ""
Expand All @@ -725,9 +727,10 @@ def write(self, path: Path) -> None:
task,
radio,
"" if laser_code is None else str(laser_code),
"" if stn is None else str(stn),
]
)
writer.table(table, ["Aircraft", "Task", "Radio", "Laser code"])
writer.table(table, ["Aircraft", "Task", "Radio", "Laser code", "STN"])

writer.write(path)

Expand Down
2 changes: 2 additions & 0 deletions game/missiongenerator/missiongenerator.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def __init__(self, game: Game, time: datetime) -> None:

self.radio_registry = RadioRegistry()
self.tacan_registry = TacanRegistry()
self.stn_prefix_allocator = iter(range(0o10000))

self.generation_started = False

Expand Down Expand Up @@ -259,6 +260,7 @@ def generate_air_units(self, tgo_generator: TgoGenerator) -> None:
self.time,
self.radio_registry,
self.tacan_registry,
self.stn_prefix_allocator,
self.unit_map,
mission_data=air_support_generator.mission_data,
helipads=tgo_generator.helipads,
Expand Down
34 changes: 34 additions & 0 deletions tests/datalink/test_sourcetracknumber.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import pytest

from game.datalink.sourcetracknumber import SourceTrackNumber
from game.datalink.sourcetracknumberprefix import SourceTrackNumberPrefix


def test_limits() -> None:
SourceTrackNumber(SourceTrackNumberPrefix(0), 0)
SourceTrackNumber(SourceTrackNumberPrefix(0o7777), 7)
with pytest.raises(ValueError):
SourceTrackNumber(SourceTrackNumberPrefix(0), -1)
with pytest.raises(ValueError):
SourceTrackNumber(SourceTrackNumberPrefix(0o7777), 0o10)


def test_str() -> None:
assert str(SourceTrackNumber(SourceTrackNumberPrefix(0), 0)) == "00000"
assert str(SourceTrackNumber(SourceTrackNumberPrefix(0o123), 4)) == "01234"
assert str(SourceTrackNumber(SourceTrackNumberPrefix(0o7777), 7)) == "77777"


def test_repr() -> None:
assert (
repr(SourceTrackNumber(SourceTrackNumberPrefix(0), 0))
== "SourceTrackNumber(SourceTrackNumberPrefix(0o0), 0)"
)
assert (
repr(SourceTrackNumber(SourceTrackNumberPrefix(0o123), 4))
== "SourceTrackNumber(SourceTrackNumberPrefix(0o123), 4)"
)
assert (
repr(SourceTrackNumber(SourceTrackNumberPrefix(0o7777), 7))
== "SourceTrackNumber(SourceTrackNumberPrefix(0o7777), 7)"
)
24 changes: 24 additions & 0 deletions tests/datalink/test_sourcetracknumberprefix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import pytest

from game.datalink.sourcetracknumberprefix import SourceTrackNumberPrefix


def test_limits() -> None:
SourceTrackNumberPrefix(0)
SourceTrackNumberPrefix(0o7777)
with pytest.raises(ValueError):
SourceTrackNumberPrefix(0o10000)
with pytest.raises(ValueError):
SourceTrackNumberPrefix(-1)


def test_str() -> None:
assert str(SourceTrackNumberPrefix(0)) == "0000"
assert str(SourceTrackNumberPrefix(0o123)) == "0123"
assert str(SourceTrackNumberPrefix(0o7777)) == "7777"


def test_repr() -> None:
assert repr(SourceTrackNumberPrefix(0)) == "SourceTrackNumberPrefix(0o0)"
assert repr(SourceTrackNumberPrefix(0o123)) == "SourceTrackNumberPrefix(0o123)"
assert repr(SourceTrackNumberPrefix(0o7777)) == "SourceTrackNumberPrefix(0o7777)"

0 comments on commit ef7d7ba

Please sign in to comment.