├── .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 |
--------------------------------------------------------------------------------