├── .github └── workflows │ └── pythonpackage.yml ├── .gitignore ├── LICENSE.txt ├── Makefile ├── Manifest.in ├── README.md ├── dcs ├── __init__.py ├── action.py ├── atcradio.py ├── beacons.py ├── cloud_presets.py ├── coalition.py ├── condition.py ├── countries.py ├── country.py ├── drawing │ ├── __init__.py │ ├── drawing.py │ ├── drawings.py │ ├── icon.py │ ├── layer.py │ ├── line.py │ ├── options.py │ ├── polygon.py │ └── text_box.py ├── flyingunit.py ├── forcedoptions.py ├── goals.py ├── groundcontrol.py ├── helicopters.py ├── installation.py ├── liveries │ ├── __init__.py │ ├── liberation.py │ ├── livery.py │ ├── liverycache.py │ ├── liveryscanner.py │ └── liveryset.py ├── lua │ ├── __init__.py │ ├── parse.py │ ├── serialize.py │ └── test_parse.py ├── mapping.py ├── mission.py ├── nav_target_point.py ├── password.py ├── payloads.py ├── planes.py ├── point.py ├── py.typed ├── scripts │ ├── __init__.py │ ├── destroy_oil_transport.py │ ├── dogfight_wwii.py │ └── nevada_random_mission.py ├── ships.py ├── statics.py ├── status_message.py ├── stub templates │ └── liveries_scanner.pyi ├── task.py ├── templates.py ├── terrain │ ├── __init__.py │ ├── caucasus │ │ ├── __init__.py │ │ ├── airports.py │ │ ├── caucasus.py │ │ ├── citygraph.p │ │ └── projection.py │ ├── falklands │ │ ├── __init__.py │ │ ├── airports.py │ │ ├── falklands.py │ │ └── projection.py │ ├── marianaislands │ │ ├── __init__.py │ │ ├── airports.py │ │ ├── marianaislands.py │ │ └── projection.py │ ├── nevada │ │ ├── __init__.py │ │ ├── airports.py │ │ ├── citygraph.p │ │ ├── nevada.py │ │ └── projection.py │ ├── normandy │ │ ├── __init__.py │ │ ├── airports.py │ │ ├── normandy.py │ │ └── projection.py │ ├── persiangulf │ │ ├── __init__.py │ │ ├── airports.py │ │ ├── persiangulf.py │ │ └── projection.py │ ├── projections │ │ ├── __init__.py │ │ └── transversemercator.py │ ├── sinai │ │ ├── __init__.py │ │ ├── airports.py │ │ ├── projection.py │ │ └── sinai.py │ ├── syria │ │ ├── __init__.py │ │ ├── airports.py │ │ ├── projection.py │ │ └── syria.py │ ├── terrain.py │ └── thechannel │ │ ├── __init__.py │ │ ├── airports.py │ │ ├── projection.py │ │ └── thechannel.py ├── translation.py ├── triggers.py ├── unit.py ├── unitgroup.py ├── unitpropertydescription.py ├── unittype.py ├── vehicles.py ├── weapons_data.py ├── weather.py └── winreg.py ├── doc ├── Makefile ├── caucasus_random_mission.rst ├── conf.py ├── dcs.lua.rst ├── dcs.rst ├── dcs.terrain.rst ├── index.rst ├── make.bat ├── mission.rst ├── modules.rst ├── quickstart.rst └── task.rst ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── bypass_triggers.miz ├── images │ ├── blue.png │ ├── m1 │ │ └── briefing.png │ └── m2 │ │ └── briefing.png ├── liveries │ ├── __init__.py │ ├── test_livery.py │ └── test_liveryscanner.py ├── loadtest.miz ├── missions │ ├── Coop_OP_Custodia_v1.miz │ ├── Draw_tool_test.miz │ ├── Forestry_Operations.miz │ ├── LUNA.miz │ ├── Mission_with_required_modules.miz │ ├── SierraHotel_Training_Mission_04.miz │ ├── TTI_GC_SC_1.68a.miz │ ├── a_out_picture.miz │ ├── big-formation-carpet-bombing.miz │ ├── big-formation.miz │ ├── countries-without-units-on-the-map.miz │ ├── g-effect-game.miz │ ├── g-effect-none.miz │ ├── g-effect-sim.miz │ ├── g-effect-uncheked.miz │ ├── linked-trigger-zone.miz │ └── payload.restrictions.miz ├── test_country.py ├── test_drawings.py ├── test_installation.py ├── test_mapping.py ├── test_mission.py ├── test_serialize.py ├── test_terrain.py ├── test_triggers.py ├── test_unittype.py └── test_weather.py └── tools ├── MissionScripting.lua ├── actions_export.py ├── airport_import.py ├── city_grapher.py ├── coord_export.lua ├── export_map_projection.py ├── goalrule_export.py ├── graph_missions ├── caucasus_graph.miz └── nevada_graph.miz ├── json.lua ├── missiondiff.py ├── polyzone.py ├── pydcs_export.lua └── weather_import.py /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: ["3.9", "3.10", "3.11"] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v1 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install deps 27 | run: | 28 | pip install -r requirements.txt 29 | - name: Type check 30 | run: | 31 | pip install mypy 32 | mypy dcs 33 | - name: Lint with flake8 34 | run: | 35 | pip install flake8 36 | # Check the default configuration. 37 | flake8 . 38 | # But also run a limited number of checks to verify that the excluded files are at least correct syntax. 39 | flake8 . --isolated --count --select=E9,F63,F7,F82 --show-source --statistics 40 | - name: Test with pytest 41 | run: | 42 | pip install pytest 43 | python setup.py test 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | /missions/ 3 | .idea 4 | build/ 5 | dist/ 6 | pydcs.egg-info/ 7 | doc/_* 8 | venv/ 9 | /export.log 10 | /.coverage 11 | /coverage.xml 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build upload clean 2 | 3 | build: 4 | python3 setup.py sdist bdist_wheel 5 | 6 | upload: 7 | python3 -m twine upload dist/* 8 | 9 | clean: 10 | rm -Rf dist/ 11 | 12 | -------------------------------------------------------------------------------- /Manifest.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE.txt 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python dcs mission framework 2 | 3 | pydcs is a python framework for creating and editing mission files 4 | from digital combat simulator. 5 | 6 | Possible use cases are: 7 | 8 | * assisting mission creators 9 | * random mission creation 10 | * write an external mission editor on top of it 11 | * data export from existing missions 12 | * ... 13 | 14 | ## Sample 15 | 16 | import dcs 17 | m = dcs.Mission() 18 | 19 | batumi = m.terrain.batumi() 20 | batumi.set_blue() 21 | 22 | usa = m.country("USA") 23 | m.awacs_flight( 24 | usa, "AWACS", dcs.planes.E_3A, 25 | batumi, dcs.Point(batumi.position.x + 20000, batumi.position.y + 80000), 26 | race_distance=120 * 1000, heading=90) 27 | 28 | m.save("sample.miz") 29 | 30 | This code generates a mission with a AWACS flight starting cold from batumi. 31 | 32 | ## Random mission scripts 33 | 34 | pydcs comes with 2 proof of concept scripts: 35 | 36 | ### dcs_random 37 | 38 | This script can generate 3 different mission types 39 | 40 | * refuel 41 | 42 | Generates a refuel mission for A-10C or M-2000C aircrafts, search your tanker and refuel. 43 | 44 | * CAS 45 | 46 | Support ground troops. 47 | 48 | * CAP 49 | 50 | Take care of your tanker and AWACS. 51 | 52 | For options see the script help with `dcs_random --help` 53 | 54 | ### dcs_dogfight_wwii 55 | 56 | This script randomly generates WWII dogfights with a given number of planes near a random airport. 57 | For options also see the script help `dcs_dogfight_wwii --help` 58 | 59 | ### 60 | 61 | ## Install 62 | 63 | pip install pydcs 64 | 65 | ## Documentation 66 | 67 | The current documentation can be found [here](http://dcs.readthedocs.org/en/latest) 68 | 69 | ## TODO 70 | 71 | * Failures 72 | * More/better documentation 73 | -------------------------------------------------------------------------------- /dcs/__init__.py: -------------------------------------------------------------------------------- 1 | from . import mission 2 | from . import task 3 | from . import templates 4 | from . import countries 5 | from . import unittype 6 | from . import planes 7 | from . import helicopters 8 | from . import statics 9 | from . import vehicles 10 | from . import ships 11 | from . import terrain 12 | from . import unit 13 | from . import unitgroup 14 | from . import goals 15 | from . import weather 16 | from . import point 17 | from . import triggers 18 | from . import condition 19 | from . import action 20 | from . import forcedoptions 21 | from . import installation 22 | from . import nav_target_point 23 | from .mapping import Point, Rectangle, Polygon 24 | from .mission import Mission 25 | -------------------------------------------------------------------------------- /dcs/atcradio.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass(frozen=True) 5 | class AtcRadio: 6 | hf_hz: int 7 | vhf_low_hz: int 8 | vhf_high_hz: int 9 | uhf_hz: int 10 | -------------------------------------------------------------------------------- /dcs/beacons.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Any, Dict 5 | 6 | 7 | @dataclass(frozen=True) 8 | class AirportBeacon: 9 | """A beacon attached to an airport that isn't specific to a runway.""" 10 | # The ID of the associated beacon (at time of writing, this data is not exposed in 11 | # pydcs). 12 | # Example: airfield12_0 13 | id: str 14 | 15 | @staticmethod 16 | def from_lua(data: Dict[str, Any]) -> AirportBeacon: 17 | """Parses runway beacon information from the Lua dump. 18 | 19 | Example data: 20 | 21 | { 22 | ["beaconId"] = "airfield12_1", 23 | } 24 | """ 25 | return AirportBeacon( 26 | data["beaconId"] 27 | ) 28 | 29 | 30 | @dataclass(frozen=True) 31 | class RunwayBeacon(AirportBeacon): 32 | """A beacon attached to a runway.""" 33 | # The name of the runway. Probably has no semantic value, but seems to follow the 34 | # form {heading0}-{heading1}. 35 | # Example: "04-22" 36 | runway_name: str 37 | # The numeric ID of the runway for the airport. Appears to be a lua index (1-based). 38 | # Example: 1 39 | runway_id: int 40 | # The side of the runway that the beacon is associated with. ILS beacons, for 41 | # example, only point one way. 42 | # Example: "22" 43 | runway_side: str 44 | 45 | @staticmethod 46 | def from_lua(data: Dict[str, Any]) -> RunwayBeacon: 47 | """Parses runway beacon information from the Lua dump. 48 | 49 | Example data: 50 | 51 | { 52 | ["runwayName"] = "04-22", 53 | ["runwayId"] = 1, 54 | ["runwaySide"] = "22", 55 | ["beaconId"] = "airfield12_1", 56 | } 57 | """ 58 | return RunwayBeacon( 59 | data["beaconId"], 60 | data["runwayName"], 61 | data["runwayId"], 62 | data["runwaySide"], 63 | ) 64 | -------------------------------------------------------------------------------- /dcs/country.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dcs.helicopters import HelicopterType 4 | from dcs.planes import PlaneType 5 | from dcs.unitgroup import VehicleGroup, ShipGroup, PlaneGroup, StaticGroup, HelicopterGroup, FlyingGroup, Group 6 | from typing import List, Dict, Optional, Set, Type, Sequence 7 | 8 | 9 | def find_exact(group_name, find_name): 10 | return group_name == find_name 11 | 12 | 13 | def find_match(group_name, find_name): 14 | return find_name in group_name 15 | 16 | 17 | find_map = { 18 | "exact": find_exact, 19 | "match": find_match 20 | } 21 | 22 | 23 | class Country: 24 | callsign: Dict[str, List[str]] = {} 25 | planes: List[Type[PlaneType]] = [] 26 | helicopters: List[Type[HelicopterType]] = [] 27 | use_western_callsigns = True 28 | 29 | def __init__(self, _id, name, short_name): 30 | self.id = _id 31 | self.name = name 32 | self.shortname = short_name 33 | self.vehicle_group: List[VehicleGroup] = [] 34 | self.ship_group: List[ShipGroup] = [] 35 | self.plane_group: List[PlaneGroup] = [] 36 | self.helicopter_group: List[HelicopterGroup] = [] 37 | self.static_group: List[StaticGroup] = [] 38 | self.current_callsign_id = 99 39 | self.current_callsign_category: Dict[str, int] = {} 40 | self._tail_numbers: Set[str] = set() 41 | 42 | def add_vehicle_group(self, vgroup) -> None: 43 | self.vehicle_group.append(vgroup) 44 | 45 | def add_ship_group(self, sgroup): 46 | self.ship_group.append(sgroup) 47 | 48 | def add_plane_group(self, pgroup): 49 | self.plane_group.append(pgroup) 50 | 51 | def add_helicopter_group(self, hgroup): 52 | self.helicopter_group.append(hgroup) 53 | 54 | def add_aircraft_group(self, group: FlyingGroup) -> None: 55 | if group.units[0].unit_type.helicopter: 56 | assert isinstance(group, HelicopterGroup) 57 | self.helicopter_group.append(group) 58 | else: 59 | assert isinstance(group, PlaneGroup) 60 | self.plane_group.append(group) 61 | 62 | def add_static_group(self, sgroup): 63 | self.static_group.append(sgroup) 64 | 65 | def remove_static_group(self, sgroup): 66 | for i in range(0, len(self.static_group)): 67 | if sgroup.id == self.static_group[i].id: 68 | del self.static_group[i] 69 | return True 70 | 71 | return False 72 | 73 | def find_group(self, group_name, search="exact"): 74 | groups = [self.vehicle_group, 75 | self.ship_group, 76 | self.plane_group, 77 | self.helicopter_group, 78 | self.static_group] 79 | for search_group in groups: 80 | for group in search_group: 81 | if find_map[search](group.name, group_name): 82 | return group 83 | return None 84 | 85 | def find_group_by_id(self, group_id: int) -> Optional[Group]: 86 | groups: List[Sequence[Group]] = [self.vehicle_group, 87 | self.ship_group, 88 | self.plane_group, 89 | self.helicopter_group, 90 | self.static_group] 91 | for search_group in groups: 92 | for group in search_group: 93 | if group.id == group_id: 94 | return group 95 | 96 | return None 97 | 98 | def find_vehicle_group(self, name: str, search="exact"): 99 | for group in self.vehicle_group: 100 | if find_map[search](group.name, name): 101 | return group 102 | return None 103 | 104 | def find_ship_group(self, name: str, search="exact"): 105 | for group in self.ship_group: 106 | if find_map[search](group.name, name): 107 | return group 108 | return None 109 | 110 | def find_plane_group(self, name: str, search="exact"): 111 | for group in self.plane_group: 112 | if find_map[search](group.name, name): 113 | return group 114 | 115 | def find_helicopter_group(self, name: str, search="exact"): 116 | for group in self.helicopter_group: 117 | if find_map[search](group.name, name): 118 | return group 119 | 120 | def find_static_group(self, name: str, search="exact"): 121 | for group in self.static_group: 122 | if find_map[search](group.name, name): 123 | return group 124 | return None 125 | 126 | def vehicle_group_within(self, point, distance) -> List[Group]: 127 | """Return all vehicle groups within the radius of a given point. 128 | 129 | Args: 130 | point(mapping.Point): Center of circle 131 | distance: Distance to the point 132 | 133 | Returns: 134 | Sequence of vehicle groups within range. 135 | """ 136 | return [x for x in self.vehicle_group if x.position.distance_to_point(point) < distance] 137 | 138 | def static_group_within(self, point, distance) -> List[Group]: 139 | """Return all static groups within the radius of a given point. 140 | 141 | Args: 142 | point(mapping.Point): Center of circle 143 | distance: Distance to the point 144 | 145 | Returns: 146 | Sequence of static groups within range. 147 | """ 148 | return [x for x in self.static_group if x.position.distance_to_point(point) < distance] 149 | 150 | def next_callsign_id(self): 151 | self.current_callsign_id += 1 152 | return self.current_callsign_id 153 | 154 | def next_callsign_category(self, category): 155 | if category not in self.current_callsign_category: 156 | self.current_callsign_category[category] = 0 157 | return self.callsign.get(category)[0] 158 | 159 | self.current_callsign_category[category] += 1 160 | if self.current_callsign_category[category] >= len(self.callsign[category]): 161 | self.current_callsign_category[category] = 0 162 | return self.callsign.get(category)[self.current_callsign_category[category]] 163 | 164 | @property 165 | def unused_onboard_numbers(self) -> Set[str]: 166 | return self._tail_numbers 167 | 168 | def reset_onboard_numbers(self): 169 | """ 170 | Resets/clears reserved onboard numbers for this country. 171 | :return: 172 | """ 173 | self._tail_numbers = set() 174 | 175 | def reserve_onboard_num(self, number: str) -> bool: 176 | """ 177 | Reserve the give onboard_num (tail number), if already used return True. 178 | :param int number: onboard num 179 | :return: True if number is already in use, else False 180 | """ 181 | is_in = number in self._tail_numbers 182 | self._tail_numbers.add(number) 183 | return is_in 184 | 185 | def next_onboard_num(self) -> str: 186 | free_set = {"{:03}".format(x) for x in range(10, 999)} - self._tail_numbers 187 | tailnum = free_set.pop() 188 | self.reserve_onboard_num(tailnum) 189 | return tailnum 190 | 191 | def dict(self): 192 | d = { 193 | "name": self.name, 194 | "id": self.id 195 | } 196 | 197 | if self.vehicle_group: 198 | d["vehicle"] = {"group": {}} 199 | i = 1 200 | for vgroup in self.vehicle_group: 201 | d["vehicle"]["group"][i] = vgroup.dict() 202 | i += 1 203 | 204 | if self.ship_group: 205 | d["ship"] = {"group": {}} 206 | i = 1 207 | for group in self.ship_group: 208 | d["ship"]["group"][i] = group.dict() 209 | i += 1 210 | 211 | if self.plane_group: 212 | d["plane"] = {"group": {}} 213 | i = 1 214 | for plane_group in self.plane_group: 215 | d["plane"]["group"][i] = plane_group.dict() 216 | i += 1 217 | 218 | if self.helicopter_group: 219 | d["helicopter"] = {"group": {}} 220 | i = 1 221 | for group in self.helicopter_group: 222 | d["helicopter"]["group"][i] = group.dict() 223 | i += 1 224 | 225 | if self.static_group: 226 | d["static"] = {"group": {}} 227 | i = 1 228 | for static_group in self.static_group: 229 | d["static"]["group"][i] = static_group.dict() 230 | i += 1 231 | return d 232 | 233 | def __eq__(self, other: object) -> bool: 234 | if not isinstance(other, Country): 235 | return False 236 | return self.id == other.id 237 | 238 | def __str__(self): 239 | return str(self.id) + "," + self.name + "," + str(self.vehicle_group) 240 | 241 | def __hash__(self) -> int: 242 | return hash(self.id) 243 | -------------------------------------------------------------------------------- /dcs/drawing/__init__.py: -------------------------------------------------------------------------------- 1 | from dcs.drawing.drawings import Drawings 2 | from dcs.drawing.layer import Layer 3 | from dcs.drawing.drawing import Drawing, LineStyle, Rgba 4 | from dcs.drawing.line import LineDrawing 5 | from dcs.drawing.polygon import ( 6 | PolygonDrawing, 7 | Circle, 8 | Oval, 9 | Rectangle, 10 | FreeFormPolygon, 11 | Arrow, 12 | ) 13 | -------------------------------------------------------------------------------- /dcs/drawing/drawing.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from dataclasses import dataclass 3 | import dcs.mapping as mapping 4 | 5 | 6 | @dataclass 7 | class Rgba: 8 | r: int 9 | g: int 10 | b: int 11 | a: int 12 | 13 | def to_color_string(self) -> str: 14 | return f"0x{int(self.r):02x}{int(self.g):02x}{int(self.b):02x}{int(self.a):02x}" 15 | 16 | @classmethod 17 | def from_color_string(cls, s: str): 18 | s = s.replace("#", "").replace("0x", "") 19 | rgba = tuple(int(s[i:i + 2], 16) for i in (0, 2, 4, 6)) 20 | return cls(rgba[0], rgba[1], rgba[2], rgba[3]) 21 | 22 | 23 | class LineStyle(Enum): 24 | Solid = "solid" 25 | Dot = "dot" 26 | Dash = "dash" 27 | Cross = "cross" 28 | Square = "square" 29 | Triangle = "triangle" 30 | Wirefence = "wirefence" 31 | Dot2 = "dot2" 32 | Solid2 = "solid2" 33 | DotDash = "dotdash" 34 | StrongPoint = "strongpoint" 35 | WireFence = "wirefence" 36 | Boundry1 = "boundry1" 37 | Boundry2 = "boundry2" 38 | Boundry3 = "boundry3" 39 | Boundry4 = "boundry4" 40 | Boundry5 = "boundry5" 41 | 42 | 43 | @dataclass 44 | class Drawing: 45 | visible: bool 46 | position: mapping.Point 47 | name: str 48 | color: Rgba 49 | layer_name: str 50 | 51 | def dict(self): 52 | d = {} 53 | d["visible"] = self.visible 54 | d["mapX"] = self.position.x 55 | d["mapY"] = self.position.y 56 | d["name"] = self.name 57 | d["colorString"] = self.color.to_color_string() 58 | d["layerName"] = self.layer_name 59 | return d 60 | 61 | def points_to_dict(self, points): 62 | d = {} 63 | i = 1 64 | for point in points: 65 | d[i] = {"x": point.x, "y": point.y} 66 | i += 1 67 | return d 68 | -------------------------------------------------------------------------------- /dcs/drawing/drawings.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import Enum 4 | from typing import TYPE_CHECKING, Any, Dict 5 | 6 | from dcs.drawing.layer import Layer 7 | from dcs.drawing.options import Options 8 | 9 | if TYPE_CHECKING: 10 | from dcs.terrain import Terrain 11 | 12 | 13 | class StandardLayer(Enum): 14 | Red = "Red" 15 | Blue = "Blue" 16 | Neutral = "Neutral" 17 | Common = "Common" 18 | Author = "Author" 19 | 20 | 21 | class Drawings: 22 | def __init__(self, terrain: Terrain) -> None: 23 | self._terrain = terrain 24 | self.options = Options() 25 | self.layers = [ 26 | Layer(True, StandardLayer.Red.value, [], terrain), 27 | Layer(True, StandardLayer.Blue.value, [], terrain), 28 | Layer(True, StandardLayer.Neutral.value, [], terrain), 29 | Layer(True, StandardLayer.Common.value, [], terrain), 30 | Layer(True, StandardLayer.Author.value, [], terrain), 31 | ] 32 | 33 | def load_from_dict(self, data: Dict[str, Any]) -> None: 34 | self.options.load_from_dict(data["options"]) 35 | self.layers = [] 36 | for layer_index in sorted(data["layers"].keys()): 37 | layer_data = data["layers"][layer_index] 38 | layer = Layer(True, "", [], self._terrain) 39 | layer.load_from_dict(layer_data) 40 | self.layers.append(layer) 41 | 42 | def dict(self): 43 | d = {} 44 | d["options"] = self.options.dict() 45 | 46 | d["layers"] = {} 47 | i = 1 48 | for layer in self.layers: 49 | d["layers"][i] = layer.dict() 50 | i += 1 51 | 52 | return d 53 | 54 | def get_layer_by_name(self, layer_name: str): 55 | for layer in self.layers: 56 | if layer.name == layer_name: 57 | return layer 58 | 59 | def get_layer(self, layer: StandardLayer): 60 | return self.get_layer_by_name(layer.value) 61 | -------------------------------------------------------------------------------- /dcs/drawing/icon.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | from dcs.drawing.drawing import Drawing 4 | 5 | 6 | class StandardIcon(Enum): 7 | Mechanized = "P91000001.png" 8 | MechanizedInfantryWithFightingVehicle = "P91000002.png" 9 | Recce = "P91000003.png" 10 | MechanizedInfantry = "P91000004.png" 11 | Logistics = "P91000005.png" 12 | MechanizedArtillery = "P91000006.png" 13 | MechanizedRocketArtillery = "P91000007.png" 14 | AirDefense = "P91000009.png" 15 | SearchRadar = "P91000010.png" 16 | 17 | 18 | @dataclass 19 | class Icon(Drawing): 20 | file: str 21 | scale: float 22 | angle: int 23 | 24 | def dict(self): 25 | d = super().dict() 26 | d["primitiveType"] = "Icon" 27 | d["file"] = self.file 28 | d["scale"] = self.scale 29 | d["angle"] = self.angle 30 | return d 31 | -------------------------------------------------------------------------------- /dcs/drawing/line.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | from typing import List 4 | 5 | from dcs.drawing.drawing import Drawing, LineStyle 6 | from dcs.mapping import Point 7 | 8 | 9 | class LineMode(Enum): 10 | Segment = "segment" 11 | Segments = "segments" 12 | Free = "free" 13 | 14 | 15 | @dataclass 16 | class LineDrawing(Drawing): 17 | closed: bool 18 | line_thickness: float 19 | line_style: LineStyle 20 | line_mode: LineMode 21 | points: List[Point] 22 | 23 | def dict(self): 24 | d = super().dict() 25 | d["primitiveType"] = "Line" 26 | d["closed"] = self.closed 27 | d["thickness"] = self.line_thickness 28 | d["style"] = self.line_style.value 29 | d["lineMode"] = self.line_mode.value 30 | d["points"] = super().points_to_dict(self.points) 31 | return d 32 | -------------------------------------------------------------------------------- /dcs/drawing/options.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | 4 | class Options: 5 | def __init__(self): 6 | self.hiddenOnF10Map = self.get_default_hidden() 7 | 8 | def load_from_dict(self, data): 9 | self.hiddenOnF10Map = data["hiddenOnF10Map"] 10 | 11 | def dict(self): 12 | d = {} 13 | d["hiddenOnF10Map"] = {} 14 | for option_group_name in self.hiddenOnF10Map.keys(): 15 | d["hiddenOnF10Map"][option_group_name] = self.hiddenOnF10Map[ 16 | option_group_name 17 | ] 18 | 19 | return d 20 | 21 | @staticmethod 22 | def get_default_hidden() -> Dict[str, Any]: 23 | d = {} 24 | d["Observer"] = { 25 | "Neutral": False, 26 | "Blue": False, 27 | "Red": False, 28 | } 29 | d["Instructor"] = { 30 | "Neutral": False, 31 | "Blue": False, 32 | "Red": False, 33 | } 34 | d["ForwardObserver"] = { 35 | "Neutral": False, 36 | "Blue": False, 37 | "Red": False, 38 | } 39 | d["Spectrator"] = { # Seems to be misspelled by DCS 40 | "Neutral": False, 41 | "Blue": False, 42 | "Red": False, 43 | } 44 | d["ArtilleryCommander"] = { 45 | "Neutral": False, 46 | "Blue": False, 47 | "Red": False, 48 | } 49 | d["Pilot"] = { 50 | "Neutral": False, 51 | "Blue": False, 52 | "Red": False, 53 | } 54 | return d 55 | -------------------------------------------------------------------------------- /dcs/drawing/polygon.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | from typing import List 4 | 5 | from dcs.drawing.drawing import Drawing, LineStyle, Rgba 6 | from dcs.mapping import Point 7 | from dcs.terrain import Terrain 8 | 9 | 10 | class PolygonMode(Enum): 11 | Circle = "circle" 12 | Oval = "oval" 13 | Rectangle = "rect" 14 | Free = "free" 15 | Arrow = "arrow" 16 | 17 | 18 | @dataclass 19 | class PolygonDrawing(Drawing): 20 | fill: Rgba 21 | line_thickness: float 22 | line_style: LineStyle 23 | 24 | def dict(self): 25 | d = super().dict() 26 | d["primitiveType"] = "Polygon" 27 | d["fillColorString"] = self.fill.to_color_string() 28 | d["thickness"] = self.line_thickness 29 | d["style"] = self.line_style.value 30 | return d 31 | 32 | 33 | @dataclass 34 | class Circle(PolygonDrawing): 35 | radius: float 36 | 37 | def dict(self): 38 | d = super().dict() 39 | d["polygonMode"] = PolygonMode.Circle.value 40 | d["radius"] = self.radius 41 | return d 42 | 43 | 44 | @dataclass 45 | class Oval(PolygonDrawing): 46 | radius1: float 47 | radius2: float 48 | angle: float 49 | 50 | def dict(self): 51 | d = super().dict() 52 | d["polygonMode"] = PolygonMode.Oval.value 53 | d["r1"] = self.radius1 54 | d["r2"] = self.radius2 55 | d["angle"] = self.angle 56 | return d 57 | 58 | 59 | @dataclass 60 | class Rectangle(PolygonDrawing): 61 | width: float 62 | height: float 63 | angle: float 64 | 65 | def dict(self): 66 | d = super().dict() 67 | d["polygonMode"] = PolygonMode.Rectangle.value 68 | d["width"] = self.width 69 | d["height"] = self.height 70 | d["angle"] = self.angle 71 | return d 72 | 73 | 74 | @dataclass 75 | class FreeFormPolygon(PolygonDrawing): 76 | points: List[Point] 77 | 78 | def dict(self): 79 | d = super().dict() 80 | d["polygonMode"] = PolygonMode.Free.value 81 | d["points"] = super().points_to_dict(self.points) 82 | return d 83 | 84 | 85 | @dataclass 86 | class Arrow(PolygonDrawing): 87 | length: float 88 | angle: float 89 | points: List[Point] 90 | 91 | def dict(self): 92 | d = super().dict() 93 | d["polygonMode"] = PolygonMode.Arrow.value 94 | d["length"] = self.length 95 | d["angle"] = self.angle 96 | d["points"] = super().points_to_dict(self.points) 97 | return d 98 | 99 | @staticmethod 100 | def get_default_arrow_points(terrain: Terrain) -> List[Point]: 101 | return [ 102 | Point(976.01054900139, 0, terrain), 103 | Point(976.01054900139, 5205.3895946741, terrain), 104 | Point(2602.694797337, 5205.3895946741, terrain), 105 | Point(0, 7808.0843920111, terrain), 106 | Point(-2602.694797337, 5205.3895946741, terrain), 107 | Point(-976.01054900139, 5205.3895946741, terrain), 108 | Point(-976.01054900139, 0, terrain), 109 | Point(976.01054900139, 0, terrain), 110 | ] 111 | -------------------------------------------------------------------------------- /dcs/drawing/text_box.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from dcs.drawing.drawing import Drawing, Rgba 3 | 4 | 5 | @dataclass() 6 | class TextBox(Drawing): 7 | text: str 8 | font_size: int 9 | font: str 10 | border_thickness: float 11 | fill: Rgba 12 | angle: float 13 | 14 | def dict(self): 15 | d = super().dict() 16 | d["primitiveType"] = "TextBox" 17 | d["text"] = self.text 18 | d["fontSize"] = self.font_size 19 | d["font"] = self.font 20 | d["borderThickness"] = self.border_thickness 21 | d["fillColorString"] = self.fill.to_color_string() 22 | d["angle"] = self.angle 23 | return d 24 | -------------------------------------------------------------------------------- /dcs/forcedoptions.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Optional 3 | 4 | 5 | class ForcedOptions: 6 | 7 | class Views(Enum): 8 | OnlyMap = "optview_onlymap" 9 | MyAircraft = "optview_myaircraft" 10 | Allies = "optview_allies" 11 | OnlyAllies = "optview_onlyallies" 12 | All = "optview_all" 13 | 14 | class CivilTraffic(Enum): 15 | Off = "" 16 | Low = "low" 17 | Medium = "medium" 18 | High = "high" 19 | 20 | class GEffect(Enum): 21 | None_ = "none" 22 | Game = "reduced" 23 | Realistic = "realistic" 24 | 25 | class Labels(Enum): 26 | None_ = 0 27 | Full = 1 28 | Abbreviate = 2 29 | DotOnly = 3 30 | NeutralDot = 4 31 | 32 | def __init__(self): 33 | # In the order they appear in the ME. 34 | self.permit_crash: Optional[bool] = None 35 | self.external_views: Optional[bool] = None 36 | self.options_view: Optional[ForcedOptions.Views] = None 37 | self.labels: Optional[ForcedOptions.Labels] = None 38 | self.easy_flight: Optional[bool] = None 39 | self.easy_radar: Optional[bool] = None 40 | self.immortal: Optional[bool] = None 41 | self.fuel: Optional[bool] = None 42 | self.weapons: Optional[bool] = None 43 | self.easy_communication: Optional[bool] = None 44 | self.radio: Optional[bool] = None 45 | self.unrestricted_satnav: Optional[bool] = None 46 | self.padlock: Optional[bool] = None 47 | self.wake_turbulence: Optional[bool] = None 48 | self.geffect: Optional[ForcedOptions.GEffect] = None 49 | self.accidental_failures = None 50 | self.mini_hud: Optional[bool] = None 51 | self.cockpit_visual_recon_mode: Optional[bool] = None 52 | self.user_marks: Optional[bool] = None 53 | self.civil_traffic: Optional[ForcedOptions.CivilTraffic] = None 54 | self.birds: Optional[int] = None 55 | self.cockpit_status_bar: Optional[bool] = None 56 | self.battle_damage_assessment: Optional[bool] = None 57 | 58 | def load_from_dict(self, d): 59 | self.fuel = d.get("fuel") 60 | self.easy_radar = d.get("easyRadar") 61 | self.mini_hud = d.get("miniHUD") 62 | self.accidental_failures = d.get("accidental_failures") 63 | if d.get("optionsView") is not None: 64 | self.options_view = ForcedOptions.Views(d["optionsView"]) 65 | self.permit_crash = d.get("permitCrash") 66 | self.immortal = d.get("immortal") 67 | self.easy_communication = d.get("easyCommunication") 68 | self.cockpit_visual_recon_mode = d.get("cockpitVisualRM") 69 | self.easy_flight = d.get("easyFlight") 70 | self.radio = d.get("radio") 71 | if d.get("geffect") is not None: 72 | self.geffect = ForcedOptions.GEffect(d["geffect"]) 73 | self.external_views = d.get("externalViews") 74 | self.birds = d.get("birds") 75 | if d.get("civTraffic") is not None: 76 | self.civil_traffic = ForcedOptions.CivilTraffic(d["civTraffic"]) 77 | self.weapons = d.get("weapons") 78 | self.padlock = d.get("padlock") 79 | if "labels" in d: 80 | self.labels = ForcedOptions.Labels(d["labels"]) 81 | self.unrestricted_satnav = d.get("unrestrictedSATNAV") 82 | self.wake_turbulence = d.get("wakeTurbulence") 83 | self.cockpit_status_bar = d.get("cockpitStatusBarAllowed") 84 | self.battle_damage_assessment = d.get("RBDAI") 85 | self.user_marks = d.get("userMarks") 86 | 87 | def dict(self): 88 | d = {} 89 | if self.fuel is not None: 90 | d["fuel"] = self.fuel 91 | if self.easy_radar is not None: 92 | d["easyRadar"] = self.easy_radar 93 | if self.mini_hud is not None: 94 | d["miniHUD"] = self.mini_hud 95 | if self.accidental_failures is not None: 96 | d["accidental_failures"] = self.accidental_failures 97 | if self.options_view is not None: 98 | d["optionsView"] = self.options_view.value 99 | if self.permit_crash is not None: 100 | d["permitCrash"] = self.permit_crash 101 | if self.immortal is not None: 102 | d["immortal"] = self.immortal 103 | if self.easy_communication is not None: 104 | d["easyCommunication"] = self.easy_communication 105 | if self.cockpit_visual_recon_mode is not None: 106 | d["cockpitVisualRM"] = self.cockpit_visual_recon_mode 107 | if self.easy_flight is not None: 108 | d["easyFlight"] = self.easy_flight 109 | if self.radio is not None: 110 | d["radio"] = self.radio 111 | if self.geffect is not None: 112 | d["geffect"] = self.geffect.value 113 | if self.external_views is not None: 114 | d["externalViews"] = self.external_views 115 | if self.birds is not None: 116 | d["birds"] = self.birds 117 | if self.civil_traffic is not None: 118 | d["civTraffic"] = self.civil_traffic.value 119 | if self.weapons is not None: 120 | d["weapons"] = self.weapons 121 | if self.padlock is not None: 122 | d["padlock"] = self.padlock 123 | if self.labels is not None: 124 | d["labels"] = self.labels.value 125 | if self.unrestricted_satnav is not None: 126 | d["unrestrictedSATNAV"] = self.unrestricted_satnav 127 | if self.wake_turbulence is not None: 128 | d["wakeTurbulence"] = self.wake_turbulence 129 | if self.cockpit_status_bar is not None: 130 | d["cockpitStatusBarAllowed"] = self.cockpit_status_bar 131 | if self.battle_damage_assessment is not None: 132 | d["RBDAI"] = self.battle_damage_assessment 133 | if self.user_marks is not None: 134 | d["userMarks"] = self.user_marks 135 | return d 136 | -------------------------------------------------------------------------------- /dcs/goals.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import dcs.condition as condition 3 | 4 | 5 | class Goal: 6 | def __init__(self, comment="", score=100): 7 | self.rules: List[condition.Condition] = [] 8 | self.side = "OFFLINE" 9 | self.score = score 10 | self.predicate = "score" 11 | self.comment = comment 12 | 13 | def load_from_dict(self, data, mission): 14 | self.side = data["side"] 15 | self.score = data["score"] 16 | self.predicate = data["predicate"] 17 | self.comment = data["comment"] 18 | self.rules = [] 19 | rules = data["rules"] 20 | for x in rules: 21 | gr = condition.condition_map[rules[x]["predicate"]].create_from_dict(rules[x], mission) 22 | self.rules.append(gr) 23 | 24 | def dict(self): 25 | return { 26 | "side": self.side, 27 | "score": self.score, 28 | "predicate": self.predicate, 29 | "comment": self.comment, 30 | "rules": {i + 1: self.rules[i].dict() for i in range(0, len(self.rules))} 31 | } 32 | 33 | 34 | class Goals: 35 | def __init__(self): 36 | self.goals = { 37 | "red": [], # type list[Goal] 38 | "blue": [], # type list[Goal] 39 | "offline": [] # type list[Goal] 40 | } 41 | 42 | def load_from_dict(self, data, mission): 43 | for x in data: 44 | g = Goal() 45 | g.load_from_dict(data[x], mission) 46 | self.goals[data[x]["side"].lower()].append(g) 47 | 48 | def add_red(self, g: Goal): 49 | g.side = "RED" 50 | self.goals["red"].append(g) 51 | 52 | def add_blue(self, g: Goal): 53 | g.side = "BLUE" 54 | self.goals["blue"].append(g) 55 | 56 | def add_offline(self, g: Goal): 57 | g.side = "OFFLINE" 58 | self.goals["offline"].append(g) 59 | 60 | def generate_result(self): 61 | d = { 62 | "total": len(self.goals["blue"]) + len(self.goals["red"]) + len(self.goals["offline"]), 63 | "blue": {}, 64 | "red": {}, 65 | "offline": {} 66 | } 67 | funcstr = "if mission.result.{side}.conditions[{idx}]() then mission.result.{side}.actions[{idx}]() end" 68 | for side in ["blue", "red", "offline"]: 69 | d[side]["conditions"] = {i + 1: condition.Condition.condition_str(self.goals[side][i].rules) 70 | for i in range(0, len(self.goals[side]))} 71 | d[side]["actions"] = {i + 1: "a_set_mission_result(" + str(self.goals[side][i].score) + ")" 72 | for i in range(0, len(self.goals[side])) if self.goals[side][i].rules} 73 | d[side]["func"] = { 74 | i + 1: funcstr.format(side=side, idx=i + 1) 75 | for i in range(0, len(self.goals[side])) if self.goals[side][i].rules} 76 | return d 77 | 78 | def dict(self): 79 | return {i + 1: self.goals[side][i].dict() 80 | for side in ["blue", "red", "offline"] for i in range(0, len(self.goals[side]))} 81 | -------------------------------------------------------------------------------- /dcs/groundcontrol.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import Enum 4 | from typing import Dict, Any, List, Tuple, Optional 5 | from dcs.action import Coalition 6 | from dcs.password import password_hash 7 | 8 | 9 | class GroundControlRole(Enum): 10 | GAME_MASTER = "instructor" 11 | TACTICAL_COMMANDER = "artillery_commander" 12 | JTAC = "forward_observer" 13 | OBSERVER = "observer" 14 | 15 | 16 | GroundControlPasswordsEntries = Dict[Coalition, Any] 17 | 18 | 19 | class GroundControlPasswords: 20 | def __init__(self, d: Optional[Dict[str, Any]] = None) -> None: 21 | self.game_masters: GroundControlPasswordsEntries = {} 22 | self.tactical_commander: GroundControlPasswordsEntries = {} 23 | self.jtac: GroundControlPasswordsEntries = {} 24 | self.observer: GroundControlPasswordsEntries = {} 25 | 26 | if d is not None: 27 | for coalition in [Coalition.Red, Coalition.Blue, Coalition.Neutral]: 28 | for miz_role, role_password in self.get_role_to_attr_mapping(): 29 | if miz_role.value in d and coalition.value in d[miz_role.value]: 30 | role_password[coalition] = d[miz_role.value][coalition.value] 31 | 32 | @classmethod 33 | def create_from_dict(cls, d: Dict[str, Any]) -> GroundControlPasswords: 34 | return GroundControlPasswords(d) 35 | 36 | def get_role_to_attr_mapping(self) -> List[Tuple[GroundControlRole, GroundControlPasswordsEntries]]: 37 | return [(GroundControlRole.GAME_MASTER, self.game_masters), 38 | (GroundControlRole.TACTICAL_COMMANDER, self.tactical_commander), 39 | (GroundControlRole.JTAC, self.jtac), 40 | (GroundControlRole.OBSERVER, self.observer)] 41 | 42 | def _find_role_password(self, role: GroundControlRole) -> GroundControlPasswordsEntries: 43 | for miz_role, role_password in self.get_role_to_attr_mapping(): 44 | if role == miz_role: 45 | return role_password 46 | raise RuntimeError(f"Unable to find passwords for {role.value} role") 47 | 48 | def lock(self, coalition: Coalition, role: GroundControlRole, password: str) -> None: 49 | role_password = self._find_role_password(role) 50 | role_password[coalition] = password_hash(password) 51 | 52 | def unlock(self, coalition: Coalition, role: GroundControlRole) -> None: 53 | role_password = self._find_role_password(role) 54 | role_password[coalition] = None 55 | 56 | def is_locked(self, coalition: Coalition, role: GroundControlRole) -> bool: 57 | role_password = self._find_role_password(role) 58 | return coalition in role_password and role_password[coalition] is not None 59 | 60 | def dict(self) -> Dict[str, Dict[str, str]]: 61 | d: Dict[str, Any] = {} 62 | for coalition in [Coalition.Red, Coalition.Blue, Coalition.Neutral]: 63 | for miz_role, role_password in self.get_role_to_attr_mapping(): 64 | if coalition in role_password and role_password[coalition] is not None: 65 | if miz_role.value not in d: 66 | d[miz_role.value] = {} 67 | d[miz_role.value][coalition.value] = role_password[coalition] 68 | return d 69 | 70 | 71 | class GroundControl: 72 | def __init__(self): 73 | self.pilot_can_control_vehicles = False 74 | self.red_game_masters = 0 75 | self.red_tactical_commander = 0 76 | self.red_jtac = 0 77 | self.red_observer = 0 78 | 79 | self.blue_game_masters = 0 80 | self.blue_tactical_commander = 0 81 | self.blue_jtac = 0 82 | self.blue_observer = 0 83 | 84 | self.neutrals_game_masters = 0 85 | self.neutrals_tactical_commander = 0 86 | self.neutrals_jtac = 0 87 | self.neutrals_observer = 0 88 | 89 | self.passwords = GroundControlPasswords() 90 | 91 | def load_from_dict(self, d): 92 | if d is None: 93 | return 94 | self.pilot_can_control_vehicles = d["isPilotControlVehicles"] 95 | 96 | self.red_game_masters = int(d["roles"]["instructor"]["red"]) 97 | self.red_tactical_commander = int(d["roles"]["artillery_commander"]["red"]) 98 | self.red_jtac = int(d["roles"]["forward_observer"]["red"]) 99 | self.red_observer = int(d["roles"]["observer"]["red"]) 100 | 101 | self.blue_game_masters = int(d["roles"]["instructor"]["blue"]) 102 | self.blue_tactical_commander = int(d["roles"]["artillery_commander"]["blue"]) 103 | self.blue_jtac = int(d["roles"]["forward_observer"]["blue"]) 104 | self.blue_observer = int(d["roles"]["observer"]["blue"]) 105 | 106 | self.neutrals_game_masters = int(d["roles"]["instructor"].get("neutrals", 0)) 107 | self.neutrals_tactical_commander = int(d["roles"]["artillery_commander"].get("neutrals", 0)) 108 | self.neutrals_jtac = int(d["roles"]["forward_observer"].get("neutrals", 0)) 109 | self.neutrals_observer = int(d["roles"]["observer"].get("neutrals", 0)) 110 | 111 | if "passwords" in d: 112 | self.passwords = GroundControlPasswords.create_from_dict(d["passwords"]) 113 | else: 114 | self.passwords = GroundControlPasswords() 115 | 116 | def lock(self, coalition: Coalition, role: GroundControlRole, password: str) -> None: 117 | self.passwords.lock(coalition, role, password) 118 | 119 | def unlock(self, coalition: Coalition, role: GroundControlRole) -> None: 120 | self.passwords.unlock(coalition, role) 121 | 122 | def is_locked(self, coalition: Coalition, role: GroundControlRole) -> bool: 123 | return self.passwords.is_locked(coalition, role) 124 | 125 | def dict(self): 126 | return { 127 | "isPilotControlVehicles": self.pilot_can_control_vehicles, 128 | "passwords": self.passwords.dict(), 129 | "roles": { 130 | "instructor": { 131 | "red": self.red_game_masters, 132 | "blue": self.blue_game_masters, 133 | "neutrals": self.neutrals_game_masters 134 | }, 135 | "artillery_commander": { 136 | "red": self.red_tactical_commander, 137 | "blue": self.blue_tactical_commander, 138 | "neutrals": self.neutrals_tactical_commander 139 | }, 140 | "forward_observer": { 141 | "red": self.red_jtac, 142 | "blue": self.blue_jtac, 143 | "neutrals": self.neutrals_jtac 144 | }, 145 | "observer": { 146 | "red": self.red_observer, 147 | "blue": self.blue_observer, 148 | "neutrals": self.neutrals_observer 149 | }, 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /dcs/installation.py: -------------------------------------------------------------------------------- 1 | """ 2 | This utility classes provides methods to check players installed DCS environment. 3 | 4 | TODO : add method 'is_using_open_beta', 'is_using_stable' 5 | TODO : [NICE to have] add method to check list of installed DCS modules 6 | (could be done either through windows registry, or through filesystem analysis ?) 7 | """ 8 | 9 | import os 10 | import re 11 | import sys 12 | from pathlib import Path 13 | from typing import List, Optional 14 | 15 | from dcs.winreg import read_current_user_value 16 | 17 | STEAM_REGISTRY_KEY_NAME = "Software\\Valve\\Steam" 18 | DCS_STABLE_REGISTRY_KEY_NAME = "Software\\Eagle Dynamics\\DCS World" 19 | DCS_BETA_REGISTRY_KEY_NAME = "Software\\Eagle Dynamics\\DCS World OpenBeta" 20 | 21 | 22 | def get_dcs_install_directory() -> str: 23 | """ 24 | Get the DCS World install directory for this computer 25 | :return DCS World install directory 26 | """ 27 | standalone_stable_path = read_current_user_value( 28 | DCS_STABLE_REGISTRY_KEY_NAME, "Path", Path 29 | ) 30 | if standalone_stable_path is not None: 31 | return f"{standalone_stable_path}{os.path.sep}" 32 | standalone_beta_path = read_current_user_value( 33 | DCS_BETA_REGISTRY_KEY_NAME, "Path", Path 34 | ) 35 | if standalone_beta_path is not None: 36 | return f"{standalone_beta_path}{os.path.sep}" 37 | steam_path = _dcs_steam_path() 38 | if steam_path is not None: 39 | return f"{steam_path}{os.path.sep}" 40 | 41 | print("Couldn't detect any installed DCS World version", file=sys.stderr) 42 | return "" 43 | 44 | 45 | def get_dcs_saved_games_directory(): 46 | """ 47 | Get the save game directory for DCS World 48 | :return: Save game directory as string 49 | """ 50 | saved_games = os.path.join(os.path.expanduser("~"), "Saved Games", "DCS") 51 | dcs_variant = os.path.join(get_dcs_install_directory(), "dcs_variant.txt") 52 | if os.path.exists(dcs_variant): 53 | # read from the file, append first line to saved games, e.g.: DCS.openbeta 54 | with open(dcs_variant, "r") as file: 55 | suffix = re.sub(r"[^\w\d-]", "", file.read()) 56 | saved_games = saved_games + "." + suffix 57 | return saved_games 58 | 59 | 60 | def _steam_library_directories() -> List[Path]: 61 | """ 62 | Get the installation directory for Steam games 63 | :return List of Steam library folders where games can be installed 64 | """ 65 | steam_dir = read_current_user_value(STEAM_REGISTRY_KEY_NAME, "SteamPath", Path) 66 | if steam_dir is None: 67 | return [] 68 | """ 69 | For reference here is what the vdf file is supposed to look like : 70 | 71 | "LibraryFolders" 72 | { 73 | "TimeNextStatsReport" "1561832478" 74 | "ContentStatsID" "-158337411110787451" 75 | "1" "D:\\Games\\Steam" 76 | "2" "E:\\Steam" 77 | } 78 | """ 79 | contents = (steam_dir / "steamapps/libraryfolders.vdf").read_text() 80 | return [ 81 | Path(line.split('"')[3]) 82 | for line in contents.splitlines()[1:] 83 | if ":\\\\" in line 84 | ] 85 | 86 | 87 | def _dcs_steam_path() -> Optional[Path]: 88 | """ 89 | Find the DCS install directory for DCS World Steam Edition 90 | :return: Install directory as string, empty string if not found 91 | """ 92 | for library_folder in _steam_library_directories(): 93 | folder = library_folder / "steamapps/common/DCSWorld" 94 | if folder.is_dir(): 95 | return folder 96 | return None 97 | 98 | 99 | if __name__ == "__main__": 100 | print("DCS Installation directory : " + get_dcs_install_directory()) 101 | print("DCS saved games directory : " + get_dcs_saved_games_directory()) 102 | -------------------------------------------------------------------------------- /dcs/liveries/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/dcs/liveries/__init__.py -------------------------------------------------------------------------------- /dcs/liveries/liberation.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import Tuple 4 | 5 | 6 | def read_liberation_preferences() -> Tuple[str, str]: 7 | # TODO: Remove Liberation-specific behavior. 8 | # This is only necessary because it doesn't appear to be possible to 100% reliably 9 | # guess the DCS install or user directory. Liberation solves this by just asking the 10 | # user on first run. That's a problem for every pydcs user, not just Liberation. 11 | # This should go away in favor of making dcs.installation *guess* at the paths, but 12 | # with an API to override from the app side (similar to PayloadDirectories APIs). 13 | install = "" 14 | saved_games = "" 15 | pref_path = os.path.join( 16 | os.path.expanduser("~"), "AppData", "Local", "DCSLiberation" 17 | ) 18 | pref_path = os.path.join(pref_path, "liberation_preferences.json") 19 | if os.path.exists(pref_path): 20 | with open(pref_path, "r") as file: 21 | try: 22 | json_dict = json.load(file) 23 | if "dcs_install_dir" in json_dict: 24 | install = json_dict["dcs_install_dir"] 25 | if "saved_game_dir" in json_dict: 26 | saved_games = json_dict["saved_game_dir"] 27 | except (KeyError, ValueError): 28 | # ValueError for decode errors (if the file is corrupted), KeyError in 29 | # case the format isn't what we expect. 30 | return "", "" 31 | return install, saved_games 32 | -------------------------------------------------------------------------------- /dcs/liveries/livery.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os.path 5 | from typing import Optional, Set 6 | from zipfile import ZipFile 7 | 8 | import dcs.lua 9 | 10 | 11 | def _attempt_read_from_filestream(filestream: bytes) -> Optional[str]: 12 | encodes = ["utf-8", "ansi"] 13 | for enc in encodes: 14 | try: 15 | code = filestream.decode(enc) 16 | except UnicodeDecodeError: 17 | continue 18 | return code 19 | return None 20 | 21 | 22 | class Livery: 23 | def __init__( 24 | self, path_id: str, name: str, order: int, countries: Optional[Set[str]] 25 | ) -> None: 26 | # ID to be used in the miz. 27 | self.id = path_id 28 | # Display name. 29 | self.name = name 30 | # UI sort order. 31 | self.order = order 32 | # List of countries that may use this livery. If None, all countries may use the 33 | # livery. The elements in this list are short names for countries, e.g. "USA" 34 | # or "RUS". 35 | self.countries = countries 36 | 37 | def __str__(self) -> str: 38 | return self.name 39 | 40 | def __repr__(self) -> str: 41 | return self.name 42 | 43 | def __lt__(self, other) -> bool: 44 | if self.order == other.order: 45 | return self.name < other.name 46 | if self.order is None: 47 | return True 48 | return False if other.order is None else self.order < other.order 49 | 50 | def __eq__(self, other) -> bool: 51 | return self.id == other.id 52 | 53 | def __hash__(self) -> int: 54 | return hash(self.id) 55 | 56 | def valid_for_country(self, country: str) -> bool: 57 | return self.countries is None or country in self.countries 58 | 59 | @staticmethod 60 | def from_lua(code: str, path: str) -> Optional[Livery]: 61 | """ 62 | Scans the given code (expecting contents from a description.lua file) 63 | to extract the name of the livery together with the countries for which the livery is applicable. 64 | If no name is found, it defaults to the folder-name like DCS would. 65 | If no countries are found, it means the livery is valid for all countries. 66 | Finally we also attempt to extract the order to sort liveries like DCS. 67 | If no order is found, we use a default value of 0. 68 | 69 | :param luacode: The contents of description.lua for the livery in question 70 | :param path: The path of the livery in question 71 | :param unit: The name of the unit as 'DCS type', 72 | i.e. name of the unit inside the Liveries folder, e.g. 'A-10CII' 73 | """ 74 | path_id = path.split("\\")[-1].replace(".zip", "") 75 | if path_id == "placeholder": 76 | return None 77 | 78 | # Some liveries files use variables in material names. Since the files read by 79 | # the scanner can be arbitrarily changed by a game update (we don't want some 80 | # liveries to be suddenly unparseable), and so far we aren't interested in the 81 | # values of liveries that rely on undefined variables, just assume those are all 82 | # the empty string and move on. 83 | try: 84 | data = dcs.lua.loads(code, unknown_variable_lookup=lambda _: "") 85 | except SyntaxError: 86 | logging.exception("Could not parse livery definition at %s", path) 87 | return None 88 | livery_name = data.get("name", path_id) 89 | countries_table = data.get("countries") 90 | if countries_table is None: 91 | countries = None 92 | else: 93 | countries = set(countries_table.values()) 94 | order = data.get("order", 0) 95 | 96 | order = None if path_id == "default" else order 97 | if order is not None and not isinstance(order, int): 98 | try: 99 | order = int(order) 100 | except ValueError: 101 | order = 0 102 | 103 | return Livery(path_id.lower(), livery_name, order, countries) 104 | 105 | @staticmethod 106 | def from_path(path: str) -> Optional[Livery]: 107 | if os.path.isdir(path): 108 | return Livery.from_directory(path) 109 | elif path.endswith(".zip"): 110 | return Livery.from_zip(path) 111 | return None 112 | 113 | @staticmethod 114 | def from_directory(path: str) -> Optional[Livery]: 115 | """ 116 | Verifies a description file exists and reads its contents, 117 | passing it to 'scan_lua_code'. 118 | 119 | :param path: The path of the livery in question 120 | :param unit: The name of the unit as 'DCS type', 121 | i.e. name of the unit inside the Liveries folder, e.g. 'A-10CII' 122 | """ 123 | description_path = os.path.join(path, "description.lua") 124 | if not os.path.exists(description_path): 125 | return None 126 | 127 | # Known encodings used for description.lua files 128 | with open(description_path, "rb") as file: 129 | code = _attempt_read_from_filestream(file.read()) 130 | 131 | if code is None: 132 | logging.warning(f" Unknown encoding found in '{description_path}'") 133 | return None 134 | 135 | try: 136 | return Livery.from_lua(code, path) 137 | except (IndexError, SyntaxError): 138 | logging.getLogger("pydcs").exception( 139 | "Failed to parse Lua code in %s", description_path 140 | ) 141 | print("Failed to parse Lua code in {}".format(description_path)) 142 | raise 143 | 144 | @staticmethod 145 | def from_zip(path: str) -> Optional[Livery]: 146 | """ 147 | Some liveries are zipped, this does the same as 'scan_lua_description', 148 | except for the fact it needs to "open the zip" first. 149 | 150 | :param path: The path of the livery in question 151 | :param unit: The name of the unit as 'DCS type', 152 | i.e. name of the unit inside the Liveries folder, e.g. 'A-10CII' 153 | """ 154 | if not os.path.exists(path): 155 | return None 156 | with ZipFile(path, "r") as zf: 157 | try: 158 | with zf.open("description.lua", "r") as file: 159 | code = _attempt_read_from_filestream(file.read()) 160 | except KeyError: 161 | return None 162 | 163 | if code is None: 164 | logging.warning(f" Unknown encoding found in '{path}/description.lua'") 165 | return None 166 | 167 | try: 168 | return Livery.from_lua(code, path) 169 | except (SyntaxError, IndexError): 170 | error_path = "!".join([path, "description.lua"]) 171 | logging.getLogger("pydcs").exception( 172 | "Failed to parse Lua code in %s", error_path 173 | ) 174 | raise 175 | -------------------------------------------------------------------------------- /dcs/liveries/liverycache.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional 2 | 3 | from .liveryscanner import LiveryScanner 4 | from .liveryset import LiverySet 5 | 6 | 7 | class LiveryCache: 8 | _cache: Optional[Dict[str, LiverySet]] = None 9 | 10 | @classmethod 11 | def cache(cls) -> Dict[str, LiverySet]: 12 | if cls._cache is None: 13 | cls._cache = LiveryScanner().load() 14 | return cls._cache 15 | 16 | @classmethod 17 | def for_unit(cls, livery_id: str) -> LiverySet: 18 | try: 19 | return LiveryCache.cache()[livery_id] 20 | except KeyError: 21 | return LiverySet(livery_id) 22 | 23 | @classmethod 24 | def __getitem__(cls, livery_id: str) -> LiverySet: 25 | return cls.for_unit(livery_id) 26 | -------------------------------------------------------------------------------- /dcs/liveries/liveryscanner.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict 3 | 4 | from dcs.installation import get_dcs_install_directory, get_dcs_saved_games_directory 5 | 6 | from .liberation import read_liberation_preferences 7 | from .livery import Livery 8 | from .liveryset import LiverySet 9 | 10 | campaign_livery_aliases = { # Known aliases in campaign liveries 11 | "FW-190D-9": "FW-190D9", # The Big Show 12 | } 13 | 14 | skip_units = { # Known obsolete units in specific locations 15 | "leopard-2": "Bazar", 16 | "Zil_135l": "Bazar", 17 | } 18 | 19 | 20 | class LiveryScanner: 21 | """ 22 | Class containing a map of all units with their respective liveries. 23 | Each livery has a set of countries to indicate applicability 24 | """ 25 | 26 | def __init__(self) -> None: 27 | """ 28 | Constructor only attempts to initialize if 'map' is empty. 29 | The first attempt to determine paths for initialization will look 30 | for Liberation's preferences file, as this gives us a way to initialize 31 | the scanner on time in Liberation. If proper initialization isn't done before 32 | importing modules that make use of this scanner, for example planes.py, we risk 33 | having those modules initialized without the proper liveries. 34 | If no preferences file is found, PyDCS will attempt to determine the correct paths instead. 35 | You can also initialize manually by calling 'Liveries.initialize(...)', 36 | but beware that this must happen on time in case of designs like planes.py or helicopters.py. 37 | """ 38 | self.map: Dict[str, LiverySet] = {} 39 | 40 | def load(self) -> Dict[str, LiverySet]: 41 | install, saved_games = read_liberation_preferences() 42 | return self.load_from(install, saved_games) 43 | 44 | def load_from( 45 | self, dcs_install_path: str, dcs_saved_games_path: str 46 | ) -> Dict[str, LiverySet]: 47 | self.scan_dcs_installation(dcs_install_path) 48 | self.scan_custom_liveries(dcs_saved_games_path) 49 | return self.map 50 | 51 | def register_livery(self, unit: str, livery: Livery) -> None: 52 | self.map[unit].add(livery) 53 | 54 | def scan_liveries(self, path: str, campaign_path: bool = False) -> None: 55 | """ 56 | Scans liveries for all units in the given path. 57 | 58 | :param path: A 'Liveries' path containing one or more units 59 | :param campaign_path: A boolean value indicating whether the path 60 | is special livery path for 3rd party campaigns. This is important 61 | because in some cases aliases are used for certain units. This would 62 | result in separate entries in the Liveries map, which is not good. 63 | """ 64 | if not os.path.exists(path): 65 | return 66 | for unit in os.listdir(path): 67 | if unit in skip_units and skip_units[unit] in path: 68 | continue 69 | liveries_path = os.path.join(path, unit) 70 | # The unit's name for liveries is NOT case-sensitive, 71 | # e.g.: 'Saved Games/Liveries/f-15c' vs 'DCS-install/Bazar/Liveries/F-15C 72 | # thus convert 'unit' to upper/lower to make sure everything "merges properly" 73 | unit = unit.upper() 74 | if "COCKPIT" in unit or not os.path.isdir(liveries_path): 75 | # Some custom mods put their cockpit liveries in the same directory, 76 | # for the time being we don't want to load those. 77 | # Other than that we're looking exclusively for directories. 78 | continue 79 | if campaign_path and unit in campaign_livery_aliases: 80 | unit = campaign_livery_aliases[unit] 81 | if unit not in self.map: 82 | self.map[unit] = LiverySet(unit) 83 | for livery_name in os.listdir(liveries_path): 84 | livery = Livery.from_path(os.path.join(liveries_path, livery_name)) 85 | if livery is not None: 86 | self.register_livery(unit, livery) 87 | 88 | def scan_mods_path(self, path: str) -> None: 89 | """ 90 | Scans all liveries for a certain "Mod". 91 | 92 | :param path: A path to a "Mod", e.g. "CoreMods", custom "Mods" in saved games, etc. 93 | """ 94 | if not os.path.exists(path): 95 | return 96 | for unit in os.listdir(path): 97 | liveries_path = os.path.join(path, unit, "Liveries") 98 | if os.path.exists(liveries_path): 99 | self.scan_liveries(liveries_path) 100 | 101 | def scan_campaign_liveries(self, path: str) -> None: 102 | """ 103 | Scans all extra liveries from campaigns. 104 | 105 | :param path: The path to 'DCS-installation/Mods/campaigns'. 106 | """ 107 | if not os.path.exists(path): 108 | return 109 | for campaign in os.listdir(path): 110 | liveries_path = os.path.join(path, campaign, "Liveries") 111 | if os.path.exists(liveries_path): 112 | self.scan_liveries(liveries_path, campaign_path=True) 113 | 114 | def scan_dcs_installation(self, install: str = ""): 115 | """ 116 | Scans all liveries in DCS' installation folder 117 | 118 | :param install: Path to DCS' installation folder, 119 | if an empty string or invalid path was given PyDCS will attempt to determine this. 120 | """ 121 | root = install 122 | if root == "" or not os.path.isdir(root): 123 | root = get_dcs_install_directory() 124 | 125 | path1 = os.path.join(root, "CoreMods", "aircraft") 126 | path2 = os.path.join(root, "CoreMods", "WWII Units") 127 | path3 = os.path.join(root, "Bazar", "Liveries") 128 | path4 = os.path.join(root, "Mods", "campaigns") 129 | path5 = os.path.join(root, "CoreMods", "tech") 130 | path6 = os.path.join(root, "Mods", "tech", "WWII Units", "Liveries") 131 | 132 | self.scan_mods_path(path1) 133 | self.scan_mods_path(path2) 134 | self.scan_liveries(path3) 135 | self.scan_campaign_liveries(path4) 136 | self.scan_mods_path(path5) 137 | self.scan_liveries(path6) 138 | 139 | def scan_custom_liveries(self, saved_games: str = ""): 140 | """ 141 | Scans all custom liveries & liveries of aircraft mods. 142 | 143 | :param saved_games: Path to Saved Games folder, 144 | if an empty string or invalid path was given PyDCS will attempt to determine this. 145 | """ 146 | root = saved_games 147 | if root == "" or not os.path.isdir(root): 148 | root = get_dcs_saved_games_directory() 149 | 150 | path1 = os.path.join(root, "Liveries") 151 | path2 = os.path.join(root, "Mods", "aircraft") 152 | 153 | self.scan_liveries(path1) 154 | self.scan_mods_path(path2) 155 | -------------------------------------------------------------------------------- /dcs/liveries/liveryset.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Set 2 | 3 | from .livery import Livery 4 | 5 | 6 | class LiverySet(Set[Livery]): 7 | def __init__(self, unit_livery_id: Optional[str] = None) -> None: 8 | super().__init__() 9 | self.unit_livery_id = unit_livery_id 10 | 11 | def __str__(self) -> str: 12 | return f"{self.unit_livery_id} => {super().__str__()}" 13 | 14 | def add(self, element: Livery) -> None: 15 | if isinstance(element, Livery): 16 | super().add(element) 17 | else: 18 | raise TypeError(f"{element} is not a Livery.") 19 | -------------------------------------------------------------------------------- /dcs/lua/__init__.py: -------------------------------------------------------------------------------- 1 | # lua table serialization 2 | 3 | from dcs.lua.parse import loads 4 | from dcs.lua.serialize import dumps 5 | -------------------------------------------------------------------------------- /dcs/lua/serialize.py: -------------------------------------------------------------------------------- 1 | def dumps(value, varname=None, indent=None): 2 | nl = "\n" if indent else "" 3 | s = varname + '=' + nl if varname else '' 4 | indentcount = 0 if indent is None else indent 5 | 6 | if isinstance(value, dict): 7 | e = [] 8 | areAllKeysInts = all(isinstance(k, int) for k in value.keys()) 9 | dictionaryKeys = value.keys() if areAllKeysInts else sorted(value.keys(), key=str) 10 | for key in dictionaryKeys: 11 | child = value[key] 12 | KNL = "\n" if indent and isinstance(child, (dict, list)) else "" 13 | selem = '\t' * indentcount 14 | skey = key if isinstance(key, int) else '"{key}"'.format(key=key) 15 | selem += '[{key}]={nl}'.format(key=skey, nl=KNL) 16 | selem += dumps(value[key], indent=indent + 1 if indent else None) 17 | e.append(selem) 18 | s += '\t' * (indent - 1) if nl else "" 19 | s += "{" 20 | if e: 21 | s += nl + ",{nl}".format(nl=nl).join(e) 22 | s += nl + '\t' * (indentcount - 1) + "}" 23 | elif isinstance(value, list): 24 | e = [] 25 | i = 1 26 | for v in value: 27 | selem = '\t' * indentcount + "[{i}]=".format(i=i) 28 | selem += dumps(v, indent=indent + 1 if indent else None) 29 | e.append(selem) 30 | i += 1 31 | s += '\t' * (indent - 1) if nl else "" 32 | s += "{" 33 | if e: 34 | s += nl + ",{nl}".format(nl=nl).join(e) 35 | s += nl + '\t' * (indentcount - 1) + "}" 36 | elif isinstance(value, str): 37 | v = value.replace('\\', '\\\\') 38 | v = v.replace('"', '\\"') 39 | v = v.replace('\n', '\\\n') 40 | s += '"{val}"'.format(val=v) 41 | elif isinstance(value, bool): 42 | s += "true" if value else "false" 43 | elif value is None: 44 | s += "nil" 45 | else: 46 | s += str(value) 47 | 48 | return s 49 | -------------------------------------------------------------------------------- /dcs/nav_target_point.py: -------------------------------------------------------------------------------- 1 | """ 2 | A nav target point is special type of point handled in DCS mission editor. 3 | 4 | Two aircraft uses these special waypoints for now : 5 | - The JF-17 Thunder 6 | - The F-14B Tomcat 7 | 8 | 9 | -------------------------------------------------------------------------- 10 | JF-17 : 11 | For the JF-17 it is possible to set up "PP" waypoint and "RP" waypoint. 12 | 13 | PP waypoints can be set by putting a comment : PP1, PP2, PP3, PP4, or PP5 14 | RP waypoints can be set by putting a comment : RP1, RP2, RP3, RP4, or RP5 15 | 16 | PP waypoints are preplanned target waypoints for guided bombs. 17 | RP waypoints are special points for the C-802AKG cruise missile. 18 | 19 | -------------------------------------------------------------------------- 20 | F-14B Tomcat : 21 | 22 | See http://www.heatblur.se/F-14Manual/dcs.html#f-14-waypoints-in-the-mission-editor 23 | 24 | -------------------------------------------------------------------------- 25 | 26 | """ 27 | 28 | from __future__ import annotations 29 | import copy 30 | 31 | from typing import TYPE_CHECKING, Any, Dict 32 | from dcs import mapping 33 | 34 | if TYPE_CHECKING: 35 | from dcs.terrain.terrain import Terrain 36 | 37 | 38 | class NavTargetPoint: 39 | def __init__(self, position: mapping.Point) -> None: 40 | self.position = copy.copy(position) 41 | self.text_comment = "" 42 | self.index = 0 43 | 44 | @classmethod 45 | def create_from_dict(cls, d: Dict[str, Any], terrain: Terrain) -> NavTargetPoint: 46 | t = cls(mapping.Point(d["x"], d["y"], terrain)) 47 | t.text_comment = d["text_comment"] 48 | t.index = d["index"] 49 | return t 50 | 51 | def dict(self): 52 | return { 53 | "x": self.position.x, 54 | "y": self.position.y, 55 | "index": self.index, 56 | "text_comment": self.text_comment, 57 | } 58 | -------------------------------------------------------------------------------- /dcs/password.py: -------------------------------------------------------------------------------- 1 | """ 2 | The password module generates DCS compliant passwords from input string 3 | """ 4 | 5 | import hashlib 6 | import base64 7 | import string 8 | import random 9 | 10 | 11 | def password_hash(password: str) -> str: 12 | # see https://www.reddit.com/r/hoggit/comments/uf2sh0/psa_creating_the_new_slot_passwords_outside_of_dcs/ 13 | 14 | # encode from https://github.com/simonwhitaker/base64url 15 | def encode(b: bytes, trim: bool = False, break_at: int = 0) -> str: 16 | encoded_string = base64.urlsafe_b64encode(b).decode("utf-8") 17 | if trim: 18 | encoded_string = encoded_string.rstrip("=") 19 | 20 | if break_at > 0: 21 | i = 0 22 | result = "" 23 | while i < len(encoded_string): 24 | result += encoded_string[i: i + break_at] + "\n" 25 | i += break_at 26 | return result 27 | else: 28 | return encoded_string 29 | 30 | SALT_SIZE = 11 31 | DIGEST_SIZE = 32 32 | 33 | key = ''.join(random.sample(string.digits + string.ascii_letters, SALT_SIZE)) 34 | p_hash = hashlib.blake2b(key=key.encode(), digest_size=DIGEST_SIZE) 35 | p_hash.update(password.encode()) 36 | b64url_hash = encode(p_hash.digest(), trim=True) 37 | 38 | return key + ":" + b64url_hash 39 | -------------------------------------------------------------------------------- /dcs/payloads.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Iterator, List, Optional 3 | 4 | import dcs.installation as installation 5 | 6 | 7 | def _find_dcs_payload_paths() -> List[Path]: 8 | dcs_dir = Path(installation.get_dcs_install_directory()) 9 | dcs_aircraft_dir = dcs_dir / "CoreMods" / "aircraft" 10 | if not dcs_aircraft_dir.exists(): 11 | return [] 12 | 13 | payload_dirs = [dcs_dir / "MissionEditor" / "data" / "scripts" / "UnitPayloads"] 14 | for entry in dcs_aircraft_dir.iterdir(): 15 | airframe_specific = entry / "UnitPayloads" 16 | if airframe_specific.exists(): 17 | payload_dirs.append(airframe_specific) 18 | 19 | return payload_dirs 20 | 21 | 22 | def _find_mod_payload_paths() -> List[Path]: 23 | user_dir = Path(installation.get_dcs_saved_games_directory()) 24 | mod_aircraft_dir = user_dir / "Mods" / "Aircraft" 25 | if not mod_aircraft_dir.exists(): 26 | return [] 27 | 28 | payload_dirs = [] 29 | for entry in mod_aircraft_dir.iterdir(): 30 | airframe_specific = entry / "UnitPayloads" 31 | if airframe_specific.exists(): 32 | payload_dirs.append(airframe_specific) 33 | 34 | return payload_dirs 35 | 36 | 37 | class PayloadDirectories: 38 | fallback: Optional[Path] = None 39 | _dcs: Optional[List[Path]] = None 40 | _mod: Optional[List[Path]] = None 41 | _user: Optional[Path] = None 42 | preferred: Optional[Path] = None 43 | 44 | @classmethod 45 | def dcs(cls) -> List[Path]: 46 | if cls._dcs is None: 47 | cls._dcs = _find_dcs_payload_paths() 48 | return cls._dcs 49 | 50 | @classmethod 51 | def mod(cls) -> List[Path]: 52 | if cls._mod is None: 53 | cls._mod = _find_mod_payload_paths() 54 | return cls._mod 55 | 56 | @classmethod 57 | def user(cls) -> Path: 58 | if cls._user is None: 59 | cls._user = ( 60 | Path(installation.get_dcs_saved_games_directory()) 61 | / "MissionEditor" 62 | / "UnitPayloads" 63 | ) 64 | return cls._user 65 | 66 | @classmethod 67 | def set_fallback(cls, path: Path) -> None: 68 | cls.fallback = path 69 | 70 | @classmethod 71 | def set_preferred(cls, path: Path) -> None: 72 | cls.preferred = path 73 | 74 | @classmethod 75 | def payload_dirs(cls) -> Iterator[Path]: 76 | # These are iterated in order of decreasing order of preference. 77 | if cls.preferred is not None: 78 | yield cls.preferred 79 | yield cls.user() 80 | yield from cls.dcs() 81 | yield from cls.mod() 82 | if cls.fallback: 83 | yield cls.fallback 84 | -------------------------------------------------------------------------------- /dcs/point.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | import dcs.task as task 4 | import dcs.mapping as mapping 5 | from typing import Any, Dict, List, Optional 6 | from enum import Enum 7 | 8 | 9 | class PointAction(Enum): 10 | None_ = "" 11 | TurningPoint = "Turning Point" 12 | FlyOverPoint = "Fly Over Point" 13 | FromParkingArea = "From Parking Area" 14 | FromParkingAreaHot = "From Parking Area Hot" 15 | FromRunway = "From Runway" 16 | Landing = "Landing" 17 | OnRoad = "On Road" 18 | OffRoad = "Off Road" 19 | LineAbreast = "Rank" 20 | Cone = "Cone" 21 | Vee = "Vee" 22 | Diamond = "Diamond" 23 | EchelonLeft = "EchelonL" 24 | EchelonRight = "EchelonR" 25 | FromGroundArea = "From Ground Area" 26 | FromGroundAreaHot = "From Ground Area Hot" 27 | LandingReFuAr = "LandingReFuAr" 28 | Custom = "Custom" 29 | OnRailroads = "On Railroads" 30 | 31 | 32 | class StaticPoint: 33 | def __init__(self, position: mapping.Point) -> None: 34 | self.position = copy.copy(position) 35 | self.alt = 0 36 | self.type = "" 37 | self.name: str = "" 38 | self.speed = 0.0 39 | self.formation_template = "" 40 | self.action = PointAction.None_ # type: PointAction 41 | self.landing_refuel_rearm_time: Optional[int] = None 42 | 43 | def load_from_dict(self, d: Dict[str, Any], translation) -> None: 44 | self.alt = d["alt"] 45 | self.type = d["type"] 46 | self.position.x = d["x"] 47 | self.position.y = d["y"] 48 | self.action = PointAction(d["action"]) 49 | self.formation_template = d["formation_template"] 50 | self.speed = d["speed"] 51 | if "name" in d: 52 | if d["name"].startswith("DictKey_Translation_"): 53 | self.name = str(translation.get_string(d["name"])) 54 | else: 55 | self.name = d["name"] 56 | try: 57 | self.landing_refuel_rearm_time = int(d["timeReFuAr"]) 58 | except KeyError: 59 | self.landing_refuel_rearm_time = None 60 | 61 | def dict(self) -> Dict[str, Any]: 62 | if not isinstance(self.name, str): 63 | raise TypeError("Point name expected to be `str`") 64 | d = { 65 | "alt": self.alt, 66 | "type": self.type, 67 | "name": self.name, 68 | "x": self.position.x, 69 | "y": self.position.y, 70 | "speed": round(self.speed, 13), 71 | "formation_template": self.formation_template, 72 | "action": self.action.value 73 | } 74 | 75 | if self.landing_refuel_rearm_time is not None: 76 | d["timeReFuAr"] = self.landing_refuel_rearm_time 77 | 78 | return d 79 | 80 | 81 | class VNav(Enum): 82 | V2D = 0 83 | V3D = 1 84 | VNone = 2 85 | 86 | 87 | class Scale(Enum): 88 | Enroute = 0 89 | Terminal = 1 90 | Approach = 2 91 | HighAcc = 3 92 | None_ = 4 93 | 94 | 95 | class Steer(Enum): 96 | ToFrom = 0 97 | Direct = 1 98 | ToTo = 2 99 | None_ = 3 100 | 101 | 102 | class PointProperties: 103 | def __init__(self, vnav: VNav = VNav.V2D, scale: Scale = Scale.Enroute, steer: Steer = Steer.ToTo, angle=None): 104 | self.vnav = vnav 105 | self.scale = scale 106 | self.steer = steer 107 | if angle is not None: 108 | self.angle = 1 109 | self.vangle = angle 110 | else: 111 | self.angle = 0 112 | self.vangle = 0 113 | 114 | def load_from_dict(self, d): 115 | self.vnav = VNav(d.get("vnav", VNav.VNone.value)) 116 | self.scale = Scale(d.get("scale", Scale.None_.value)) 117 | self.steer = Steer(d.get("steer", Steer.None_.value)) 118 | self.angle = d.get("angle", 0) 119 | self.vangle = d.get("vangle", 0) 120 | 121 | def dict(self): 122 | return { 123 | "vnav": self.vnav.value, 124 | "scale": self.scale.value, 125 | "angle": self.angle, 126 | "vangle": self.vangle, 127 | "steer": self.steer.value 128 | } 129 | 130 | 131 | class MovingPoint(StaticPoint): 132 | def __init__(self, position: mapping.Point) -> None: 133 | super().__init__(position) 134 | self.type = "Turning Point" 135 | self.action: PointAction = PointAction.TurningPoint 136 | self.alt_type = "BARO" 137 | self.ETA = 0 138 | self.ETA_locked = True 139 | self.speed_locked = True 140 | self.tasks: List[task.Task] = [] 141 | self.properties: Optional[PointProperties] = None 142 | self.airdrome_id: Optional[int] = None 143 | self.helipad_id = None 144 | self.link_unit = None 145 | 146 | def load_from_dict(self, d, translation): 147 | super(MovingPoint, self).load_from_dict(d, translation) 148 | self.alt_type = d.get("alt_type", None) 149 | self.ETA_locked = d["ETA_locked"] 150 | self.ETA = d["ETA"] 151 | self.speed_locked = d["speed_locked"] 152 | if d.get("task") is not None: 153 | task_keys = sorted(d["task"]["params"]["tasks"].keys()) 154 | for t in task_keys: 155 | self.tasks.append(task._create_from_dict(d["task"]["params"]["tasks"][t])) 156 | self.airdrome_id = d.get("airdromeId", None) 157 | self.helipad_id = d.get("helipadId", None) 158 | self.link_unit = d.get("linkUnit", None) 159 | if d.get("properties"): 160 | self.properties = PointProperties() 161 | self.properties.load_from_dict(d.get("properties")) 162 | else: 163 | self.properties = None 164 | 165 | def find_task(self, task_type): 166 | """Searches tasks in this point for the given task class 167 | 168 | :param task_type: task class to search, :py:mod:`dcs.task` 169 | :return: task instance if found, else None 170 | """ 171 | for t in self.tasks: 172 | if isinstance(t, task_type): 173 | return t 174 | return None 175 | 176 | def add_task(self, task_: task.Task): 177 | self.tasks.append(task_) 178 | 179 | def dict(self): 180 | d = super(MovingPoint, self).dict() 181 | d["alt_type"] = self.alt_type 182 | d["ETA"] = self.ETA 183 | d["ETA_locked"] = self.ETA_locked 184 | d["speed_locked"] = self.speed_locked 185 | tasks = {} 186 | for i in range(0, len(self.tasks)): 187 | self.tasks[i].number = i + 1 188 | tasks[i + 1] = self.tasks[i].dict() 189 | d["task"] = { 190 | "id": "ComboTask", 191 | "params": { 192 | "tasks": {i + 1: self.tasks[i].dict() for i in range(0, len(self.tasks))} 193 | } 194 | } 195 | if self.airdrome_id is not None: 196 | d["airdromeId"] = self.airdrome_id 197 | if self.helipad_id is not None: 198 | d["helipadId"] = self.helipad_id 199 | if self.link_unit is not None: 200 | d["linkUnit"] = self.link_unit 201 | if self.properties is not None: 202 | d["properties"] = self.properties.dict() 203 | return d 204 | -------------------------------------------------------------------------------- /dcs/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/dcs/py.typed -------------------------------------------------------------------------------- /dcs/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/dcs/scripts/__init__.py -------------------------------------------------------------------------------- /dcs/scripts/dogfight_wwii.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | import os 4 | import random 5 | 6 | import dcs 7 | 8 | 9 | def main(): 10 | red = [ 11 | (dcs.countries.Germany.name, dcs.countries.Germany.Plane.Bf_109K_4.id), 12 | (dcs.countries.Germany.name, dcs.countries.Germany.Plane.FW_190D9.id), 13 | ] 14 | 15 | blue = [ 16 | (dcs.countries.USA.name, dcs.countries.USA.Plane.P_51D.id) 17 | ] 18 | 19 | types = red + blue 20 | 21 | aircraft_types = [x[1] for x in types] 22 | 23 | parser = argparse.ArgumentParser(description="DCS WWII dogfight generator") 24 | parser.add_argument("-a", "--aircrafttype", default=dcs.planes.Bf_109K_4.id, 25 | choices=aircraft_types, 26 | help="Player aircraft type") 27 | parser.add_argument("-p", "--playercount", default=1, type=int) 28 | parser.add_argument("-n", "--numberplanes", default=16, type=int, help="Count of planes per side") 29 | parser.add_argument("-t", "--terrain", choices=["caucasus", "nevada"], default='caucasus') 30 | parser.add_argument("-s", "--skill", choices=[x.value for x in dcs.unit.Skill], default=dcs.unit.Skill.Average.value) 31 | parser.add_argument("-o", "--output", help="Name and path of the generated mission", default=None) 32 | 33 | args = parser.parse_args() 34 | terrain_map = { 35 | "caucasus": dcs.terrain.Caucasus, 36 | "nevada": dcs.terrain.Nevada 37 | } 38 | 39 | if args.output is None: 40 | if args.terrain == "caucasus": 41 | args.output = os.path.join(os.path.expanduser("~"), "Saved Games\\DCS\\Missions\\dogfight_wwii.miz") 42 | else: 43 | args.output = os.path.join(os.path.expanduser("~"), "Saved Games\\DCS.openalpha\\Missions\\dogfight_wwii.miz") 44 | 45 | m = dcs.Mission(terrain_map[args.terrain]()) 46 | 47 | m.coalition["blue"].swap_country(m.coalition["red"], dcs.countries.Germany.name) 48 | 49 | # pick a random airport as fight reference 50 | airport = random.choice(list(m.terrain.airport_list())) 51 | battle_point = dcs.Rectangle.from_point(airport.position, 40 * 1000).random_point() 52 | heading = random.randrange(0, 360) 53 | 54 | # set editor mapview 55 | m.map.position = battle_point 56 | m.map.zoom = 100000 57 | 58 | # fight altitude 59 | altitude = random.randrange(2000, 6000, 100) 60 | 61 | if args.playercount == 1: 62 | player_country = m.country([x[0] for x in types if x[1] == args.aircrafttype][0]) 63 | hdg = heading + 180 if m.is_blue(player_country) else heading 64 | fg = m.flight_group_inflight(player_country, "Player " + args.aircrafttype, 65 | dcs.planes.plane_map[args.aircrafttype], 66 | position=battle_point.point_from_heading(hdg, 10 * 800), 67 | altitude=altitude, speed=500, maintask=dcs.task.Intercept) 68 | fg.add_waypoint(battle_point, altitude=altitude) 69 | fg.units[0].set_player() 70 | else: 71 | for x in types: 72 | country = m.country(x[0]) 73 | hdg = heading + 180 if m.is_blue(country) else heading 74 | pos = battle_point.point_from_heading(hdg, 10 * 800) 75 | pos = pos.point_from_heading(hdg + 90, random.randrange(-5000, 5000, 100)) 76 | fg = m.flight_group_inflight(country, x[1] + " group", 77 | dcs.planes.plane_map[x[1]], 78 | position=pos, altitude=random.randrange(altitude - 200, altitude + 200), 79 | speed=500, 80 | maintask=dcs.task.FighterSweep, 81 | group_size=args.playercount) 82 | fg.set_client() 83 | fg.add_waypoint(battle_point, altitude=altitude) 84 | 85 | # spawn AI 86 | group_count = int(args.numberplanes / 4) 87 | last_group_size = args.numberplanes % 4 88 | 89 | for c in [red, blue]: 90 | gc = 1 91 | for x in range(0, group_count): 92 | planetype = random.choice(c) 93 | country = m.country(planetype[0]) 94 | hdg = heading + 180 if m.is_blue(country) else heading 95 | pos = battle_point.point_from_heading(hdg, 10 * 800) 96 | pos = pos.point_from_heading(hdg + 90, random.randrange(-5000, 5000, 100)) 97 | fg = m.flight_group_inflight(country, planetype[0] + " " + planetype[1] + " #" + str(gc), 98 | dcs.planes.plane_map[planetype[1]], 99 | position=pos, altitude=random.randrange(altitude - 400, altitude + 400, 50), 100 | speed=500, 101 | maintask=dcs.task.FighterSweep, 102 | group_size=4) 103 | fg.set_skill(dcs.unit.Skill(args.skill)) 104 | fg.add_waypoint(battle_point, altitude=altitude) 105 | gc += 1 106 | 107 | if last_group_size: 108 | planetype = random.choice(c) 109 | country = m.country(planetype[0]) 110 | hdg = heading + 180 if m.is_blue(country) else heading 111 | pos = battle_point.point_from_heading(hdg, 10 * 800) 112 | pos = pos.point_from_heading(hdg + 90, random.randrange(-5000, 5000, 100)) 113 | fg = m.flight_group_inflight(m.country(planetype[0]), planetype[0] + " " + planetype[1] + " #" + str(gc), 114 | dcs.planes.plane_map[planetype[1]], 115 | position=pos, altitude=random.randrange(altitude - 400, altitude + 400, 50), 116 | speed=500, 117 | maintask=dcs.task.FighterSweep, 118 | group_size=last_group_size) 119 | fg.set_skill(dcs.unit.Skill(args.skill)) 120 | fg.add_waypoint(battle_point, altitude=altitude) 121 | 122 | m.set_sortie_text("WWII dogfight") 123 | stats = m.stats() 124 | m.set_description_text("""A WWII dogfight encounter 125 | There are {pc} planes in the air battling for life and death.""".format( 126 | pc=stats["red"]["plane_groups"]["unit_count"] + stats["blue"]["plane_groups"]["unit_count"])) 127 | m.set_description_redtask_text("Fight the other planes!") 128 | m.set_description_bluetask_text("Fight the other planes!") 129 | 130 | m.save(args.output) 131 | 132 | print("Mission created: " + args.output) 133 | return 0 134 | 135 | 136 | if __name__ == '__main__': 137 | sys.exit(main()) 138 | -------------------------------------------------------------------------------- /dcs/status_message.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class MessageSeverity(IntEnum): 5 | INFO = 0 6 | WARN = 1 7 | ERROR = 2 8 | 9 | 10 | class MessageType(IntEnum): 11 | ONBOARD_NUM_DUPLICATE = 0 12 | PARKING_SLOT_NOT_VALID = 1 13 | PARKING_SLOTS_FULL = 3 14 | MISSION_FORMAT_OLD = 4 15 | 16 | 17 | class StatusMessage: 18 | def __init__(self, message: str, type_: MessageType, severity: MessageSeverity = MessageSeverity.ERROR): 19 | self._message = message 20 | self._type = type_ 21 | self._severity = severity 22 | 23 | @property 24 | def type(self) -> MessageType: 25 | return self._type 26 | 27 | @property 28 | def severity(self) -> MessageSeverity: 29 | return self._severity 30 | 31 | @property 32 | def message(self) -> str: 33 | return self._message 34 | -------------------------------------------------------------------------------- /dcs/stub templates/liveries_scanner.pyi: -------------------------------------------------------------------------------- 1 | from typing import Dict, Set, Optional, Iterator 2 | 3 | 4 | class Livery: 5 | id: str = "" 6 | name: str = "" 7 | order: int = 0 8 | countries: set[str] | None = None 9 | 10 | def __init__(self, path_id: str, name: str, order: int, countries: Optional[Set[str]]) -> None: 11 | pass 12 | 13 | def __str__(self) -> str: 14 | pass 15 | 16 | def __repr__(self) -> str: 17 | pass 18 | 19 | def __lt__(self, other) -> bool: 20 | pass 21 | 22 | def __eq__(self, other) -> bool: 23 | pass 24 | 25 | def __hash__(self) -> int: 26 | pass 27 | 28 | def valid_for_country(self, country: str) -> bool: 29 | pass 30 | 31 | 32 | class LiverySet(set): 33 | unit_livery_id: str = "" 34 | 35 | def __init__(self, unit_livery_id: Optional[str] = None) -> None: 36 | pass 37 | 38 | def __str__(self) -> str: 39 | pass 40 | 41 | def add(self, element: Livery) -> None: 42 | pass 43 | 44 | 45 | class Liveries: 46 | map: Dict[str, LiverySet] = {} 47 | 48 | def __init__(self) -> None: 49 | pass 50 | 51 | def __getitem__(self, unit: str) -> LiverySet: 52 | pass 53 | 54 | def __setitem__(self, unit: str, liveries: LiverySet) -> None: 55 | pass 56 | 57 | def __delitem__(self, unit: str) -> None: 58 | pass 59 | 60 | def __iter__(self) -> Iterator[str]: 61 | pass 62 | 63 | @staticmethod 64 | def initialize(install: str, saved_games: str) -> None: 65 | pass 66 | 67 | @staticmethod 68 | def scan_lua_code(code: str, path: str, unit: str) -> None: 69 | pass 70 | 71 | @staticmethod 72 | def scan_dcs_installation(install: str) -> None: 73 | pass 74 | 75 | @staticmethod 76 | def scan_custom_liveries(saved_games: str) -> None: 77 | pass 78 | 79 | @staticmethod 80 | def scan_lua_description(livery_path: str, unit: str) -> None: 81 | pass 82 | 83 | @staticmethod 84 | def scan_zip_file(livery_path: str, unit: str) -> None: 85 | pass 86 | 87 | @staticmethod 88 | def scan_liveries(liveries_path: str, campaign_path: bool = False) -> None: 89 | pass 90 | 91 | @staticmethod 92 | def scan_mods_path(path: str) -> None: 93 | pass 94 | 95 | @staticmethod 96 | def scan_campaign_liveries(path: str) -> None: 97 | pass 98 | -------------------------------------------------------------------------------- /dcs/terrain/__init__.py: -------------------------------------------------------------------------------- 1 | from dcs.terrain.terrain import ParkingSlot, Airport, Runway, RunwayApproach, Terrain 2 | from dcs.terrain.terrain import RunwayOccupiedError, NoParkingSlotError, Graph, Node, MapView 3 | from dcs.terrain.caucasus.caucasus import Caucasus 4 | from dcs.terrain.falklands import Falklands 5 | from dcs.terrain.nevada import Nevada 6 | from dcs.terrain.normandy import Normandy 7 | from dcs.terrain.persiangulf import PersianGulf 8 | from dcs.terrain.thechannel import TheChannel 9 | from dcs.terrain.sinai import Sinai 10 | from dcs.terrain.syria import Syria 11 | from dcs.terrain.marianaislands import MarianaIslands 12 | -------------------------------------------------------------------------------- /dcs/terrain/caucasus/__init__.py: -------------------------------------------------------------------------------- 1 | from .caucasus import Caucasus 2 | -------------------------------------------------------------------------------- /dcs/terrain/caucasus/citygraph.p: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/dcs/terrain/caucasus/citygraph.p -------------------------------------------------------------------------------- /dcs/terrain/caucasus/projection.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT: 2 | # This file is generated by tools/export_map_projection.py. 3 | from dcs.terrain.projections import TransverseMercator 4 | 5 | PARAMETERS = TransverseMercator( 6 | central_meridian=33, 7 | false_easting=-99516.9999999732, 8 | false_northing=-4998114.999999984, 9 | scale_factor=0.9996, 10 | ) 11 | -------------------------------------------------------------------------------- /dcs/terrain/falklands/__init__.py: -------------------------------------------------------------------------------- 1 | from .falklands import Falklands 2 | -------------------------------------------------------------------------------- /dcs/terrain/falklands/falklands.py: -------------------------------------------------------------------------------- 1 | from dcs import mapping 2 | from dcs.terrain import Terrain, MapView 3 | from .airports import ALL_AIRPORTS 4 | from .projection import PARAMETERS 5 | 6 | 7 | class Falklands(Terrain): 8 | center = {"lat": 52.468, "long": 59.173} 9 | temperature = [ 10 | (5, 17), 11 | (5, 17), 12 | (2, 14), 13 | (2, 14), 14 | (2, 14), 15 | (-5, 11), 16 | (-5, 11), 17 | (-5, 11), 18 | (1, 15), 19 | (1, 15), 20 | (1, 15), 21 | (5, 17) 22 | ] 23 | assert len(temperature) == 12 24 | 25 | def __init__(self): 26 | super().__init__( 27 | "Falklands", 28 | PARAMETERS, 29 | bounds=mapping.Rectangle(74967, -114995, -129982, 129991, self), 30 | map_view_default=MapView(mapping.Point(0, 0, self), self, 1000000) 31 | ) 32 | self.bullseye_blue = {"x": 0, "y": 0} 33 | self.bullseye_red = {"x": 0, "y": 0} 34 | 35 | self.airports = {a.name: a(self) for a in ALL_AIRPORTS} 36 | -------------------------------------------------------------------------------- /dcs/terrain/falklands/projection.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT: 2 | # This file is generated by tools/export_map_projection.py. 3 | from dcs.terrain.projections import TransverseMercator 4 | 5 | PARAMETERS = TransverseMercator( 6 | central_meridian=-57, 7 | false_easting=147639.99999997593, 8 | false_northing=5815417.000000032, 9 | scale_factor=0.9996, 10 | ) 11 | -------------------------------------------------------------------------------- /dcs/terrain/marianaislands/__init__.py: -------------------------------------------------------------------------------- 1 | from .marianaislands import MarianaIslands 2 | -------------------------------------------------------------------------------- /dcs/terrain/marianaislands/marianaislands.py: -------------------------------------------------------------------------------- 1 | from dcs import mapping 2 | from dcs.terrain import Terrain, MapView 3 | from .airports import ALL_AIRPORTS 4 | from .projection import PARAMETERS 5 | 6 | 7 | class MarianaIslands(Terrain): 8 | center = {"lat": 13.485, "long": 144.798} 9 | temperature = [ 10 | # https://en.wikipedia.org/wiki/Guam#Climate 11 | (24, 30), 12 | (24, 30), 13 | (24, 30), 14 | (25, 31), 15 | (25, 31), 16 | (25, 31), 17 | (25, 31), 18 | (25, 30), 19 | (25, 30), 20 | (25, 30), 21 | (25, 30), 22 | (25, 30) 23 | ] 24 | 25 | assert len(temperature) == 12 26 | 27 | def __init__(self): 28 | super().__init__( 29 | "MarianaIslands", 30 | PARAMETERS, 31 | bounds=mapping.Rectangle(1000 * 10000, -1000 * 1000, -300 * 1000, 500 * 1000, self), 32 | map_view_default=MapView(mapping.Point(76432, 48051, self), self, 1000000) 33 | ) 34 | 35 | self.airports = {a.name: a(self) for a in ALL_AIRPORTS} 36 | -------------------------------------------------------------------------------- /dcs/terrain/marianaislands/projection.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT: 2 | # This file is generated by tools/export_map_projection.py. 3 | from dcs.terrain.projections import TransverseMercator 4 | 5 | PARAMETERS = TransverseMercator( 6 | central_meridian=147, 7 | false_easting=238417.99999989968, 8 | false_northing=-1491840.000000048, 9 | scale_factor=0.9996, 10 | ) 11 | -------------------------------------------------------------------------------- /dcs/terrain/nevada/__init__.py: -------------------------------------------------------------------------------- 1 | from .nevada import Nevada 2 | -------------------------------------------------------------------------------- /dcs/terrain/nevada/citygraph.p: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/dcs/terrain/nevada/citygraph.p -------------------------------------------------------------------------------- /dcs/terrain/nevada/nevada.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from dcs.terrain import Terrain, MapView, Graph 3 | import dcs.mapping as mapping 4 | import os 5 | from .airports import ALL_AIRPORTS 6 | from .projection import PARAMETERS 7 | 8 | 9 | class Nevada(Terrain): 10 | center = {"lat": 39.81806, "long": -114.73333} 11 | temperature = [ 12 | (0, 10), 13 | (2, 16), 14 | (6, 22), 15 | (10, 24), 16 | (14, 28), 17 | (19, 35), 18 | (23, 40), 19 | (22, 38), 20 | (18, 33), 21 | (11, 26), 22 | (5, 19), 23 | (1, 13) 24 | ] 25 | assert len(temperature) == 12 26 | 27 | def __init__(self): 28 | super().__init__( 29 | "Nevada", 30 | PARAMETERS, 31 | bounds=mapping.Rectangle(-166934.953125, -329334.875000, -497177.656250, 209836.890625, self), 32 | map_view_default=MapView(mapping.Point(-340928.57142857, -55928.571428568, self), self, 1000000) 33 | ) 34 | # nttr center MGRS 35 | # 11SPE9400410022 36 | self.bullseye_blue = {"x": -409931.344, "y": -14024.097} 37 | self.bullseye_red = {"x": -288293.969, "y": -88022.641} 38 | 39 | try: 40 | self.city_graph = Graph.from_pickle(os.path.join(os.path.dirname(__file__), 'nevada.p')) # type: Graph 41 | except FileNotFoundError: 42 | pass 43 | 44 | self.airports = {a.name: a(self) for a in ALL_AIRPORTS} 45 | -------------------------------------------------------------------------------- /dcs/terrain/nevada/projection.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT: 2 | # This file is generated by tools/export_map_projection.py. 3 | from dcs.terrain.projections import TransverseMercator 4 | 5 | PARAMETERS = TransverseMercator( 6 | central_meridian=-117, 7 | false_easting=-193996.80999964548, 8 | false_northing=-4410028.063999966, 9 | scale_factor=0.9996, 10 | ) 11 | -------------------------------------------------------------------------------- /dcs/terrain/normandy/__init__.py: -------------------------------------------------------------------------------- 1 | from .normandy import Normandy 2 | -------------------------------------------------------------------------------- /dcs/terrain/normandy/normandy.py: -------------------------------------------------------------------------------- 1 | from dcs.terrain.terrain import Terrain, MapView 2 | import dcs.mapping as mapping 3 | from .airports import ALL_AIRPORTS 4 | from .projection import PARAMETERS 5 | 6 | 7 | class Normandy(Terrain): 8 | center = {"lat": 41.3, "long": 0.18} 9 | temperature = [ 10 | (-10, 10), 11 | (-9, 10), 12 | (-3, 12), 13 | (-1, 14), 14 | (0, 18), 15 | (2, 22), 16 | (7, 30), 17 | (8, 32), 18 | (3, 28), 19 | (0, 22), 20 | (-2, 16), 21 | (-8, 10) 22 | ] 23 | assert len(temperature) == 12 24 | 25 | def __init__(self) -> None: 26 | bounds = mapping.Rectangle(-132707.843750, -389942.906250, 185756.156250, 165065.078125, self) 27 | super().__init__( 28 | "Normandy", 29 | PARAMETERS, 30 | bounds, 31 | map_view_default=MapView(bounds.center(), self, 1000000) 32 | ) 33 | self.bullseye_blue = {"x": self.bounds.center().x, "y": self.bounds.center().y} 34 | self.bullseye_red = {"x": self.bounds.center().x, "y": self.bounds.center().y} 35 | 36 | # Ignore type checking on the following line because for some reason this 37 | # doesn't type-check for Normandy but is fine for every other terrain. 38 | self.airports = {a.name: a(self) for a in ALL_AIRPORTS} # type: ignore 39 | -------------------------------------------------------------------------------- /dcs/terrain/normandy/projection.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT: 2 | # This file is generated by tools/export_map_projection.py. 3 | from dcs.terrain.projections import TransverseMercator 4 | 5 | PARAMETERS = TransverseMercator( 6 | central_meridian=-3, 7 | false_easting=-195526.00000000204, 8 | false_northing=-5484812.999999951, 9 | scale_factor=0.9996, 10 | ) 11 | -------------------------------------------------------------------------------- /dcs/terrain/persiangulf/__init__.py: -------------------------------------------------------------------------------- 1 | from .persiangulf import PersianGulf 2 | -------------------------------------------------------------------------------- /dcs/terrain/persiangulf/persiangulf.py: -------------------------------------------------------------------------------- 1 | from dcs.terrain.terrain import Terrain, MapView 2 | import dcs.mapping as mapping 3 | from .airports import ALL_AIRPORTS 4 | from .projection import PARAMETERS 5 | 6 | 7 | class PersianGulf(Terrain): 8 | center = {"lat": 0, "long": 0} 9 | temperature = [ 10 | (10, 20), 11 | (11, 20), 12 | (13, 22), 13 | (16, 26), 14 | (18, 30), 15 | (20, 36), 16 | (24, 39), 17 | (25, 40), 18 | (22, 37), 19 | (20, 32), 20 | (16, 26), 21 | (12, 22) 22 | ] 23 | assert len(temperature) == 12 24 | 25 | def __init__(self): 26 | bounds = mapping.Rectangle(-218768.750000, -392081.937500, 197357.906250, 333129.125000, self) 27 | super().__init__( 28 | "PersianGulf", 29 | PARAMETERS, 30 | bounds, 31 | map_view_default=MapView(bounds.center(), self, 1000000) 32 | ) 33 | self.bullseye_blue = {"x": self.bounds.center().x, "y": self.bounds.center().y} 34 | self.bullseye_red = {"x": self.bounds.center().x, "y": self.bounds.center().y} 35 | 36 | self.airports = {a.name: a(self) for a in ALL_AIRPORTS} 37 | -------------------------------------------------------------------------------- /dcs/terrain/persiangulf/projection.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT: 2 | # This file is generated by tools/export_map_projection.py. 3 | from dcs.terrain.projections import TransverseMercator 4 | 5 | PARAMETERS = TransverseMercator( 6 | central_meridian=57, 7 | false_easting=75755.99999999645, 8 | false_northing=-2894933.0000000377, 9 | scale_factor=0.9996, 10 | ) 11 | -------------------------------------------------------------------------------- /dcs/terrain/projections/__init__.py: -------------------------------------------------------------------------------- 1 | from .transversemercator import TransverseMercator 2 | -------------------------------------------------------------------------------- /dcs/terrain/projections/transversemercator.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from pyproj import CRS 4 | 5 | 6 | @dataclass(frozen=True) 7 | class TransverseMercator: 8 | central_meridian: int 9 | false_easting: float 10 | false_northing: float 11 | scale_factor: float 12 | 13 | def to_crs(self) -> CRS: 14 | return CRS.from_proj4( 15 | " ".join( 16 | [ 17 | "+proj=tmerc", 18 | "+lat_0=0", 19 | f"+lon_0={self.central_meridian}", 20 | f"+k_0={self.scale_factor}", 21 | f"+x_0={self.false_easting}", 22 | f"+y_0={self.false_northing}", 23 | "+towgs84=0,0,0,0,0,0,0", 24 | "+units=m", 25 | "+vunits=m", 26 | "+ellps=WGS84", 27 | "+no_defs", 28 | "+axis=neu", 29 | ] 30 | ) 31 | ) 32 | -------------------------------------------------------------------------------- /dcs/terrain/sinai/__init__.py: -------------------------------------------------------------------------------- 1 | from .sinai import Sinai 2 | -------------------------------------------------------------------------------- /dcs/terrain/sinai/projection.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT: 2 | # This file is generated by tools/export_map_projection.py. 3 | from dcs.terrain.projections import TransverseMercator 4 | 5 | PARAMETERS = TransverseMercator( 6 | central_meridian=33, 7 | false_easting=169221.9999999585, 8 | false_northing=-3325312.9999999693, 9 | scale_factor=0.9996, 10 | ) 11 | -------------------------------------------------------------------------------- /dcs/terrain/sinai/sinai.py: -------------------------------------------------------------------------------- 1 | import dcs.mapping as mapping 2 | from dcs.terrain.terrain import Terrain, MapView 3 | from .airports import ALL_AIRPORTS 4 | from .projection import PARAMETERS 5 | 6 | 7 | class Sinai(Terrain): 8 | center = {"lat": 31.0, "long": 32.0} 9 | temperature = [ 10 | (2, 8), 11 | (3, 11), 12 | (6, 15), 13 | (10, 21), 14 | (15, 27), 15 | (18, 32), 16 | (22, 35), 17 | (22, 35), 18 | (19, 32), 19 | (14, 26), 20 | (7, 17), 21 | (4, 10) 22 | ] 23 | assert len(temperature) == 12 24 | 25 | def __init__(self): 26 | bounds = mapping.Rectangle(-450000, -280000, 500000, 560000, self) 27 | super().__init__( 28 | "Sinai", 29 | PARAMETERS, 30 | bounds, 31 | map_view_default=MapView(bounds.center(), self, 1000000) 32 | ) 33 | self.bullseye_blue = {"x": 0, "y": 0} 34 | self.bullseye_red = {"x": 0, "y": 0} 35 | 36 | self.airports = {a.name: a(self) for a in ALL_AIRPORTS} 37 | 38 | @property 39 | def miz_theatre_name(self) -> str: 40 | return "SinaiMap" 41 | -------------------------------------------------------------------------------- /dcs/terrain/syria/__init__.py: -------------------------------------------------------------------------------- 1 | from .syria import Syria 2 | -------------------------------------------------------------------------------- /dcs/terrain/syria/projection.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT: 2 | # This file is generated by tools/export_map_projection.py. 3 | from dcs.terrain.projections import TransverseMercator 4 | 5 | PARAMETERS = TransverseMercator( 6 | central_meridian=39, 7 | false_easting=282801.00000003993, 8 | false_northing=-3879865.9999999935, 9 | scale_factor=0.9996, 10 | ) 11 | -------------------------------------------------------------------------------- /dcs/terrain/syria/syria.py: -------------------------------------------------------------------------------- 1 | import dcs.mapping as mapping 2 | from dcs.terrain.terrain import Terrain, MapView 3 | from .airports import ALL_AIRPORTS 4 | from .projection import PARAMETERS 5 | 6 | 7 | class Syria(Terrain): 8 | center = {"lat": 35.021, "long": 35.901} 9 | temperature = [ 10 | (2, 8), 11 | (3, 11), 12 | (6, 15), 13 | (10, 21), 14 | (15, 27), 15 | (18, 32), 16 | (22, 35), 17 | (22, 35), 18 | (19, 32), 19 | (14, 26), 20 | (7, 17), 21 | (4, 10) 22 | ] 23 | assert len(temperature) == 12 24 | 25 | def __init__(self): 26 | bounds = mapping.Rectangle(-320000, -579986, 300000, 579998, self) 27 | super().__init__( 28 | "Syria", 29 | PARAMETERS, 30 | bounds, 31 | map_view_default=MapView(bounds.center(), self, 1000000) 32 | ) 33 | self.bullseye_blue = {"x": 0, "y": 0} 34 | self.bullseye_red = {"x": 0, "y": 0} 35 | 36 | self.airports = {a.name: a(self) for a in ALL_AIRPORTS} 37 | -------------------------------------------------------------------------------- /dcs/terrain/thechannel/__init__.py: -------------------------------------------------------------------------------- 1 | from .thechannel import TheChannel 2 | -------------------------------------------------------------------------------- /dcs/terrain/thechannel/projection.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT: 2 | # This file is generated by tools/export_map_projection.py. 3 | from dcs.terrain.projections import TransverseMercator 4 | 5 | PARAMETERS = TransverseMercator( 6 | central_meridian=3, 7 | false_easting=99376.00000000288, 8 | false_northing=-5636889.00000001, 9 | scale_factor=0.9996, 10 | ) 11 | -------------------------------------------------------------------------------- /dcs/terrain/thechannel/thechannel.py: -------------------------------------------------------------------------------- 1 | import dcs.mapping as mapping 2 | from dcs.terrain.terrain import Terrain, MapView 3 | from .airports import ALL_AIRPORTS 4 | from .projection import PARAMETERS 5 | 6 | 7 | class TheChannel(Terrain): 8 | center = {"lat": 50.875, "long": 1.5875} 9 | temperature = [ 10 | (-10, 10), 11 | (-9, 10), 12 | (-3, 12), 13 | (-1, 14), 14 | (0, 18), 15 | (2, 22), 16 | (7, 30), 17 | (8, 32), 18 | (3, 28), 19 | (0, 22), 20 | (-2, 16), 21 | (-8, 10) 22 | ] 23 | assert len(temperature) == 12 24 | 25 | def __init__(self): 26 | super().__init__( 27 | "TheChannel", 28 | PARAMETERS, 29 | bounds=mapping.Rectangle(74967, -114995, -129982, 129991, self), 30 | map_view_default=MapView(mapping.Point(0, 0, self), self, 1000000) 31 | ) 32 | self.bullseye_blue = {"x": 0, "y": 0} 33 | self.bullseye_red = {"x": 0, "y": 0} 34 | 35 | self.airports = {a.name: a(self) for a in ALL_AIRPORTS} 36 | -------------------------------------------------------------------------------- /dcs/translation.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | 4 | class String: 5 | def __init__(self, _id='', translation=None): 6 | self.translation = translation 7 | self.id = _id 8 | 9 | def set(self, text, lang='DEFAULT'): 10 | self.translation.set_string(self.id, text, lang) 11 | return str(self) 12 | 13 | def str(self, lang='DEFAULT'): 14 | if self.translation: 15 | return self.translation.strings[lang][self.id] 16 | 17 | return "" 18 | 19 | def __str__(self): 20 | return self.str('DEFAULT') 21 | 22 | def __repr__(self): 23 | return self.id + ":" + str(self) 24 | 25 | 26 | class ResourceKey: 27 | def __init__(self, res_key: str): 28 | self.res_key = res_key 29 | 30 | @property 31 | def key(self) -> str: 32 | return self.res_key 33 | 34 | def __str__(self): 35 | return self.res_key 36 | 37 | def __eq__(self, other): 38 | if isinstance(other, ResourceKey): 39 | return self.__dict__ == other.__dict__ 40 | return False 41 | 42 | 43 | class Translation: 44 | def __init__(self, _mission): 45 | self.strings: Dict[str, Dict[str, str]] = {} 46 | self.mission = _mission 47 | 48 | def has_string(self, _id: str, lang: str = 'DEFAULT') -> bool: 49 | return _id in self.strings[lang] 50 | 51 | def set_string(self, _id, string, lang='DEFAULT'): 52 | if lang not in self.strings: 53 | self.strings[lang] = {} 54 | self.strings[lang][_id] = string 55 | return _id 56 | 57 | def get_string(self, _id: str) -> String: 58 | return String(_id, self) 59 | 60 | def create_string(self, s: str, lang: str = 'DEFAULT') -> String: 61 | _id = 'DictKey_Translation_{dict_id}'.format(dict_id=self.mission.next_dict_id()) 62 | self.set_string(_id, s, lang) 63 | return String(_id, self) 64 | 65 | def delete_string(self, _id): 66 | for lang in self.strings: 67 | if _id in self.strings[lang]: 68 | del self.strings[lang][_id] 69 | 70 | def languages(self) -> List[str]: 71 | return list(self.strings.keys()) 72 | 73 | def dict(self, lang='DEFAULT'): 74 | if lang in self.strings: 75 | return {x: self.strings[lang][x] for x in self.strings[lang]} 76 | return {} 77 | 78 | def __str__(self): 79 | return str(self.strings) 80 | 81 | def __repr__(self): 82 | return repr(self.strings) 83 | -------------------------------------------------------------------------------- /dcs/unitpropertydescription.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Dict, Literal, Optional, Union 3 | 4 | 5 | @dataclass(frozen=True) 6 | class UnitPropertyDescription: 7 | # The ID used in the miz file for this property. 8 | identifier: str 9 | 10 | # The type of control shown in the UI for this property. "label" type properties are 11 | # not really properties, but text-only lines shown in the mission editor for 12 | # grouping properties. 13 | control: Literal[ 14 | "checkbox", 15 | "comboList", 16 | "editbox", 17 | "groupbox", 18 | "label", 19 | "slider", 20 | "spinbox", 21 | ] 22 | 23 | # The human readable name of this property. When the control is a label, 24 | # the value may be None to indicate that a blank line should be printed in 25 | # the mission editor UI. 26 | label: Optional[str] = None 27 | 28 | # True if the property is only valid for units that have skill set to Player or 29 | # Client. 30 | player_only: bool = False 31 | 32 | # The minimum value of the property. Only present for spinbox properties. 33 | minimum: Optional[int] = None 34 | 35 | # The minimum value of the property. Only present for spinbox properties. 36 | maximum: Optional[int] = None 37 | 38 | # The default value of the property. Should be defined for all non-label properties. 39 | default: Optional[Union[bool, float, int, str]] = None 40 | 41 | # A weight adjustment applied to the aircraft when this property is enabled. 42 | # Only present for boolean values. 43 | weight_when_on: Optional[float] = None 44 | 45 | # The options allowed for comboList properties. The key of the dict is the ID of the 46 | # option which is how the value is represented in the miz. The value is the display 47 | # name for the UI. 48 | values: Optional[Dict[Union[str, int, float, None], str]] = None 49 | 50 | # No idea what these are for. 51 | dimension: Optional[str] = None 52 | x_lbl: Optional[int] = None 53 | w_ctrl: Optional[int] = None 54 | -------------------------------------------------------------------------------- /dcs/unittype.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import dcs.lua as lua 4 | from dcs.payloads import PayloadDirectories 5 | import re 6 | import sys 7 | from typing import Any, Dict, Iterator, List, Optional, Set, Type, Union, TYPE_CHECKING 8 | 9 | from dcs.liveries.livery import Livery 10 | from dcs.liveries.liverycache import LiveryCache 11 | from dcs.liveries.liveryset import LiverySet 12 | 13 | if TYPE_CHECKING: 14 | from dcs.country import Country 15 | from dcs.task import MainTask 16 | from .unitpropertydescription import UnitPropertyDescription 17 | 18 | 19 | class UnitType: 20 | id: str 21 | name: str 22 | 23 | 24 | class VehicleType(UnitType): 25 | eplrs = False 26 | detection_range = 0 27 | threat_range = 0 28 | air_weapon_dist = 0 29 | 30 | 31 | class ShipType(UnitType): 32 | helicopter_num = 0 33 | plane_num = 0 34 | parking = 0 35 | 36 | 37 | class StaticType(UnitType): 38 | shape_name: str 39 | rate = 0 40 | category = "Fortifications" 41 | sea_object = False 42 | can_cargo = False 43 | mass = None 44 | 45 | 46 | class LiveryOverwrites: 47 | map = { 48 | "M-2000C.FRA": "AdA Chasse 2-5" 49 | } 50 | 51 | 52 | #: A dict of 1-indexed channel IDs to the preset frequency. 53 | RadioPresets = Dict[int, float] 54 | 55 | 56 | #: A dict mapping "channels" to the radio channel presets. 57 | RadioConfig = Dict[str, RadioPresets] 58 | 59 | 60 | #: A dict mapping the 1-indexed radio ID to the radio configuration. 61 | AircraftRadioPresets = Dict[int, RadioConfig] 62 | 63 | 64 | class FlyingType(UnitType): 65 | flyable = False 66 | group_size_max = 4 67 | large_parking_slot = False 68 | helicopter = False 69 | fuel_max: float = 0 70 | max_speed: float = 500 71 | height: float = 0 72 | width: float = 0 73 | length: float = 0 74 | ammo_type = None 75 | chaff = 0 76 | flare = 0 77 | charge_total = 0 78 | chaff_charge_size = 1 79 | flare_charge_size = 2 80 | category = "Air" 81 | 82 | tacan = False 83 | eplrs = False 84 | 85 | radio_frequency: float = 251 86 | 87 | #: The preset radio channels for the aircraft, if the aircraft supports them. Not all aircraft support radio presets. Those 88 | # that don't will have None for their panel_radio. 89 | panel_radio: Optional[AircraftRadioPresets] = None 90 | 91 | property_defaults: Optional[Dict[str, Any]] = None 92 | 93 | properties: Dict[str, UnitPropertyDescription] = {} 94 | 95 | pylons: Set[int] = set() 96 | livery_name: Optional[str] = None 97 | Liveries: LiverySet = LiverySet() 98 | # Dict from payload name to the DCS payload structure. None if not yet initialized. 99 | payloads: Optional[Dict[str, Dict[str, Any]]] = None 100 | 101 | tasks: List[Union[str, Type["MainTask"]]] = ["Nothing"] 102 | task_default: Optional[Type["MainTask"]] = None 103 | 104 | _payload_cache = None 105 | _UnitPayloadGlobals = None 106 | 107 | @classmethod 108 | def scan_payload_dir(cls): 109 | if FlyingType._payload_cache: 110 | return 111 | FlyingType._payload_cache = {} 112 | for payload_dir in PayloadDirectories.payload_dirs(): 113 | if not payload_dir.exists(): 114 | continue 115 | for payload_path in payload_dir.glob("*.lua"): 116 | if payload_path not in FlyingType._payload_cache: 117 | with payload_path.open('r', encoding='utf-8') as payload_file: 118 | for line in payload_file: 119 | g = re.search(r'\["unitType"]\s*=\s*"([^"]*)', line) 120 | if g: 121 | FlyingType._payload_cache[payload_path] = g.group(1) 122 | break 123 | 124 | @classmethod 125 | def load_payloads(cls): 126 | # avoid cyclic dependency 127 | if FlyingType._UnitPayloadGlobals is None: 128 | from . import task 129 | FlyingType._UnitPayloadGlobals = {v.internal_name: v.id for k, v in task.MainTask.map.items()} 130 | 131 | FlyingType.scan_payload_dir() 132 | if cls.payloads is not None: 133 | return cls.payloads 134 | cls.payloads = {} 135 | 136 | for payload_dir in PayloadDirectories.payload_dirs(): 137 | if not payload_dir.exists(): 138 | continue 139 | for payload_path in payload_dir.glob("*.lua"): 140 | if FlyingType._payload_cache[payload_path] == cls.id and payload_path.exists(): 141 | try: 142 | payload_main = lua.loads(payload_path.read_text(), _globals=FlyingType._UnitPayloadGlobals) 143 | except SyntaxError: 144 | print("Error parsing lua file '{f}'".format(f=payload_path), file=sys.stderr) 145 | raise 146 | pays = payload_main["unitPayloads"] 147 | if pays["unitType"] == cls.id: 148 | for load in pays["payloads"].values(): 149 | name = load["name"] 150 | # Payload directories are iterated in decreasing order of 151 | # preference, so if we already have a payload matching the 152 | # name, ignore it. 153 | if name not in cls.payloads: 154 | cls.payloads[load["name"]] = load 155 | 156 | return cls.payloads 157 | 158 | @classmethod 159 | def loadout(cls, _task): 160 | if cls.payloads is not None: 161 | for payload in cls.payloads.values(): 162 | tasks = [payload["tasks"][x] for x in payload["tasks"]] 163 | if _task.id in tasks: 164 | pylons = payload["pylons"] 165 | r = [(pylons[x]["num"], {"clsid": pylons[x]["CLSID"]}) for x in pylons] 166 | return r 167 | return None 168 | 169 | @classmethod 170 | def loadout_by_name(cls, loadout_name): 171 | if cls.payloads is not None: 172 | payload = cls.payloads.get(loadout_name) 173 | if payload is None: 174 | return None 175 | pylons = payload["pylons"] 176 | r = [(pylons[x]["num"], {"clsid": pylons[x]["CLSID"]}) for x in pylons] 177 | return r 178 | return None 179 | 180 | @classmethod 181 | def iter_liveries(cls) -> Iterator[Livery]: 182 | if cls.livery_name is None: 183 | return 184 | yield from LiveryCache.for_unit(cls.livery_name) 185 | 186 | @classmethod 187 | def iter_liveries_for_country(cls, country: Country) -> Iterator[Livery]: 188 | if cls.livery_name is None: 189 | return 190 | for livery in LiveryCache.for_unit(cls.livery_name): 191 | if livery.valid_for_country(country.shortname): 192 | yield livery 193 | 194 | @classmethod 195 | def default_livery(cls, country_name) -> str: 196 | if cls.id + "." + country_name in LiveryOverwrites.map: 197 | return LiveryOverwrites.map[cls.id + "." + country_name] 198 | else: 199 | liveries = sorted( 200 | livery 201 | for livery in cls.iter_liveries() 202 | if livery.valid_for_country(country_name) 203 | ) 204 | if liveries: 205 | return liveries[0].id 206 | return "" 207 | -------------------------------------------------------------------------------- /dcs/winreg.py: -------------------------------------------------------------------------------- 1 | """Wrapper around the stdlib winreg that fails gracefully on non-Windows.""" 2 | import logging 3 | import sys 4 | from typing import Any, Callable, Optional, TypeVar 5 | 6 | T = TypeVar("T") 7 | 8 | 9 | def read_current_user_value( 10 | key: str, value: str, ctor: Callable[[Any], T] = lambda x: x 11 | ) -> Optional[T]: 12 | if sys.platform == "win32": 13 | import winreg 14 | 15 | try: 16 | with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key) as hkey: 17 | # QueryValueEx returns a tuple of (value, type ID). 18 | return ctor(winreg.QueryValueEx(hkey, value)[0]) 19 | except FileNotFoundError: 20 | return None 21 | else: 22 | logging.getLogger("pydcs").error( 23 | "Cannot read registry keys on non-Windows OS, returning None" 24 | ) 25 | return None 26 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pydcs.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pydcs.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pydcs" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pydcs" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /doc/caucasus_random_mission.rst: -------------------------------------------------------------------------------- 1 | Random mission script 2 | ===================== 3 | 4 | pydcs delivers a small random mission creator script. 5 | This script was indented to show features of the framework and is also a little testbed. 6 | 7 | It is invoked by the commandline with the following arguments:: 8 | 9 | usage: caucasus_random_mission.py [-h] [-a {A-10C,Su-25T,M-2000C,Ka-50,MiG-21Bis}] 10 | [-p PLAYERCOUNT] [-s {inflight,runway,warm,cold}] 11 | [-t {main,CAS,CAP,refuel}] 12 | [-d {random,day,night,dusk,dawn,noon}] 13 | [-w {dynamic,dyncyclone,dynanti,dynone,clear}] [-u] 14 | [--show-stats] [-o OUTPUT] 15 | 16 | Random DCS mission generator 17 | 18 | optional arguments: 19 | -h, --help show this help message and exit 20 | -a {A-10C,Su-25T,M-2000C,Ka-50,MiG-21Bis}, --aircrafttype {A-10C,Su-25T,M-2000C,Ka-50,MiG-21Bis} 21 | Player aircraft type 22 | -p PLAYERCOUNT, --playercount PLAYERCOUNT 23 | -s {inflight,runway,warm,cold}, --start {inflight,runway,warm,cold} 24 | -t {main,CAS,CAP,refuel}, --missiontype {main,CAS,CAP,refuel} 25 | -d {random,day,night,dusk,dawn,noon}, --daytime {random,day,night,dusk,dawn,noon} 26 | -w {dynamic,dyncyclone,dynanti,dynone,clear}, --weather {dynamic,dyncyclone,dynanti,dynone,clear} 27 | -u, --unhide Show enemy pre mission 28 | --show-stats Show generated missions stats 29 | -o OUTPUT, --output OUTPUT 30 | Name and path of the generated mission 31 | -------------------------------------------------------------------------------- /doc/dcs.lua.rst: -------------------------------------------------------------------------------- 1 | dcs.lua package 2 | =============== 3 | 4 | Submodules 5 | ---------- 6 | 7 | dcs.lua.parse module 8 | -------------------- 9 | 10 | .. automodule:: dcs.lua.parse 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | dcs.lua.serialize module 16 | ------------------------ 17 | 18 | .. automodule:: dcs.lua.serialize 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | Module contents 24 | --------------- 25 | 26 | .. automodule:: dcs.lua 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /doc/dcs.rst: -------------------------------------------------------------------------------- 1 | dcs package 2 | =========== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | dcs.lua 10 | dcs.terrain 11 | 12 | Submodules 13 | ---------- 14 | 15 | dcs.country module 16 | ------------------ 17 | 18 | .. automodule:: dcs.country 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | dcs.forcedoptions module 24 | ------------------------ 25 | 26 | .. automodule:: dcs.forcedoptions 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | dcs.goals module 32 | ---------------- 33 | 34 | .. automodule:: dcs.goals 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | dcs.groundcontrol module 40 | ------------------------ 41 | 42 | .. automodule:: dcs.groundcontrol 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | dcs.mapping module 48 | ------------------ 49 | 50 | .. automodule:: dcs.mapping 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | dcs.point module 56 | ---------------- 57 | 58 | .. automodule:: dcs.point 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | dcs.templates module 64 | -------------------- 65 | 66 | .. automodule:: dcs.templates 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | dcs.translation module 72 | ---------------------- 73 | 74 | .. automodule:: dcs.translation 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | dcs.unit module 80 | --------------- 81 | 82 | .. automodule:: dcs.unit 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | dcs.unitgroup module 88 | -------------------- 89 | 90 | .. automodule:: dcs.unitgroup 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | 95 | dcs.unittype module 96 | ------------------- 97 | 98 | .. automodule:: dcs.unittype 99 | :members: 100 | :undoc-members: 101 | :show-inheritance: 102 | 103 | dcs.weather module 104 | ------------------ 105 | 106 | .. automodule:: dcs.weather 107 | :members: 108 | :undoc-members: 109 | :show-inheritance: 110 | 111 | 112 | Module contents 113 | --------------- 114 | 115 | .. automodule:: dcs 116 | :members: 117 | :undoc-members: 118 | :show-inheritance: 119 | -------------------------------------------------------------------------------- /doc/dcs.terrain.rst: -------------------------------------------------------------------------------- 1 | dcs.terrain package 2 | =================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | dcs.terrain.caucasus module 8 | --------------------------- 9 | 10 | .. automodule:: dcs.terrain.caucasus 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | dcs.terrain.nevada module 16 | ------------------------- 17 | 18 | .. automodule:: dcs.terrain.nevada 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | dcs.terrain.terrain module 24 | -------------------------- 25 | 26 | .. automodule:: dcs.terrain.terrain 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: dcs.terrain 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. pydcs documentation master file, created by 2 | sphinx-quickstart on Sun Mar 20 16:20:16 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to pydcs's documentation! 7 | ================================= 8 | 9 | pydcs is a Python(3) framework for creating digital combat simulator missions. 10 | 11 | Contents: 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | quickstart 17 | caucasus_random_mission 18 | mission 19 | task 20 | modules 21 | 22 | Indices and tables 23 | ================== 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` 28 | 29 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 1>NUL 2>NUL 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pydcs.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pydcs.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /doc/mission.rst: -------------------------------------------------------------------------------- 1 | dcs.mission module 2 | ------------------ 3 | 4 | .. automodule:: dcs.mission 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/modules.rst: -------------------------------------------------------------------------------- 1 | dcs 2 | === 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | dcs 8 | -------------------------------------------------------------------------------- /doc/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ========== 3 | 4 | Creating a mission is fairly simple, the most important class in this respect is 5 | :py:class:`dcs.mission.Mission`. This class contains all information for running a dcs mission. 6 | It is a .zip file that contains several lua data structures and other resources like 7 | briefing images, voice overs and other lua scripts. 8 | 9 | :: 10 | 11 | m = dcs.Mission() 12 | m.save('mission.miz') 13 | 14 | This code is enough to create a mission file without any unit groups in the Caucasus(default) 15 | terrain. 16 | 17 | To add a A-10C flight group starting from Batumi airport use the following snippet:: 18 | 19 | fg = m.flight_group_from_airport(m.country("USA"), "A-10C Flight Group", 20 | dcs.planes.A-10C, m.terrain.batumi(), group_size=2) 21 | fg.units[0].set_player() 22 | 23 | This adds a A-10C flight with 2 planes starting cold from a free parking slot. 24 | In the next line it also sets the first unit of the flight as player. 25 | For more options when adding a flight see :py:meth:`dcs.mission.Mission.flight_group_from_airport`. 26 | -------------------------------------------------------------------------------- /doc/task.rst: -------------------------------------------------------------------------------- 1 | dcs.task module 2 | --------------- 3 | 4 | .. automodule:: dcs.task 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyproj 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | count = True 3 | show-source = True 4 | statistics = True 5 | ignore = C901,W503 6 | max-complexity = 10 7 | 8 | # GitHub editor width. 9 | max-line-length = 127 10 | 11 | extend-exclude = 12 | doc/, 13 | tests/, 14 | tools/, 15 | venv/, 16 | dcs/helicopters.py, 17 | dcs/planes.py, 18 | dcs/ships.py, 19 | dcs/statics.py, 20 | dcs/vehicles.py, 21 | dcs/weapons_data.py, 22 | 23 | per-file-ignores = 24 | __init__.py:F401 25 | syria.py:E126 26 | thechannel.py:E126 27 | persiangulf.py:E126 28 | normandy.py:E126 29 | nevada.py:E126 30 | caucasus.py:E126 31 | marianaislands.py:E126 32 | dcs/lua/test_parse.py:E501 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from codecs import open 3 | from os import path 4 | 5 | here = path.abspath(path.dirname(__file__)) 6 | 7 | long_description = '' 8 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 9 | for line in f: 10 | if line.startswith('## Sample'): 11 | break 12 | long_description += line 13 | 14 | setup( 15 | name="pydcs", 16 | version='0.15.0', 17 | description="A Digital Combat Simulator mission builder framework", 18 | long_description=long_description, 19 | url='https://github.com/pydcs/dcs', 20 | author="Peinthor Rene", 21 | author_email="peinthor@gmail.com", 22 | license="LGPLv3", 23 | classifiers=[ 24 | 'Development Status :: 4 - Beta', 25 | 'Topic :: Games/Entertainment :: Simulation', 26 | 'Intended Audience :: Developers', 27 | 'Intended Audience :: End Users/Desktop', 28 | 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', 29 | 'Programming Language :: Python :: 3.8', 30 | 'Programming Language :: Python :: 3.9', 31 | 'Programming Language :: Python :: 3.10', 32 | 'Programming Language :: Python :: 3.11', 33 | 'Programming Language :: Python :: 3 :: Only' 34 | ], 35 | keywords='dcs digital combat simulator eagle dynamics mission framework', 36 | install_requires=[ 37 | 'pyproj' 38 | ], 39 | packages=[ 40 | 'dcs', 41 | 'dcs/drawing', 42 | 'dcs/liveries', 43 | 'dcs/lua', 44 | 'dcs/scripts', 45 | 'dcs/terrain', 46 | 'dcs/terrain/caucasus', 47 | 'dcs/terrain/falklands', 48 | 'dcs/terrain/marianaislands', 49 | 'dcs/terrain/nevada', 50 | 'dcs/terrain/normandy', 51 | 'dcs/terrain/persiangulf', 52 | 'dcs/terrain/projections', 53 | 'dcs/terrain/sinai', 54 | 'dcs/terrain/syria', 55 | 'dcs/terrain/thechannel', 56 | ], 57 | package_data={ 58 | 'dcs': ['py.typed'], 59 | 'dcs/terrain': ['caucasus.p', 'nevada.p'], 60 | }, 61 | entry_points={ 62 | 'console_scripts': [ 63 | 'dcs_random=dcs.scripts.caucasus_random_mission:main', 64 | 'dcs_dogfight_wwii=dcs.scripts.dogfight_wwii:main', 65 | 'dcs_oil_convoy=dcs.scripts.destroy_oil_transport:main' 66 | ] 67 | }, 68 | test_suite="tests" 69 | ) 70 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from dcs.lua.test_parse import * -------------------------------------------------------------------------------- /tests/bypass_triggers.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/bypass_triggers.miz -------------------------------------------------------------------------------- /tests/images/blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/images/blue.png -------------------------------------------------------------------------------- /tests/images/m1/briefing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/images/m1/briefing.png -------------------------------------------------------------------------------- /tests/images/m2/briefing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/images/m2/briefing.png -------------------------------------------------------------------------------- /tests/liveries/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/liveries/__init__.py -------------------------------------------------------------------------------- /tests/liveries/test_livery.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from zipfile import ZipFile 3 | 4 | from dcs.liveries.livery import Livery 5 | from dcs.liveries.liveryset import LiverySet 6 | 7 | 8 | def test_placeholder_livery() -> None: 9 | """Tests that liveries named "placeholder" are ignored.""" 10 | assert Livery.from_lua("", "placeholder") is None 11 | assert Livery.from_lua("", "placeholder.zip") is None 12 | 13 | 14 | def test_named_livery() -> None: 15 | """Tests description decoding when the livery lua sets the name.""" 16 | livery = Livery.from_lua('name = "Foobar"', "foobar") 17 | assert livery is not None 18 | assert livery.name == "Foobar" 19 | 20 | 21 | def test_livery_defaults() -> None: 22 | """Tests description decoding when the livery is not explicitly named.""" 23 | livery = Livery.from_lua("", "foobar") 24 | assert livery is not None 25 | assert livery.name == "foobar" 26 | assert livery.countries is None 27 | assert livery.order == 0 28 | 29 | 30 | def test_livery_id_from_directory() -> None: 31 | """Tests that the livery ID is set appropriately for a directory livery.""" 32 | livery = Livery.from_lua("", str(Path("foo/bar"))) 33 | assert livery is not None 34 | assert livery.id == "bar" 35 | 36 | 37 | def test_livery_id_from_zip() -> None: 38 | """Tests that the livery ID for a zipped livery does not include .zip.""" 39 | livery = Livery.from_lua("", str(Path("foo/bar.zip"))) 40 | assert livery is not None 41 | assert livery.id == "bar" 42 | 43 | 44 | def test_parse_single_country() -> None: 45 | livery = Livery.from_lua('countries = {"USA"}', "foobar") 46 | assert livery is not None 47 | assert livery.countries == {"USA"} 48 | 49 | 50 | def test_parse_multiple_countries() -> None: 51 | livery = Livery.from_lua('countries = {"USA", "RUS"}', "foobar") 52 | assert livery is not None 53 | assert livery.countries == {"USA", "RUS"} 54 | 55 | 56 | def test_sort_order() -> None: 57 | livery = Livery.from_lua("order = 2", "foobar") 58 | assert livery is not None 59 | livery.order == 2 60 | 61 | 62 | def test_livery_valid_for_country() -> None: 63 | assert Livery("", "", 0, None).valid_for_country("fadfasdf") 64 | assert Livery("", "", 0, {"USA"}).valid_for_country("USA") 65 | assert not Livery("", "", 0, {"USA"}).valid_for_country("RUS") 66 | assert Livery("", "", 0, {"USA", "RUS"}).valid_for_country("RUS") 67 | 68 | 69 | def test_ansi_encoded_description(tmp_path: Path) -> None: 70 | (tmp_path / "description.lua").write_bytes("name = 'Š'".encode("ansi")) 71 | livery = Livery.from_path(str(tmp_path)) 72 | assert livery is not None 73 | assert livery.name == "Š" 74 | 75 | 76 | def test_find_unzipped_livery(tmp_path: Path) -> None: 77 | description = tmp_path / "description.lua" 78 | description.touch() 79 | assert Livery.from_path(str(tmp_path)) is not None 80 | 81 | 82 | def test_find_zipped_livery(tmp_path: Path) -> None: 83 | zip_path = tmp_path / "foo.zip" 84 | with ZipFile(zip_path, "w") as zip_file: 85 | with zip_file.open("description.lua", "w") as description: 86 | pass 87 | assert Livery.from_path(str(zip_path)) is not None 88 | 89 | 90 | def test_livery_id_forced_lower_case(tmp_path: Path) -> None: 91 | path = tmp_path / "FOO" 92 | path.mkdir() 93 | description = path / "description.lua" 94 | description.touch() 95 | livery = Livery.from_path(str(path)) 96 | assert livery is not None 97 | assert livery.id.islower() 98 | -------------------------------------------------------------------------------- /tests/liveries/test_liveryscanner.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Iterator 3 | from unittest.mock import ANY, Mock, call, patch 4 | 5 | import pytest 6 | 7 | from dcs.liveries.livery import Livery 8 | from dcs.liveries.liveryscanner import LiveryScanner 9 | 10 | 11 | @pytest.fixture(name="livery_from_path_mock") 12 | def livery_from_path_mock_fixture() -> Iterator[Mock]: 13 | with patch("dcs.liveries.livery.Livery.from_path") as mock: 14 | mock.return_value = Livery("", "", 0, None) 15 | yield mock 16 | 17 | 18 | @pytest.fixture(name="scan_liveries_mock") 19 | def scan_liveries_mock_fixture() -> Iterator[Mock]: 20 | with patch("dcs.liveries.liveryscanner.LiveryScanner.scan_liveries") as mock: 21 | yield mock 22 | 23 | 24 | @pytest.fixture(name="register_livery_mock") 25 | def register_livery_mock_fixture() -> Iterator[Mock]: 26 | with patch("dcs.liveries.liveryscanner.LiveryScanner.register_livery") as mock: 27 | yield mock 28 | 29 | 30 | def test_find_multiple_liveries(tmp_path: Path, livery_from_path_mock: Mock) -> None: 31 | description0 = tmp_path / "A-10CII/foobar/description.lua" 32 | description0.parent.mkdir(parents=True) 33 | description0.write_text("desc") 34 | 35 | description1 = tmp_path / "A-10CII/baz/description.lua" 36 | description1.parent.mkdir(parents=True) 37 | description1.write_text("desc") 38 | 39 | LiveryScanner().scan_liveries(str(tmp_path)) 40 | livery_from_path_mock.assert_has_calls( 41 | [ 42 | call(str(description0.parent)), 43 | call(str(description1.parent)), 44 | ], 45 | any_order=True, 46 | ) 47 | 48 | 49 | def test_find_liveries_for_multiple_aircraft( 50 | tmp_path: Path, livery_from_path_mock: Mock 51 | ) -> None: 52 | description0 = tmp_path / "A-10CII/foobar/description.lua" 53 | description0.parent.mkdir(parents=True) 54 | description0.write_text("desc") 55 | 56 | description1 = tmp_path / "A-10CIII/foobar/description.lua" 57 | description1.parent.mkdir(parents=True) 58 | description1.write_text("desc") 59 | 60 | LiveryScanner().scan_liveries(str(tmp_path)) 61 | livery_from_path_mock.assert_has_calls( 62 | [ 63 | call(str(description0.parent)), 64 | call(str(description1.parent)), 65 | ], 66 | any_order=True, 67 | ) 68 | 69 | 70 | def test_find_no_liveries_for_aircraft( 71 | tmp_path: Path, livery_from_path_mock: Mock 72 | ) -> None: 73 | (tmp_path / "A-10CII").mkdir(parents=True) 74 | LiveryScanner().scan_liveries(str(tmp_path)) 75 | livery_from_path_mock.assert_not_called() 76 | 77 | 78 | def test_find_no_aircraft_in_liveries_dir( 79 | tmp_path: Path, livery_from_path_mock: Mock 80 | ) -> None: 81 | LiveryScanner().scan_liveries(str(tmp_path)) 82 | livery_from_path_mock.assert_not_called() 83 | 84 | 85 | def test_ignore_cockpit_liveries(tmp_path: Path) -> None: 86 | description = tmp_path / "A-10CII_COCKPIT/foobar/description.lua" 87 | description.parent.mkdir(parents=True) 88 | description.write_text("desc") 89 | 90 | with patch("dcs.liveries.liveryset.LiverySet.add") as mock: 91 | LiveryScanner().scan_liveries(str(tmp_path)) 92 | mock.assert_not_called() 93 | 94 | 95 | def test_case_insensitive_unit_id(tmp_path: Path, register_livery_mock: Mock) -> None: 96 | description = tmp_path / "a-10Cii/foobar/description.lua" 97 | description.parent.mkdir(parents=True) 98 | description.touch() 99 | LiveryScanner().scan_liveries(str(tmp_path)) 100 | register_livery_mock.assert_called_once_with("A-10CII", ANY) 101 | 102 | 103 | def test_campaign_alias_livery(tmp_path: Path, register_livery_mock: Mock) -> None: 104 | description = tmp_path / "FW-190D-9/foobar/description.lua" 105 | description.parent.mkdir(parents=True) 106 | description.touch() 107 | LiveryScanner().scan_liveries(str(tmp_path), campaign_path=True) 108 | register_livery_mock.assert_called_once_with("FW-190D9", ANY) 109 | 110 | 111 | def test_campaign_alias_livery_only_for_campaign_paths( 112 | tmp_path: Path, register_livery_mock: Mock 113 | ) -> None: 114 | description = tmp_path / "FW-190D-9/foobar/description.lua" 115 | description.parent.mkdir(parents=True) 116 | description.touch() 117 | LiveryScanner().scan_liveries(str(tmp_path)) 118 | register_livery_mock.assert_called_once_with("FW-190D-9", ANY) 119 | 120 | 121 | def test_scan_mod_livery_directory(tmp_path: Path, scan_liveries_mock: Mock) -> None: 122 | paths = [ 123 | tmp_path / "foo/Liveries", 124 | tmp_path / "bar", 125 | tmp_path / "baz/Liveries", 126 | ] 127 | for path in paths: 128 | path.mkdir(parents=True) 129 | 130 | LiveryScanner().scan_mods_path(str(tmp_path)) 131 | scan_liveries_mock.assert_has_calls( 132 | [ 133 | call(str(paths[0])), 134 | call(str(paths[2])), 135 | ], 136 | any_order=True, 137 | ) 138 | 139 | 140 | def test_scan_empty_mod_livery_directory( 141 | tmp_path: Path, scan_liveries_mock: Mock 142 | ) -> None: 143 | LiveryScanner().scan_mods_path(str(tmp_path / "nonexistent")) 144 | scan_liveries_mock.assert_not_called() 145 | 146 | 147 | def test_scan_campaign_livery_directory( 148 | tmp_path: Path, scan_liveries_mock: Mock 149 | ) -> None: 150 | paths = [ 151 | tmp_path / "foo/Liveries", 152 | tmp_path / "bar", 153 | tmp_path / "baz/Liveries", 154 | ] 155 | for path in paths: 156 | path.mkdir(parents=True) 157 | 158 | LiveryScanner().scan_campaign_liveries(str(tmp_path)) 159 | scan_liveries_mock.assert_has_calls( 160 | [ 161 | call(str(paths[0]), campaign_path=True), 162 | call(str(paths[2]), campaign_path=True), 163 | ], 164 | any_order=True, 165 | ) 166 | 167 | 168 | def test_scan_empty_campaign_livery_directory( 169 | tmp_path: Path, scan_liveries_mock: Mock 170 | ) -> None: 171 | LiveryScanner().scan_campaign_liveries(str(tmp_path / "nonexistent")) 172 | scan_liveries_mock.assert_not_called() 173 | 174 | 175 | def test_scan_core_directories(tmp_path: Path, scan_liveries_mock) -> None: 176 | paths = [ 177 | (tmp_path / "CoreMods/aircraft/mod1/Liveries", False), 178 | (tmp_path / "CoreMods/WWII Units/mod2/Liveries", False), 179 | (tmp_path / "Bazar/Liveries", False), 180 | (tmp_path / "Mods/campaigns/campaign/Liveries", True), 181 | (tmp_path / "CoreMods/tech/mod3/Liveries", False), 182 | (tmp_path / "Mods/tech/WWII Units/Liveries", False), 183 | ] 184 | 185 | calls = [] 186 | for path, campaign in paths: 187 | path.mkdir(parents=True) 188 | if campaign: 189 | calls.append(call(str(path), campaign_path=True)) 190 | else: 191 | calls.append(call(str(path))) 192 | 193 | LiveryScanner().scan_dcs_installation(str(tmp_path)) 194 | scan_liveries_mock.assert_has_calls(calls, any_order=True) 195 | 196 | 197 | def test_scan_user_directories(tmp_path: Path, scan_liveries_mock) -> None: 198 | paths = [ 199 | tmp_path / "Mods/aircraft/mod1/Liveries", 200 | tmp_path / "Liveries", 201 | ] 202 | 203 | calls = [] 204 | for path in paths: 205 | path.mkdir(parents=True) 206 | calls.append(call(str(path))) 207 | 208 | LiveryScanner().scan_custom_liveries(str(tmp_path)) 209 | scan_liveries_mock.assert_has_calls(calls, any_order=True) 210 | -------------------------------------------------------------------------------- /tests/loadtest.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/loadtest.miz -------------------------------------------------------------------------------- /tests/missions/Coop_OP_Custodia_v1.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/missions/Coop_OP_Custodia_v1.miz -------------------------------------------------------------------------------- /tests/missions/Draw_tool_test.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/missions/Draw_tool_test.miz -------------------------------------------------------------------------------- /tests/missions/Forestry_Operations.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/missions/Forestry_Operations.miz -------------------------------------------------------------------------------- /tests/missions/LUNA.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/missions/LUNA.miz -------------------------------------------------------------------------------- /tests/missions/Mission_with_required_modules.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/missions/Mission_with_required_modules.miz -------------------------------------------------------------------------------- /tests/missions/SierraHotel_Training_Mission_04.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/missions/SierraHotel_Training_Mission_04.miz -------------------------------------------------------------------------------- /tests/missions/TTI_GC_SC_1.68a.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/missions/TTI_GC_SC_1.68a.miz -------------------------------------------------------------------------------- /tests/missions/a_out_picture.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/missions/a_out_picture.miz -------------------------------------------------------------------------------- /tests/missions/big-formation-carpet-bombing.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/missions/big-formation-carpet-bombing.miz -------------------------------------------------------------------------------- /tests/missions/big-formation.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/missions/big-formation.miz -------------------------------------------------------------------------------- /tests/missions/countries-without-units-on-the-map.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/missions/countries-without-units-on-the-map.miz -------------------------------------------------------------------------------- /tests/missions/g-effect-game.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/missions/g-effect-game.miz -------------------------------------------------------------------------------- /tests/missions/g-effect-none.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/missions/g-effect-none.miz -------------------------------------------------------------------------------- /tests/missions/g-effect-sim.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/missions/g-effect-sim.miz -------------------------------------------------------------------------------- /tests/missions/g-effect-uncheked.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/missions/g-effect-uncheked.miz -------------------------------------------------------------------------------- /tests/missions/linked-trigger-zone.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/missions/linked-trigger-zone.miz -------------------------------------------------------------------------------- /tests/missions/payload.restrictions.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tests/missions/payload.restrictions.miz -------------------------------------------------------------------------------- /tests/test_country.py: -------------------------------------------------------------------------------- 1 | import dcs.countries 2 | 3 | 4 | def test_countries_eq() -> None: 5 | assert dcs.countries.get_by_id(0) == dcs.countries.get_by_id(0) 6 | assert dcs.countries.get_by_id(0) != dcs.countries.get_by_id(1) 7 | assert dcs.countries.get_by_id(0) != "dynamic typing is stupid" 8 | 9 | 10 | def test_country_by_name() -> None: 11 | dcs.countries.get_by_name("Russia") == dcs.countries.get_by_id(0) 12 | 13 | 14 | def test_country_by_short_name() -> None: 15 | dcs.countries.get_by_short_name("RUS") == dcs.countries.get_by_id(0) 16 | -------------------------------------------------------------------------------- /tests/test_drawings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import dcs 4 | from dcs.drawing.drawing import LineStyle, Rgba 5 | from dcs.drawing.drawings import StandardLayer 6 | from dcs.drawing.icon import StandardIcon 7 | from dcs.drawing.polygon import Circle 8 | from dcs.mapping import Point 9 | from dcs.mission import Mission 10 | 11 | 12 | class DrawingTests(unittest.TestCase): 13 | def setUp(self): 14 | os.makedirs('missions', exist_ok=True) 15 | 16 | def test_load_save_load(self) -> None: 17 | m: Mission = dcs.mission.Mission() 18 | self.assertEqual(0, len(m.load_file('tests/missions/Draw_tool_test.miz'))) 19 | self.assert_expected_stuff(m) 20 | 21 | mission_path = 'missions/Draw_tool_test_saved.miz' 22 | m.save(mission_path) 23 | 24 | m2 = dcs.mission.Mission() 25 | self.assertEqual(0, len(m2.load_file(mission_path))) 26 | self.assert_expected_stuff(m2) 27 | 28 | def assert_expected_stuff(self, m: Mission) -> None: 29 | self.assertEqual(5, len(m.drawings.layers)) 30 | self.assertEqual(False, m.drawings.options.hiddenOnF10Map["Observer"]["Neutral"]) 31 | red_layer = m.drawings.get_layer(StandardLayer.Red) 32 | self.assertEqual(True, red_layer.visible) 33 | self.assertEqual("Red", red_layer.name) 34 | self.assertEqual("Icon 2", red_layer.objects[0].name) 35 | 36 | line = m.drawings.get_layer(StandardLayer.Blue).objects[0] 37 | self.assertEqual("Line 2 segments closed", line.name) 38 | 39 | self.assertEqual(Rgba(255, 255, 0, 131), line.color) 40 | self.assertEqual(-260885.56415634, line.position.x) 41 | self.assertEqual(671996.90379981, line.position.y) 42 | self.assertEqual(0, line.points[0].x) 43 | self.assertEqual(0, line.points[0].y) 44 | self.assertEqual(-6076.521389334, line.points[2].x) 45 | self.assertEqual(3038.260694667, line.points[2].y) 46 | 47 | 48 | def test_add_drawings_to_loaded_mission(self) -> None: 49 | m: Mission = dcs.mission.Mission() 50 | self.assertEqual(0, len(m.load_file('tests/missions/Draw_tool_test.miz'))) 51 | 52 | circle = Circle( 53 | True, 54 | Point(10, 10, m.terrain), 55 | "TEST CIRCLE", 56 | Rgba(20, 30, 40, 200), 57 | ":S", 58 | Rgba(50, 60, 70, 150), 59 | 10, 60 | LineStyle.Solid, 61 | 100 62 | ) 63 | m.drawings.layers[0].add_drawing(circle) 64 | self.assertEqual("TEST CIRCLE", m.drawings.layers[0].objects[1].name) 65 | 66 | mission_path = 'missions/Draw_tool_test_added_drawings.miz' 67 | m.save(mission_path) 68 | 69 | m2 = dcs.mission.Mission() 70 | self.assertEqual(0, len(m2.load_file(mission_path))) 71 | self.assert_expected_stuff(m2) 72 | self.assertEqual("TEST CIRCLE", m2.drawings.layers[0].objects[1].name) 73 | 74 | 75 | def test_add_drawings_to_new_mission(self) -> None: 76 | m: Mission = dcs.mission.Mission() 77 | 78 | circle = Circle( 79 | True, 80 | Point(10, 10, m.terrain), 81 | "TEST CIRCLE", 82 | Rgba(20, 30, 40, 200), 83 | ":S", 84 | Rgba(50, 60, 70, 150), 85 | 10, 86 | LineStyle.Solid, 87 | 100 88 | ) 89 | red_layer = m.drawings.get_layer(StandardLayer.Red) 90 | red_layer.add_drawing(circle) 91 | red_layer.add_line_segments( 92 | Point(1, 1, m.terrain), 93 | [Point(6, 6, m.terrain), Point(7, 7, m.terrain)], 94 | closed=True, 95 | ) 96 | 97 | m.drawings.options.hiddenOnF10Map["Pilot"]["Red"] = True 98 | m.drawings.options.hiddenOnF10Map["Instructor"]["Blue"] = True 99 | m.drawings.options.hiddenOnF10Map["Observer"]["Neutral"] = True 100 | 101 | mission_path = 'missions/New_mission_w_added_drawings.miz' 102 | m.save(mission_path) 103 | 104 | m2 = dcs.mission.Mission() 105 | self.assertEqual(0, len(m2.load_file(mission_path))) 106 | red_layer2 = m2.drawings.get_layer(StandardLayer.Red) 107 | self.assertEqual("TEST CIRCLE", red_layer2.objects[0].name) 108 | self.assertEqual("A line", red_layer2.objects[1].name) 109 | self.assertEqual(True, red_layer2.objects[1].closed) 110 | self.assertEqual("Red", red_layer2.objects[0].layer_name) 111 | self.assertEqual("Red", red_layer2.objects[1].layer_name) 112 | 113 | 114 | def test_set_options_hidden_f10(self) -> None: 115 | m: Mission = dcs.mission.Mission() 116 | 117 | m.drawings.options.hiddenOnF10Map["Pilot"]["Red"] = True 118 | m.drawings.options.hiddenOnF10Map["Instructor"]["Blue"] = True 119 | m.drawings.options.hiddenOnF10Map["Observer"]["Neutral"] = True 120 | mission_path = 'missions/New_mission_w_added_drawings.miz' 121 | m.save(mission_path) 122 | 123 | m2 = dcs.mission.Mission() 124 | self.assertEqual(0, len(m2.load_file(mission_path))) 125 | self.assertEqual(False, m2.drawings.options.hiddenOnF10Map["Pilot"]["Blue"]) 126 | self.assertEqual(True, m2.drawings.options.hiddenOnF10Map["Pilot"]["Red"]) 127 | self.assertEqual(True, m2.drawings.options.hiddenOnF10Map["Instructor"]["Blue"]) 128 | self.assertEqual(True, m2.drawings.options.hiddenOnF10Map["Observer"]["Neutral"]) 129 | 130 | def test_add_std_icon(self) -> None: 131 | m: Mission = dcs.mission.Mission() 132 | 133 | red_layer = m.drawings.get_layer(StandardLayer.Red) 134 | red_layer.add_icon( 135 | Point(1000, 1000, m.terrain), 136 | StandardIcon.MechanizedArtillery, 137 | ) 138 | mission_path = 'missions/New_mission_w_added_std_icon.miz' 139 | m.save(mission_path) 140 | 141 | m2 = dcs.mission.Mission() 142 | self.assertEqual(0, len(m2.load_file(mission_path))) 143 | red_layer = m.drawings.get_layer(StandardLayer.Red) 144 | 145 | self.assertEqual(StandardIcon.MechanizedArtillery.value, red_layer.objects[0].file) 146 | 147 | def test_add_oblong(self) -> None: 148 | m: Mission = dcs.mission.Mission() 149 | 150 | layer = m.drawings.get_layer(StandardLayer.Common) 151 | self.assertEqual(0, len(layer.objects)) 152 | oblong = layer.add_oblong( 153 | Point(1000, 1000, m.terrain), 154 | Point(4000, 1000, m.terrain), 155 | 1000, 156 | resolution=20, 157 | ) 158 | self.assertEqual(1, len(layer.objects)) 159 | # Resolution 20 should give 43 points 160 | # (21 in each end and one extra to close polygon) 161 | self.assertEqual(43, len(oblong.points)) 162 | -------------------------------------------------------------------------------- /tests/test_installation.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | from pathlib import Path 3 | from typing import Any, Callable, Iterator, Optional, TypeVar 4 | from unittest.mock import Mock, patch 5 | 6 | import pytest 7 | 8 | from dcs.installation import ( 9 | DCS_BETA_REGISTRY_KEY_NAME, 10 | DCS_STABLE_REGISTRY_KEY_NAME, 11 | STEAM_REGISTRY_KEY_NAME, 12 | get_dcs_install_directory, 13 | get_dcs_saved_games_directory, 14 | ) 15 | 16 | T = TypeVar("T") 17 | 18 | 19 | def configure_registry_mock( 20 | mock: Mock, 21 | steam: Optional[Path] = None, 22 | standalone_stable: Optional[Path] = None, 23 | standalone_beta: Optional[Path] = None, 24 | ) -> None: 25 | def registry_mock( 26 | key: str, value: str, ctor: Callable[[Any], T] = lambda x: x 27 | ) -> Optional[T]: 28 | if ( 29 | key == STEAM_REGISTRY_KEY_NAME 30 | and value == "SteamPath" 31 | and steam is not None 32 | ): 33 | return ctor(steam) 34 | if ( 35 | key == DCS_STABLE_REGISTRY_KEY_NAME 36 | and value == "Path" 37 | and standalone_stable is not None 38 | ): 39 | return ctor(standalone_stable) 40 | if ( 41 | key == DCS_BETA_REGISTRY_KEY_NAME 42 | and value == "Path" 43 | and standalone_beta is not None 44 | ): 45 | return ctor(standalone_beta) 46 | return None 47 | 48 | mock.side_effect = registry_mock 49 | 50 | 51 | @pytest.fixture(name="steam_dcs_install") 52 | def steam_dcs_install_fixture(tmp_path: Path) -> Iterator[Path]: 53 | escaped_path = str(tmp_path).replace("\\", "\\\\") 54 | vdf_path = tmp_path / "steamapps/libraryfolders.vdf" 55 | vdf_path.parent.mkdir(parents=True) 56 | vdf_path.write_text( 57 | textwrap.dedent( 58 | f"""\ 59 | "LibraryFolders" 60 | {{ 61 | "TimeNextStatsReport" "1561832478" 62 | "ContentStatsID" "-158337411110787451" 63 | "1" "D:\\\\Games\\\\Steam" 64 | "2" "{escaped_path}" 65 | }} 66 | """ 67 | ) 68 | ) 69 | 70 | dcs_install_path = tmp_path / "steamapps/common/DCSWorld" 71 | dcs_install_path.mkdir(parents=True) 72 | 73 | with patch("dcs.installation.read_current_user_value") as mock: 74 | configure_registry_mock(mock, steam=tmp_path) 75 | yield dcs_install_path 76 | 77 | 78 | @pytest.fixture(name="stable_dcs_install") 79 | def stable_dcs_install_fixture(tmp_path: Path) -> Iterator[Path]: 80 | with patch("dcs.installation.read_current_user_value") as mock: 81 | configure_registry_mock(mock, standalone_stable=tmp_path) 82 | yield tmp_path 83 | 84 | 85 | @pytest.fixture(name="beta_dcs_install") 86 | def beta_dcs_install_fixture(tmp_path: Path) -> Iterator[Path]: 87 | with patch("dcs.installation.read_current_user_value") as mock: 88 | configure_registry_mock(mock, standalone_beta=tmp_path) 89 | yield tmp_path 90 | 91 | 92 | @pytest.fixture(name="none_installed") 93 | def none_installed_fixture() -> Iterator[None]: 94 | with patch("dcs.installation.read_current_user_value") as mock: 95 | configure_registry_mock(mock) 96 | yield 97 | 98 | 99 | def test_get_dcs_install_directory_stable(stable_dcs_install: Path) -> None: 100 | assert get_dcs_install_directory() == f"{stable_dcs_install}\\" 101 | 102 | 103 | def test_get_dcs_install_directory_beta(beta_dcs_install: Path) -> None: 104 | assert get_dcs_install_directory() == f"{beta_dcs_install}\\" 105 | 106 | 107 | def test_get_dcs_install_directory_steam(steam_dcs_install: Path) -> None: 108 | assert get_dcs_install_directory() == f"{steam_dcs_install}\\" 109 | 110 | 111 | def test_get_dcs_install_directory_not_installed(none_installed: None) -> None: 112 | assert get_dcs_install_directory() == "" 113 | 114 | 115 | @patch("os.path.expanduser") 116 | @patch("dcs.installation.get_dcs_install_directory") 117 | def test_get_dcs_saved_games_directory_no_variant_file( 118 | mock_get_dcs_install_directory: Mock, mock_expanduser: Mock, tmp_path: Path 119 | ) -> None: 120 | home_dir = tmp_path / "home" 121 | 122 | mock_expanduser.return_value = str(home_dir) 123 | mock_get_dcs_install_directory.return_value = str(tmp_path) 124 | 125 | assert get_dcs_saved_games_directory() == str(home_dir / "Saved Games/DCS") 126 | 127 | 128 | @patch("os.path.expanduser") 129 | @patch("dcs.installation.get_dcs_install_directory") 130 | def test_get_dcs_saved_games_directory_beta_variant( 131 | mock_get_dcs_install_directory: Mock, mock_expanduser: Mock, tmp_path: Path 132 | ) -> None: 133 | install_dir = tmp_path / "install/steamapps/common/DCSWorld" 134 | install_dir.mkdir(parents=True) 135 | home_dir = tmp_path / "home" 136 | 137 | (install_dir / "dcs_variant.txt").write_text("openbeta") 138 | 139 | mock_expanduser.return_value = str(home_dir) 140 | mock_get_dcs_install_directory.return_value = install_dir 141 | 142 | assert get_dcs_saved_games_directory() == str(home_dir / "Saved Games/DCS.openbeta") 143 | -------------------------------------------------------------------------------- /tests/test_mapping.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from dcs.mapping import Polygon, Point, Rectangle, Triangle 3 | from dcs.terrain import Caucasus 4 | 5 | 6 | class PointTests(unittest.TestCase): 7 | def test_point(self) -> None: 8 | terrain = Caucasus() 9 | 10 | p1 = Point(2, 2, terrain) 11 | p2 = p1 + Point(1, 1, terrain) 12 | 13 | self.assertEqual(p2.x, 3) 14 | self.assertEqual(p2.y, 3) 15 | 16 | p2 = 1 + p1 17 | self.assertEqual(p2.x, 3) 18 | self.assertEqual(p2.y, 3) 19 | 20 | self.assertEqual(3 * p2, Point(9, 9, terrain)) 21 | 22 | self.assertEqual(p2.x, 3) 23 | self.assertEqual(p2.y, 3) 24 | 25 | self.assertEqual(p2 * 0.5, Point(1.5, 1.5, terrain)) 26 | 27 | p2 = p1 - 1 28 | self.assertEqual(p2.x, 1) 29 | self.assertEqual(p2.y, 1) 30 | 31 | p2 = p1 - p2 32 | self.assertEqual(p2.x, 1) 33 | self.assertEqual(p2.y, 1) 34 | 35 | def test_latlng(self) -> None: 36 | terrain = Caucasus() 37 | p = Point(0, 0, terrain) 38 | ll = p.latlng() 39 | self.assertAlmostEqual(ll.lat, 45.129497060328966) 40 | self.assertAlmostEqual(ll.lng, 34.265515188456) 41 | 42 | p2 = Point.from_latlng(ll, terrain) 43 | self.assertAlmostEqual(p2.x, 0) 44 | self.assertAlmostEqual(p2.y, 0) 45 | 46 | def test_point_eq(self) -> None: 47 | terrain = Caucasus() 48 | self.assertEqual(Point(0, 0, terrain), Point(0, 0, terrain)) 49 | self.assertNotEqual(Point(0, 0, terrain), Point(0, 1, terrain)) 50 | self.assertNotEqual(Point(0, 0, terrain), Point(1, 0, terrain)) 51 | self.assertNotEqual(Point(0, 0, terrain), None) 52 | 53 | 54 | class RectangleTests(unittest.TestCase): 55 | def test_rectangle(self) -> None: 56 | terrain = Caucasus() 57 | r = Rectangle(0, 0, 10, 10, terrain) 58 | 59 | self.assertEqual(r.center(), Point(5, 5, terrain)) 60 | 61 | def test_resize(self) -> None: 62 | terrain = Caucasus() 63 | r = Rectangle(0, 0, 10, 10, terrain) 64 | 65 | r2 = r.resize(0.5) 66 | self.assertEqual(r2, Rectangle(2.5, 2.5, 7.5, 7.5, terrain)) 67 | 68 | r2 = r.resize(2) 69 | self.assertEqual(r2, Rectangle(-5, -5, 15, 15, terrain)) 70 | 71 | def test_random_point(self) -> None: 72 | terrain = Caucasus() 73 | r = Rectangle(10, 0, 0, 10, terrain) 74 | 75 | for i in range(0, 100): 76 | rp = r.random_point() 77 | self.assertTrue(r.point_in_rect(rp)) 78 | 79 | 80 | class TriangleTests(unittest.TestCase): 81 | def test_triangle_random(self) -> None: 82 | terrain = Caucasus() 83 | t = Triangle((Point(0, 5, terrain), Point(1, 2, terrain), Point(3, 1, terrain))) 84 | self.assertIsNotNone(t) 85 | 86 | 87 | class PolygonTests(unittest.TestCase): 88 | def test_poly_triangulation(self) -> None: 89 | terrain = Caucasus() 90 | points = [Point(1, 2, terrain), 91 | Point(3, 1, terrain), 92 | Point(7, 2, terrain), 93 | Point(9, 4, terrain), 94 | Point(6, 6, terrain), 95 | Point(6, 9, terrain), 96 | Point(4, 8, terrain), 97 | Point(2, 9, terrain), 98 | Point(1, 7, terrain), 99 | Point(0, 5, terrain)] 100 | poly = Polygon(terrain, points) 101 | areas = [x.area() for x in poly.triangulate()] 102 | self.assertEqual(areas, [2.5, 9.5, 10.0, 7.5, 9.0, 1.0, 5.0, 0.0]) 103 | 104 | def test_poly_random(self) -> None: 105 | terrain = Caucasus() 106 | points = [ 107 | Point(1, 2, terrain), 108 | Point(3, 1, terrain), 109 | Point(7, 2, terrain), 110 | Point(9, 4, terrain), 111 | Point(6, 6, terrain), 112 | Point(6, 9, terrain), 113 | Point(4, 8, terrain), 114 | Point(2, 9, terrain), 115 | Point(1, 7, terrain), 116 | Point(0, 5, terrain)] 117 | poly = Polygon(terrain, points) 118 | 119 | for i in range(0, 100): 120 | rp = poly.random_point() 121 | self.assertTrue(poly.point_in_poly(rp)) 122 | -------------------------------------------------------------------------------- /tests/test_serialize.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from dcs.lua.serialize import dumps 3 | 4 | class TestLuaSerialize(unittest.TestCase): 5 | 6 | def test_all_keys_are_ints_should_arrange_in_numeric_sequence(self): 7 | original = {9: 9, 10: 10 } 8 | 9 | dumped = dumps(original, 'v') 10 | 11 | self.assertEqual('v={[9]=9,[10]=10}', dumped) 12 | 13 | def test_all_keys_are_string_should_arrange_in_alphabetical_order(self): 14 | original = {"z": 1, "a": 2, "b": 3, "y": 4} 15 | 16 | dumped = dumps(original, 'v') 17 | 18 | self.assertEqual('v={["a"]=2,["b"]=3,["y"]=4,["z"]=1}', dumped) 19 | 20 | def test_some_key_is_string_should_arrange_in_alphabetical_order(self): 21 | original = {3: 2, 10:3, "x": 4, 1: 1} 22 | 23 | dumped = dumps(original, 'v') 24 | 25 | self.assertEqual('v={[1]=1,[10]=3,[3]=2,["x"]=4}', dumped) 26 | -------------------------------------------------------------------------------- /tests/test_terrain.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import dcs 3 | 4 | 5 | class CaucasusTest(unittest.TestCase): 6 | def test_parking_slots(self): 7 | m = dcs.mission.Mission(terrain=dcs.terrain.Caucasus()) 8 | large_slots = m.terrain.airports["Batumi"].free_parking_slots(dcs.planes.KC_135) 9 | self.assertEqual(len(large_slots), 2) 10 | 11 | slots = m.terrain.airports["Batumi"].free_parking_slots(dcs.planes.A_10C) 12 | self.assertEqual(len(slots), len(m.terrain.airports["Batumi"].parking_slots)) 13 | 14 | slot = m.terrain.airports["Batumi"].free_parking_slot(dcs.planes.A_10C) 15 | slot.unit_id = 1 16 | 17 | slots = m.terrain.airports["Batumi"].free_parking_slots(dcs.planes.A_10C) 18 | self.assertEqual(len(slots), 9) 19 | 20 | slot.unit_id = None 21 | slots = m.terrain.airports["Batumi"].free_parking_slots(dcs.planes.A_10C) 22 | self.assertEqual(len(slots), 10) 23 | 24 | def test_parking_large_used(self): 25 | m = dcs.mission.Mission(terrain=dcs.terrain.Caucasus()) 26 | 27 | for x in range(0, 2): 28 | large_slot = m.terrain.airports["Batumi"].free_parking_slot(dcs.planes.KC_135) 29 | self.assertIsNotNone(large_slot) 30 | large_slot.unit_id = x 31 | 32 | self.assertIsNone(m.terrain.airports["Batumi"].free_parking_slot(dcs.planes.KC_135)) 33 | 34 | def test_heli_parking_slots(self): 35 | m = dcs.mission.Mission(terrain=dcs.terrain.Caucasus()) 36 | 37 | self.assertEqual(len(m.terrain.airports["Tbilisi-Lochini"].parking_slots), 74) 38 | 39 | hslots = m.terrain.airports["Tbilisi-Lochini"].free_parking_slots(dcs.helicopters.UH_1H) 40 | self.assertEqual(len(hslots), 44) 41 | 42 | slots = m.terrain.airports["Tbilisi-Lochini"].free_parking_slots(dcs.planes.A_10A) 43 | self.assertEqual(len(slots), 70) 44 | 45 | slot = m.terrain.airports["Tbilisi-Lochini"].free_parking_slot(dcs.planes.A_10C) 46 | slot.unit_id = 1 47 | 48 | hslots = m.terrain.airports["Tbilisi-Lochini"].free_parking_slots(dcs.helicopters.UH_1H) 49 | self.assertEqual(len(hslots), 44) 50 | 51 | def test_parking_mixed_used(self): 52 | m = dcs.mission.Mission(terrain=dcs.terrain.Caucasus()) 53 | 54 | used = [] 55 | for x in range(0, 1): 56 | large_slot = m.terrain.airports["Batumi"].free_parking_slot(dcs.planes.KC_135) 57 | self.assertIsNotNone(large_slot) 58 | large_slot.unit_id = x 59 | used.append(large_slot) 60 | 61 | self.assertIsNotNone(m.terrain.airports["Batumi"].free_parking_slot(dcs.planes.KC_135)) 62 | 63 | for x in range(3, 10): 64 | slot = m.terrain.airports["Batumi"].free_parking_slot(dcs.planes.A_10A) 65 | self.assertIsNotNone(slot) 66 | slot.unit_id = x 67 | used.append(slot) 68 | 69 | slot = m.terrain.airports["Batumi"].free_parking_slot(dcs.planes.A_10A) 70 | 71 | self.assertEqual(len(used)+2, len(m.terrain.airports["Batumi"].parking_slots)) 72 | 73 | 74 | class NevadaTest(unittest.TestCase): 75 | 76 | def test_parking_slots(self): 77 | m = dcs.mission.Mission(terrain=dcs.terrain.Nevada()) 78 | slots = m.terrain.airports["Nellis"].free_parking_slots(dcs.planes.A_10C) 79 | self.assertEqual(len(slots), 104) 80 | 81 | slot = m.terrain.airports["Nellis"].free_parking_slot(dcs.planes.A_10C) 82 | slot.unit_id = 1 83 | 84 | slots = m.terrain.airports["Nellis"].free_parking_slots(dcs.planes.A_10C) 85 | self.assertEqual(len(slots), 103) 86 | 87 | slot.unit_id = None 88 | slots = m.terrain.airports["Nellis"].free_parking_slots(dcs.planes.A_10C) 89 | self.assertEqual(len(slots), 104) 90 | 91 | hslots = m.terrain.airports["Nellis"].free_parking_slots(dcs.helicopters.UH_1H) 92 | self.assertEqual(len(hslots), 51) 93 | 94 | slots = m.terrain.airports["Nellis"].free_parking_slots(dcs.planes.KC_135) 95 | 96 | 97 | class NormandyTest(unittest.TestCase): 98 | 99 | def test_creation(self): 100 | m = dcs.mission.Mission(terrain=dcs.terrain.Normandy()) 101 | self.assertIsInstance(m.terrain, dcs.terrain.Normandy) 102 | 103 | 104 | class SyriaTest(unittest.TestCase): 105 | 106 | def test_airplane_parking_used(self): 107 | m = dcs.mission.Mission(terrain=dcs.terrain.Syria()) 108 | 109 | self.assertEqual(len(m.terrain.airports["Aleppo"].parking_slots), 23) 110 | 111 | hslots = m.terrain.airports["Aleppo"].free_parking_slots(dcs.helicopters.UH_1H) 112 | self.assertEqual(len(hslots), 17) 113 | 114 | slots = m.terrain.airports["Aleppo"].free_parking_slots(dcs.planes.A_10A) 115 | self.assertEqual(len(slots), 9) 116 | 117 | for x in range(0, 9): 118 | airplane_slot = m.terrain.airports["Aleppo"].free_parking_slot(dcs.planes.A_10A) 119 | self.assertIsNotNone(airplane_slot) 120 | airplane_slot.unit_id = x 121 | 122 | self.assertIsNone(m.terrain.airports["Aleppo"].free_parking_slot(dcs.planes.A_10A)) 123 | 124 | hslots = m.terrain.airports["Aleppo"].free_parking_slots(dcs.helicopters.UH_1H) 125 | self.assertEqual(len(hslots), 8) 126 | 127 | -------------------------------------------------------------------------------- /tests/test_unittype.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | from pathlib import Path 3 | 4 | import dcs.countries 5 | from dcs.liveries.livery import Livery 6 | from dcs.liveries.liverycache import LiveryCache 7 | from dcs.liveries.liveryscanner import LiveryScanner 8 | from dcs.planes import F_16C_50 9 | 10 | 11 | def test_plane_liveries(tmp_path: Path) -> None: 12 | dcs_install = tmp_path / "install" 13 | saved_games = tmp_path / "savedgames" 14 | 15 | viper_liveries = ( 16 | dcs_install 17 | / "CoreMods/aircraft" 18 | / F_16C_50.id 19 | / "Liveries" 20 | / F_16C_50.livery_name 21 | ) 22 | 23 | foo_livery = viper_liveries / "foo" 24 | foo_livery.mkdir(parents=True) 25 | (foo_livery / "description.lua").touch() 26 | 27 | bar_livery = viper_liveries / "bar" 28 | bar_livery.mkdir(parents=True) 29 | (bar_livery / "description.lua").touch() 30 | 31 | LiveryCache._cache = LiveryScanner().load_from(str(dcs_install), str(saved_games)) 32 | 33 | expected = { 34 | Livery("foo", "foo", 0, None), 35 | Livery("bar", "bar", 0, None), 36 | } 37 | assert set(F_16C_50.iter_liveries()) == expected 38 | 39 | 40 | def test_plane_liveries_for_country(tmp_path: Path) -> None: 41 | dcs_install = tmp_path / "install" 42 | saved_games = tmp_path / "savedgames" 43 | 44 | viper_liveries = ( 45 | dcs_install 46 | / "CoreMods/aircraft" 47 | / F_16C_50.id 48 | / "Liveries" 49 | / F_16C_50.livery_name 50 | ) 51 | 52 | foo_livery = viper_liveries / "foo" 53 | foo_livery.mkdir(parents=True) 54 | (foo_livery / "description.lua").write_text( 55 | textwrap.dedent( 56 | """\ 57 | countries = {"USA", "FRA"} 58 | """ 59 | ) 60 | ) 61 | 62 | bar_livery = viper_liveries / "bar" 63 | bar_livery.mkdir(parents=True) 64 | (bar_livery / "description.lua").write_text( 65 | textwrap.dedent( 66 | """\ 67 | countries = {"RUS"} 68 | """ 69 | ) 70 | ) 71 | 72 | baz_livery = viper_liveries / "baz" 73 | baz_livery.mkdir(parents=True) 74 | (baz_livery / "description.lua").touch() 75 | 76 | LiveryCache._cache = LiveryScanner().load_from(str(dcs_install), str(saved_games)) 77 | 78 | expected = { 79 | Livery("foo", "foo", 0, None), 80 | Livery("baz", "baz", 0, None), 81 | } 82 | assert ( 83 | set(F_16C_50.iter_liveries_for_country(dcs.countries.get_by_short_name("USA"))) 84 | == expected 85 | ) 86 | -------------------------------------------------------------------------------- /tests/test_weather.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from dcs.terrain import Caucasus 4 | from dcs.weather import CloudPreset, Weather 5 | from dcs.cloud_presets import Clouds 6 | 7 | 8 | class CloudPresetTests(unittest.TestCase): 9 | def test_validate_base(self) -> None: 10 | preset = CloudPreset("test preset", "", "", 2, 3) 11 | 12 | with self.assertRaises(ValueError): 13 | preset.validate_base(1) 14 | 15 | preset.validate_base(2) 16 | preset.validate_base(3) 17 | 18 | with self.assertRaises(ValueError): 19 | preset.validate_base(4) 20 | 21 | def test_by_name(self) -> None: 22 | with self.assertRaises(KeyError): 23 | CloudPreset.by_name("does not exist") 24 | 25 | self.assertEqual(CloudPreset.by_name("Preset1"), Clouds.LightScattered1.value) 26 | 27 | 28 | class CloudsTests(unittest.TestCase): 29 | def test_list(self) -> None: 30 | self.assertGreater(len(Clouds), 0) 31 | 32 | def test_enum(self) -> None: 33 | self.assertIsNotNone(Clouds.LightScattered1) 34 | 35 | 36 | class WeatherTests(unittest.TestCase): 37 | def test_old_clouds(self) -> None: 38 | weather = Weather(Caucasus()) 39 | clouds = weather.dict()["clouds"] 40 | self.assertNotIn("preset", clouds) 41 | 42 | def test_cloud_presets(self) -> None: 43 | weather = Weather(Caucasus()) 44 | weather.clouds_preset = Clouds.LightScattered1.value 45 | weather.clouds_base = 1000 46 | weather_dict = weather.dict() 47 | clouds = weather.dict()["clouds"] 48 | 49 | self.assertEqual(clouds["preset"], "Preset1") 50 | self.assertEqual(clouds["base"], 1000) 51 | self.assertEqual(clouds["thickness"], 200) 52 | self.assertEqual(clouds["density"], 0) 53 | self.assertEqual(clouds["iprecptns"], 0) 54 | 55 | weather.clouds_base = 0 56 | with self.assertRaises(ValueError): 57 | weather.dict() 58 | 59 | weather_dict["clouds"]["preset"] = "Preset2" 60 | weather.load_from_dict(weather_dict) 61 | self.assertEqual(weather.clouds_preset.name, "Preset2") 62 | 63 | del weather_dict["clouds"]["preset"] 64 | weather.load_from_dict(weather_dict) 65 | self.assertIsNone(weather.clouds_preset) 66 | -------------------------------------------------------------------------------- /tools/MissionScripting.lua: -------------------------------------------------------------------------------- 1 | --Initialization script for the Mission lua Environment (SSE) 2 | 3 | dofile('Scripts/ScriptingSystem.lua') 4 | 5 | --Sanitize Mission Scripting environment 6 | --This makes unavailable some unsecure functions. 7 | --Mission downloaded from server to client may contain potentialy harmful lua code that may use these functions. 8 | --You can remove the code below and make availble these functions at your own risk. 9 | 10 | -- local function sanitizeModule(name) 11 | -- _G[name] = nil 12 | -- package.loaded[name] = nil 13 | -- end 14 | -- 15 | -- do 16 | -- sanitizeModule('os') 17 | -- sanitizeModule('io') 18 | -- sanitizeModule('lfs') 19 | -- require = nil 20 | -- loadlib = nil 21 | -- end -------------------------------------------------------------------------------- /tools/city_grapher.py: -------------------------------------------------------------------------------- 1 | import dcs 2 | import os 3 | 4 | 5 | def load_graph(mission_file): 6 | m = dcs.mission.Mission() 7 | m.load_file(mission_file) 8 | 9 | graph = dcs.terrain.Graph() 10 | 11 | # add nodes 12 | for g in [x for x in m.country('USA').vehicle_group if x.units[0].type == dcs.vehicles.Armor.APC_AAV_7.id]: 13 | splitname = str(g.name).split(' ') 14 | rating = None 15 | if not splitname[-1] in dcs.terrain.Graph.Edge_indicators \ 16 | and not splitname[-1].startswith('#') \ 17 | and not splitname[-1] == 'shortcut': 18 | rating = g.spawn_probability * 100 19 | graph.add_node(dcs.terrain.Node(str(g.name), rating, dcs.Point(g.position.x, g.position.y))) 20 | 21 | # add building air defence positions 22 | for g in [x for x in m.country('USA').vehicle_group 23 | if x.units[0].type == dcs.vehicles.AirDefence.SAM_Stinger_MANPADS.id]: 24 | nodename = str(g.name) 25 | nodename = nodename.split(' ')[:-1][0] 26 | graph.node(nodename).air_defence_pos_small.append(g.position) 27 | 28 | # add node edges 29 | for g in [x for x in m.country('USA').vehicle_group if x.units[0].type == dcs.vehicles.Armor.APC_AAV_7.id]: 30 | nodename = str(g.name) 31 | splitname = nodename.split(' ') 32 | if splitname[-1] in dcs.terrain.Graph.Edge_indicators or splitname[-1].startswith('#') or splitname[-1] == 'shortcut': 33 | from_node = graph.node(nodename) 34 | 35 | if not nodename.endswith('shortcut'): 36 | mainnode_name = ' '.join(splitname[:-1]) 37 | main_node = graph.node(mainnode_name) 38 | graph.add_edge(from_node, main_node, g.position.distance_to_point(main_node.position)) 39 | graph.add_edge(main_node, from_node, g.position.distance_to_point(main_node.position)) 40 | # print(from_node, main_node) 41 | 42 | targets = str(g.units[0].name) 43 | targets = targets.split(',') 44 | for target in targets: 45 | target = target.strip() 46 | r = target.find('#') 47 | if r >= 0: 48 | target = target[:r].strip() 49 | 50 | if target.endswith('.'): 51 | on_road = False 52 | target = target[:-1] 53 | else: 54 | on_road = True 55 | 56 | # print(from_node, target) 57 | to_node = graph.node(target) 58 | dist = g.position.distance_to_point(to_node.position) 59 | graph.add_edge(from_node, to_node, dist, on_road) 60 | 61 | # print(self.nodes) 62 | 63 | return graph 64 | 65 | 66 | def main(): 67 | basedir = os.path.dirname(__file__) 68 | for x in os.listdir(os.path.join(basedir, 'graph_missions')): 69 | split = x.split('_') 70 | terrainname = split[0] 71 | graph = load_graph(os.path.join(basedir, 'graph_missions', x)) 72 | graph.store_pickle(os.path.join(basedir, '..', 'dcs', 'terrain', '{name}.p'.format(name=terrainname))) 73 | 74 | if __name__ == '__main__': 75 | main() 76 | -------------------------------------------------------------------------------- /tools/coord_export.lua: -------------------------------------------------------------------------------- 1 | local function dump_coords() 2 | local coordinates = {} 3 | local bases = world.getAirbases() 4 | for i = 1, #bases do 5 | local base = bases[i] 6 | point = Airbase.getPoint(base) 7 | lat, lon, alt = coord.LOtoLL(point) 8 | coordinates[Airbase.getName(base)] = { 9 | ["point"] = point, 10 | ["LL"] = { 11 | ["lat"] = lat, 12 | ["lon"] = lon, 13 | ["alt"] = alt, 14 | }, 15 | } 16 | end 17 | 18 | zero = { 19 | ["x"] = 0, 20 | ["y"] = 0, 21 | ["z"] = 0, 22 | } 23 | lat, lon, alt = coord.LOtoLL(zero) 24 | coordinates["zero"] = { 25 | ["point"] = zero, 26 | ["LL"] = { 27 | ["lat"] = lat, 28 | ["lon"] = lon, 29 | ["alt"] = alt, 30 | }, 31 | } 32 | 33 | local fp = io.open(lfs.writedir() .. "\\coords.json", 'w') 34 | fp:write(json:encode(coordinates)) 35 | fp:close() 36 | end 37 | 38 | dump_coords() -------------------------------------------------------------------------------- /tools/graph_missions/caucasus_graph.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tools/graph_missions/caucasus_graph.miz -------------------------------------------------------------------------------- /tools/graph_missions/nevada_graph.miz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydcs/dcs/38989596a8ec20d4095b9d84541d7a7bafa36704/tools/graph_missions/nevada_graph.miz -------------------------------------------------------------------------------- /tools/missiondiff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from datadiff import diff 4 | import argparse 5 | import zipfile 6 | import dcs 7 | 8 | 9 | def main(): 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument("fileA") 12 | parser.add_argument("fileB") 13 | 14 | args = parser.parse_args() 15 | 16 | def loaddict(fname, miz): 17 | with miz.open(fname) as mfile: 18 | data = mfile.read() 19 | data = data.decode() 20 | return dcs.lua.loads(data) 21 | 22 | with zipfile.ZipFile(args.fileA, 'r') as mizA: 23 | missionA = loaddict('mission', mizA) 24 | 25 | with zipfile.ZipFile(args.fileB, 'r') as mizA: 26 | missionB = loaddict('mission', mizA) 27 | 28 | print(diff(missionA, missionB)) 29 | 30 | if __name__ == "__main__": 31 | main() 32 | -------------------------------------------------------------------------------- /tools/polyzone.py: -------------------------------------------------------------------------------- 1 | import dcs 2 | import argparse 3 | import re 4 | from itertools import groupby 5 | 6 | import dcs.triggers 7 | 8 | 9 | def dump_polyzones(mfile: str): 10 | 11 | m = dcs.mission.Mission() 12 | m.load_file(mfile) 13 | 14 | def sortkey(x: dcs.triggers.TriggerZone): 15 | g = re.match(r"([^0-9]*)([0-9]*)", x.name) 16 | if g: 17 | return g.group(1).strip() 18 | 19 | def num(x: dcs.triggers.TriggerZone): 20 | g = re.match(r"([^0-9]*)([0-9]*)", x.name) 21 | if g: 22 | return int(g.group(2) if g.group(2) else 0) 23 | zones = sorted(m.triggers.zones(), key=sortkey) 24 | zonedict = {} 25 | for k, g in groupby(zones, sortkey): 26 | sgroups = sorted(g, key=num) 27 | zonedict[k] = dcs.mapping.Polygon([t.position for t in sgroups]) 28 | 29 | for z in zonedict: 30 | print(z, zonedict[z]) 31 | return [] 32 | 33 | 34 | def main(): 35 | parser = argparse.ArgumentParser() 36 | parser.add_argument("missionfiles", nargs="+") 37 | 38 | args = parser.parse_args() 39 | for file in args.missionfiles: 40 | print(file) 41 | dump_polyzones(file) 42 | return 0 43 | 44 | 45 | if __name__ == "__main__": 46 | main() 47 | -------------------------------------------------------------------------------- /tools/weather_import.py: -------------------------------------------------------------------------------- 1 | """Generates resources/dcs/beacons.json from the DCS installation. 2 | 3 | DCS has a clouds.lua file that defines all the cloud presets introduced in DCS 2.7. 4 | Example below. The only values we need for serialization are the names of the presets 5 | and their min/max preset altitudes, but the names might also be useful to clients. 6 | Translation of names and descriptions are not handled. 7 | 8 | clouds = 9 | { 10 | presets = 11 | { 12 | Preset1 = 13 | { 14 | visibleInGUI = true, 15 | readableName = '01 ##'.. _('Few Scattered Clouds \nMETAR:FEW/SCT 7/8'), 16 | readableNameShort = _('Light Scattered 1'), 17 | thumbnailName = 'Bazar/Effects/Clouds/Thumbnails/cloud_1.png', 18 | levelMap = 'bazar/effects/clouds/cloudsMap01.png', 19 | detailNoiseMapSize = 8500.00, 20 | precipitationPower = -1.00, 21 | presetAltMin = 840.00, 22 | presetAltMax = 4200.00, 23 | layers = { 24 | { 25 | altitudeMin = 2520, 26 | altitudeMax = 3780, 27 | tile = 9.024, 28 | shapeFactor = 0.015, 29 | density = 0.396, 30 | densityGrad = 0.622, 31 | coverage = 0.359, 32 | noiseFreq = 0.384, 33 | noiseBlur = 1.125, 34 | coverageMapFactor = 0.00, 35 | coverageMapUVOffsetX = 0.00, 36 | coverageMapUVOffsetY = 0.00, 37 | }, 38 | { 39 | altitudeMin = 5040, 40 | altitudeMax = 6300, 41 | tile = 5.104, 42 | shapeFactor = 0.132, 43 | density = 0.410, 44 | densityGrad = 1.032, 45 | coverage = 0.308, 46 | noiseFreq = 0.813, 47 | noiseBlur = 1.500, 48 | coverageMapFactor = 0.00, 49 | coverageMapUVOffsetX = 0.00, 50 | coverageMapUVOffsetY = 0.00, 51 | }, 52 | { 53 | altitudeMin = 10500, 54 | altitudeMax = 12180, 55 | tile = 0.880, 56 | shapeFactor = 0.201, 57 | density = 0.000, 58 | densityGrad = 2.000, 59 | coverage = 0.000, 60 | noiseFreq = 3.000, 61 | noiseBlur = 1.500, 62 | coverageMapFactor = 0.00, 63 | coverageMapUVOffsetX = 0.00, 64 | coverageMapUVOffsetY = 0.00, 65 | }, 66 | } 67 | }, 68 | ... 69 | """ 70 | import argparse 71 | from contextlib import contextmanager 72 | import dataclasses 73 | import logging 74 | 75 | try: 76 | import lupa 77 | except ImportError as ex: 78 | raise RuntimeError( 79 | "Run `pip install lupa` to use this tool. It is not included in " 80 | "requirements.txt since most users will not need this dependency." 81 | ) from ex 82 | 83 | from pathlib import Path 84 | import operator 85 | import os 86 | import textwrap 87 | from typing import Dict, Iterable, Union 88 | 89 | from dcs.weather import CloudPreset 90 | 91 | THIS_DIR = Path(__file__).parent.resolve() 92 | SRC_DIR = THIS_DIR.parent 93 | EXPORT_PATH = SRC_DIR / "dcs/cloud_presets.py" 94 | 95 | 96 | @contextmanager 97 | def cd(path: Path): 98 | cwd = os.getcwd() 99 | os.chdir(path) 100 | try: 101 | yield 102 | finally: 103 | os.chdir(cwd) 104 | 105 | 106 | def cloud_presets(dcs_path: Path) -> Iterable[CloudPreset]: 107 | clouds_lua = dcs_path / "Config/Effects/clouds.lua" 108 | logging.info(f"Loading cloud presets from {clouds_lua}") 109 | 110 | with cd(dcs_path): 111 | lua = lupa.LuaRuntime() 112 | 113 | # Define the translation function to a no-op because we don't need it. 114 | lua.execute( 115 | textwrap.dedent( 116 | """\ 117 | function _(key) 118 | return key 119 | end 120 | 121 | """ 122 | ) 123 | ) 124 | 125 | lua.execute(clouds_lua.read_text()) 126 | 127 | for name, data in lua.eval("clouds['presets']").items(): 128 | if not data["visibleInGUI"]: 129 | # Not something choosable in the ME, so skip it. 130 | continue 131 | 132 | yield CloudPreset( 133 | name, 134 | ui_name=data["readableNameShort"], 135 | description=data["readableName"], 136 | min_base=data["presetAltMin"], 137 | max_base=data["presetAltMax"], 138 | ) 139 | 140 | 141 | class Importer: 142 | """Imports cloud presets.""" 143 | 144 | def __init__(self, dcs_path: Path, export_path: Path) -> None: 145 | self.dcs_path = dcs_path 146 | self.export_path = export_path 147 | 148 | def run(self) -> None: 149 | """Exports the beacons for each available terrain mod.""" 150 | self.export_path.parent.mkdir(parents=True, exist_ok=True) 151 | 152 | with self.export_path.open("w") as output: 153 | output.write(textwrap.dedent( 154 | """\ 155 | from enum import Enum, unique 156 | 157 | from dcs.weather import CloudPreset 158 | 159 | 160 | @unique 161 | class Clouds(Enum): 162 | 163 | @staticmethod 164 | def from_name(name: str) -> "Clouds": 165 | return CLOUD_PRESETS[name] 166 | 167 | """ 168 | )) 169 | 170 | presets = sorted(cloud_presets(self.dcs_path), 171 | key=operator.attrgetter("name")) 172 | names = {} 173 | for preset in presets: 174 | name = preset.ui_name.replace(" ", "") 175 | names[preset.name] = name 176 | preset_src = textwrap.dedent( 177 | f"""\ 178 | {name} = CloudPreset( 179 | name={repr(preset.name)}, 180 | ui_name={repr(preset.ui_name)}, 181 | description={repr(preset.description)}, 182 | min_base={repr(preset.min_base)}, 183 | max_base={repr(preset.max_base)}, 184 | ) 185 | 186 | """ 187 | ) 188 | output.write(textwrap.indent(preset_src, " ")) 189 | 190 | output.write("\nCLOUD_PRESETS = {\n") 191 | for name in names: 192 | output.write(f" {repr(name)}: Clouds.{names[name]},\n") 193 | output.write("}\n") 194 | 195 | 196 | def parse_args() -> argparse.Namespace: 197 | """Parses and returns command line arguments.""" 198 | parser = argparse.ArgumentParser() 199 | 200 | def resolved_path(val: str) -> Path: 201 | """Returns the given string as a fully resolved Path.""" 202 | return Path(val).resolve() 203 | 204 | parser.add_argument( 205 | "--export-to", 206 | type=resolved_path, 207 | default=EXPORT_PATH, 208 | help="Output directory for generated JSON files.", 209 | ) 210 | 211 | parser.add_argument( 212 | "dcs_path", 213 | metavar="DCS_PATH", 214 | type=resolved_path, 215 | help="Path to DCS installation.", 216 | ) 217 | 218 | return parser.parse_args() 219 | 220 | 221 | def main() -> None: 222 | """Program entry point.""" 223 | logging.basicConfig(level=logging.DEBUG) 224 | args = parse_args() 225 | Importer(args.dcs_path, args.export_to).run() 226 | 227 | 228 | if __name__ == "__main__": 229 | main() 230 | --------------------------------------------------------------------------------