diff --git a/game/sidc.py b/game/sidc.py new file mode 100644 index 00000000..7ae4ace2 --- /dev/null +++ b/game/sidc.py @@ -0,0 +1,276 @@ +"""Implements Symbol Identification Codes (SIDCs) as defined by NATO APP-6(D). + +This implementation only covers assembly of the identifier strings. The front-end is +responsible for drawing the icons. + +The third ten digits (used for national modifications and additions not covered by +APP-6) are not implemented. The third set of ten digits are optional and will be omitted +from the output. + +https://nso.nato.int/nso/nsdd/main/standards/ap-details/1912/EN +https://www.spatialillusions.com/milsymbol/docs/milsymbol-2525d.html +""" +from __future__ import annotations + +from dataclasses import dataclass +from enum import IntEnum, unique + +# Version field defined by A.5. +VERSION = 10 + + +@unique +class Context(IntEnum): + """Context field defined by A.6..""" + + REALITY = 0 + EXERCISE = 1 + SIMULATION = 2 + # 3-9 are reserved for future use. + + def __str__(self) -> str: + return str(self.value) + + +@unique +class StandardIdentity(IntEnum): + """Standard identity field defined by A.6.""" + + PENDING = 0 + UNKNOWN = 1 + ASSUMED_FRIEND = 2 + FRIEND = 3 + NEUTRAL = 4 + SUSPECT_JOKER = 5 + HOSTILE_FAKER = 6 + # 7-9 are reserved for future use. + + def __str__(self) -> str: + return str(self.value) + + +@unique +class SymbolSet(IntEnum): + """Symbol set field defined by A.7.""" + + UNKNOWN = 0 + AIR = 1 + AIR_MISSILE = 2 + SPACE = 5 + SPACE_MISSILE = 6 + LAND_UNIT = 10 + LAND_CIVILIAN_UNIT_ORGANIZATION = 11 + LAND_EQUIPMENT = 15 + LAND_INSTALLATIONS = 20 + CONTROL_MEASURE = 25 + DISMOUNTED_INDIVIDUAL = 27 + SEA_SURFACE = 30 + SEA_SUBSURFACE = 35 + MINE_WARFARE = 36 + ACTIVITY_EVENT = 40 + ATMOSPHERIC = 45 + OCEANOGRAPHIC = 46 + METEOROLOGICAL_SPACE = 47 + SIGNALS_INTELLIGENCE_SPACE = 50 + SIGNALS_INTELLIGENCE_AIR = 51 + SIGNALS_INTELLIGENCE_LAND = 52 + SIGNALS_INTELLIGENCE_SURFACE = 53 + SIGNALS_INTELLIGENCE_SUBSURFACE = 54 + CYBERSPACE_SPACE = 60 + CYBERSPACE_AIR = 61 + CYBERSPACE_LAND = 62 + CYBERSPACE_SURFACE = 63 + CYBERSPACE_SUBSURFACE = 64 + VERSION_EXTENSION_FLAG = 99 + # All other values reserved for future use. + + def __str__(self) -> str: + return f"{self.value:02}" + + +@unique +class Status(IntEnum): + """Status field defined by A.8 Status.""" + + PRESENT = 0 + PLANNED_ANTICIPATED_SUSPECT = 1 + PRESENT_FULLY_CAPABLE = 2 + PRESENT_DAMAGED = 3 + PRESENT_DESTROYED = 4 + PRESENT_FULL_TO_CAPACITY = 5 + # 6-8 reserved for future use. + VERSION_EXTENSION_FLAG = 9 + + def __str__(self) -> str: + return str(self.value) + + +@unique +class HeadquartersTaskForceDummy(IntEnum): + """Headquarters/Task Force/Dummy field defined by A.9.""" + + NOT_APPLICABLE = 0 + FEINT_DUMMY = 1 + HEADQUARTERS = 2 + FEINT_DUMMY_HEADQUARTERS = 3 + TASK_FORCE = 4 + FEINT_DUMMY_TASK_FORCE = 5 + TASK_FORCE_HEADQUARTERS = 6 + FEINT_DUMMY_TASK_FORCE_HEADQUARTERS = 7 + # 8 reserved for future use. + VERSION_EXTENSION_FLAG = 9 + + def __str__(self) -> str: + return str(self.value) + + +@unique +class Amplifier(IntEnum): + """Unit Echelon/Equipment Mobility/Naval Towed Array Amplifier defined by A.10""" + + UNKNOWN = 0 + + # Echelon at brigade and below + TEAM_CREW = 11 + SQUAD = 12 + SECTION = 13 + PLATOON_DETACHMENT = 14 + COMPANY_BATTERY_TROOP = 15 + BATTALION_SQUADRON = 16 + REGIMENT_GROUP = 17 + BRIGADE = 18 + VERSION_EXTENSION_FLAG = 19 + + # Echelon at brigade and above + DIVISION = 21 + CORP_MARINE_EXPEDITIONARY_FORCE = 22 + ARMY = 23 + ARMY_GROUP_FRONT = 24 + REGION_THEATRE = 25 + COMMAND = 26 + # 27-28 reserved for future use. + VERSION_EXTENSION_FLAG2 = 29 + + # Equipment mobility on land + WHEELED_LIMITED_CROSS_COUNTRY = 31 + WHEELED_CROSS_COUNTRY = 32 + TRACKED = 33 + WHEELED_AND_TRAKCED_COMBINATION = 34 + TOWED = 35 + RAIL = 36 + PACK_ANIMALS = 37 + # 38 reserved for future use. + VERSION_EXTENSION_FLAG3 = 39 + + # Equipment mobility on snow + OVER_SNOW = 41 + SLED = 42 + # 3-8 reserved for future use. + VERSION_EXTENSION_FLAG4 = 49 + + # Equipment mobility on water + BARGE = 51 + AMPHIBIOUS = 52 + # 3-8 reserved for future use. + VERSION_EXTENSION_FLAG5 = 59 + + # Naval towed array + SHORT_TOWED_ARRAY = 61 + LONG_TOWED_ARRAY = 62 + # 3-8 reserved for future use. + VERSION_EXTENSION_FLAG6 = 69 + + # Leadership indicator + LEADER_INDIVIDUAL = 71 + DEPUTY_INDIVIDUAL = 72 + # 3-8 reserved for future use. + VERSION_EXTENSION_FLAG7 = 79 + + # 80-89 reserved for future use. + # 90-99 version extension flag. + + def __str__(self) -> str: + return f"{self.value:02}" + + +class Entity(IntEnum): + def __str__(self) -> str: + return f"{self.value:06}" + + +# Entity types (the second set of ten digits are implemented as-needed. These are +# defined by section A.13. Entity/Entity Type/Entity Subtype and Sector 1 and Sector 2 +# Modifiers. The specific entity enum used by the SIDC depends on the symbol set used. +@unique +class AirEntity(Entity): + """Air Entity/Entity Type/Entity Subtype defined by table A-10.""" + + UNSPECIFIED = 0 + + +@unique +class LandInstallationEntity(Entity): + """Land Installation Entity/Entity Type/Entity Subtype defined by table A-27.""" + + UNSPECIFIED = 0 + + MILITARY_BASE = 120802 + AIPORT_AIR_BASE = 121301 + + +@unique +class SeaSurfaceEntity(Entity): + """Sea Surface Entity/Entity Type/Entity Subtype defined by table A-34.""" + + UNSPECIFIED = 0 + + CARRIER = 120100 + AMPHIBIOUS_ASSAULT_SHIP_GENERAL = 120303 + + +@unique +class UnknownEntity(Entity): + """Fallback entity type used when the symbol set is not known.""" + + UNSPECIFIED = 0 + + +class Modifier(IntEnum): + """Fallback modifier used when the symbol set is not known.""" + + UNSPECIFIED = 0 + + def __str__(self) -> str: + return f"{self.value:02}" + + +@dataclass +class SymbolIdentificationCode: + version = VERSION + context: Context = Context.REALITY + standard_identity: StandardIdentity = StandardIdentity.UNKNOWN + symbol_set: SymbolSet = SymbolSet.UNKNOWN + status: Status = Status.PRESENT + headquarters_task_force_dummy: HeadquartersTaskForceDummy = ( + HeadquartersTaskForceDummy.NOT_APPLICABLE + ) + amplifier: Amplifier = Amplifier.UNKNOWN + entity: Entity = UnknownEntity.UNSPECIFIED + sector_one_modifier = Modifier.UNSPECIFIED + sector_two_modifier = Modifier.UNSPECIFIED + + def __str__(self) -> str: + return "".join( + [ + f"{self.version:02}", + str(self.context), + str(self.standard_identity), + str(self.symbol_set), + str(self.status), + str(self.headquarters_task_force_dummy), + str(self.amplifier), + str(self.entity), + str(self.sector_one_modifier), + str(self.sector_two_modifier), + ] + ) diff --git a/tests/test_sidc.py b/tests/test_sidc.py new file mode 100644 index 00000000..6c6fac36 --- /dev/null +++ b/tests/test_sidc.py @@ -0,0 +1,17 @@ +from game.sidc import ( + LandInstallationEntity, + StandardIdentity, + Status, + SymbolIdentificationCode, + SymbolSet, +) + + +def test_sidc() -> None: + sidc = SymbolIdentificationCode( + standard_identity=StandardIdentity.FRIEND, + symbol_set=SymbolSet.LAND_INSTALLATIONS, + status=Status.PRESENT_DAMAGED, + entity=LandInstallationEntity.AIPORT_AIR_BASE, + ) + assert str(sidc) == "10032030001213010000"