From 92c404fbb6497e7a31049d5bd63a017873f24784 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 13 May 2021 18:54:09 -0700 Subject: [PATCH 1/6] Persist DCS configuration across installs. --- changelog.md | 1 + qt_ui/liberation_install.py | 30 +++++++++++++++++------------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/changelog.md b/changelog.md index c8d4200f..66b5d3a1 100644 --- a/changelog.md +++ b/changelog.md @@ -13,6 +13,7 @@ Saves from 2.5 are not compatible with 3.0. * **[Modding]** Campaigns now choose locations for factories to spawn. * **[Modding]** Can now install custom factions to /Liberation/Factions instead of the Liberation install directory. * **[Performance Settings]** Added a settings to lower the number of smoke effects generated on frontlines. Lowered default settings for frontline smoke generators, so less smoke should be generated by default. +* **[Configuration]** Liberation preferences (DCS install and save game location) are now saved to `%LOCALAPPDATA%/DCSLiberation` to prevent needing to reconfigure each new install. ## Fixes diff --git a/qt_ui/liberation_install.py b/qt_ui/liberation_install.py index 164c9323..0bbcb0b0 100644 --- a/qt_ui/liberation_install.py +++ b/qt_ui/liberation_install.py @@ -1,5 +1,7 @@ import json +import logging import os +from pathlib import Path from shutil import copyfile import dcs @@ -10,7 +12,10 @@ global __dcs_saved_game_directory global __dcs_installation_directory global __last_save_file -PREFERENCES_FILE_PATH = "liberation_preferences.json" + +USER_PATH = Path(os.environ["LOCALAPPDATA"]) / "DCSLiberation" + +PREFERENCES_PATH = USER_PATH / "liberation_preferences.json" def init(): @@ -18,18 +23,16 @@ def init(): global __dcs_installation_directory global __last_save_file - if os.path.isfile(PREFERENCES_FILE_PATH): + if PREFERENCES_PATH.exists(): try: - with (open(PREFERENCES_FILE_PATH)) as prefs: - pref_data = json.loads(prefs.read()) - __dcs_saved_game_directory = pref_data["saved_game_dir"] - __dcs_installation_directory = pref_data["dcs_install_dir"] - if "last_save_file" in pref_data: - __last_save_file = pref_data["last_save_file"] - else: - __last_save_file = "" + logging.debug("Loading Liberation preferences from %s", PREFERENCES_PATH) + with PREFERENCES_PATH.open() as prefs: + pref_data = json.load(prefs) + __dcs_saved_game_directory = pref_data["saved_game_dir"] + __dcs_installation_directory = pref_data["dcs_install_dir"] + __last_save_file = pref_data.get("last_save_file", "") is_first_start = False - except: + except KeyError: __dcs_saved_game_directory = "" __dcs_installation_directory = "" __last_save_file = "" @@ -78,8 +81,9 @@ def save_config(): "dcs_install_dir": __dcs_installation_directory, "last_save_file": __last_save_file, } - with (open(PREFERENCES_FILE_PATH, "w")) as prefs: - prefs.write(json.dumps(pref_data)) + PREFERENCES_PATH.parent.mkdir(exist_ok=True, parents=True) + with PREFERENCES_PATH.open("w") as prefs: + json.dump(pref_data, prefs, indent=" ") def get_dcs_install_directory(): From 44154296611f2e34b645f3a068374218f049f8e1 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 13 May 2021 19:16:40 -0700 Subject: [PATCH 2/6] Stop requiring user input for mkrelease.py. --- resources/tools/mkrelease.py | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/resources/tools/mkrelease.py b/resources/tools/mkrelease.py index 5d70e86d..1704dda1 100644 --- a/resources/tools/mkrelease.py +++ b/resources/tools/mkrelease.py @@ -1,7 +1,9 @@ import os +from pathlib import Path import shutil -from zipfile import * +THIS_DIR = Path(__file__).resolve() +SRC_DIR = THIS_DIR.parents[1] IGNORED_PATHS = [ @@ -15,10 +17,8 @@ IGNORED_PATHS = [ "venv", ] -VERSION = input("version str:") - -def _zip_dir(archieve, path): +def _zip_dir(archive, path): for path, directories, files in os.walk(path): is_ignored = False for ignored_path in IGNORED_PATHS: @@ -32,29 +32,16 @@ def _zip_dir(archieve, path): for file in files: if file in IGNORED_PATHS: continue - archieve.write(os.path.join(path, file)) + archive.write(os.path.join(path, file)) -def _mk_archieve(): - path = os.path.join( - os.path.dirname(__file__), - os.pardir, - os.pardir, - "build", - "dcs_liberation_{}.zip".format(VERSION), - ) - if os.path.exists(path): - print("version already exists") - return - +def main(): try: shutil.rmtree("./dist") except FileNotFoundError: pass os.system("pyinstaller.exe --clean pyinstaller.spec") - # archieve = ZipFile(path, "w") - # archieve.writestr("dcs_liberation.bat", "cd dist\\dcs_liberation\r\nliberation_main \"%UserProfile%\\Saved Games\" \"{}\"".format(VERSION)) - # _zip_dir(archieve, "./dist/dcs_liberation") -_mk_archieve() +if __name__ == "__main__": + main() From 3c5f1f7c4b691d3a26a8a1d570adf49f654515da Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 13 May 2021 19:16:54 -0700 Subject: [PATCH 3/6] Log python version at startup. --- qt_ui/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qt_ui/main.py b/qt_ui/main.py index 8f5af44c..0d6e4570 100644 --- a/qt_ui/main.py +++ b/qt_ui/main.py @@ -223,6 +223,8 @@ def lint_weapon_data() -> None: def main(): logging_config.init_logging(VERSION) + logging.debug("Python version %s", sys.version) + game: Optional[Game] = None args = parse_args() From 5adcfbd7bdfc5a1c3d1570eb006445e11c51cb07 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 13 May 2021 20:24:22 -0700 Subject: [PATCH 4/6] Work around PySide2 bug in Property. https://bugreports.qt.io/browse/PYSIDE-1426 For whatever reason this only shows up in packaged builds for us, and also the recommended workaround of using a member property rather than a decorated method does not work for us. Until PySide2 5.15.3 (or later) is released, we need to use a named signal for every property we expose. --- qt_ui/widgets/map/mapmodel.py | 89 ++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 23 deletions(-) diff --git a/qt_ui/widgets/map/mapmodel.py b/qt_ui/widgets/map/mapmodel.py index 4d2800d0..1785d55d 100644 --- a/qt_ui/widgets/map/mapmodel.py +++ b/qt_ui/widgets/map/mapmodel.py @@ -27,8 +27,27 @@ from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu LeafletLatLon = List[float] +# **EVERY PROPERTY NEEDS A NOTIFY SIGNAL** +# +# https://bugreports.qt.io/browse/PYSIDE-1426 +# +# PySide2 5.15.2 released 6 days before the fix for this was merged, but presumably we +# can clean up after 5.15.3 (or a future version) is released. +# +# Until then, all properties must use a notify signal. For some reason the error doesn't +# show up when running from source, and member properties also are not sufficient. +# Failing to do this will cause every sync of the property to emit an expensive log +# message. This can prevent the UI from being responsive. +# +# A local signal (i.e. `@Property(t, notify=Signal())`) is not sufficient. The class +# needs a named signal for every property, even if it is constant. + class ControlPointJs(QObject): + nameChanged = Signal() + blueChanged = Signal() + positionChanged = Signal() + def __init__( self, control_point: ControlPoint, @@ -41,15 +60,15 @@ class ControlPointJs(QObject): self.theater = theater self.dialog: Optional[QBaseMenu2] = None - @Property(str) + @Property(str, notify=nameChanged) def name(self) -> str: return self.control_point.name - @Property(bool) + @Property(bool, notify=blueChanged) def blue(self) -> bool: return self.control_point.captured - @Property(list) + @Property(list, notify=positionChanged) def position(self) -> LeafletLatLon: ll = self.theater.point_to_ll(self.control_point.position) return [ll.latitude, ll.longitude] @@ -66,6 +85,13 @@ class ControlPointJs(QObject): class GroundObjectJs(QObject): + nameChanged = Signal() + unitsChanged = Signal() + blueChanged = Signal() + positionChanged = Signal() + samThreatRangesChanged = Signal() + samDetectionRangesChanged = Signal() + def __init__(self, tgo: TheaterGroundObject, game: Game) -> None: super().__init__() self.tgo = tgo @@ -96,7 +122,7 @@ class GroundObjectJs(QObject): def showPackageDialog(self) -> None: Dialog.open_new_package_dialog(self.tgo) - @Property(str) + @Property(str, notify=nameChanged) def name(self) -> str: return self.tgo.name @@ -110,7 +136,7 @@ class GroundObjectJs(QObject): ) return f"Unit #{unit.id} - {unit_display_name}{dead_label}" - @Property(list) + @Property(list, notify=unitsChanged) def units(self) -> List[str]: units = [] if self.buildings: @@ -124,16 +150,16 @@ class GroundObjectJs(QObject): units.append(self.make_unit_name(unit, dead=True)) return units - @Property(bool) + @Property(bool, notify=blueChanged) def blue(self) -> bool: return self.tgo.control_point.captured - @Property(list) + @Property(list, notify=positionChanged) def position(self) -> LeafletLatLon: ll = self.theater.point_to_ll(self.tgo.position) return [ll.latitude, ll.longitude] - @Property(list) + @Property(list, notify=samThreatRangesChanged) def samThreatRanges(self) -> List[float]: if not self.tgo.might_have_aa: return [] @@ -145,7 +171,7 @@ class GroundObjectJs(QObject): ranges.append(threat_range.meters) return ranges - @Property(list) + @Property(list, notify=samDetectionRangesChanged) def samDetectionRanges(self) -> List[float]: if not self.tgo.might_have_aa: return [] @@ -159,6 +185,11 @@ class GroundObjectJs(QObject): class SupplyRouteJs(QObject): + pointsChanged = Signal() + frontActiveChanged = Signal() + isSeaChanged = Signal() + blueChanged = Signal() + def __init__( self, a: ControlPoint, @@ -172,32 +203,34 @@ class SupplyRouteJs(QObject): self._points = points self.sea_route = sea_route - @Property(list) + @Property(list, notify=pointsChanged) def points(self) -> List[LeafletLatLon]: return self._points - @Property(bool) + @Property(bool, notify=frontActiveChanged) def frontActive(self) -> bool: if self.sea_route: return False return self.control_point_a.front_is_active(self.control_point_b) - @Property(bool) + @Property(bool, notify=isSeaChanged) def isSea(self) -> bool: return self.sea_route - @Property(bool) + @Property(bool, notify=blueChanged) def blue(self) -> bool: return self.control_point_a.captured class FrontLineJs(QObject): + extentsChanged = Signal() + def __init__(self, front_line: FrontLine, theater: ConflictTheater) -> None: super().__init__() self.front_line = front_line self.theater = theater - @Property(list) + @Property(list, notify=extentsChanged) def extents(self) -> List[LeafletLatLon]: a = self.theater.point_to_ll( self.front_line.position.point_from_heading( @@ -217,6 +250,14 @@ class FrontLineJs(QObject): class WaypointJs(QObject): + numberChanged = Signal() + positionChanged = Signal() + altitudeFtChanged = Signal() + altitudeReferenceChanged = Signal() + nameChanged = Signal() + timingChanged = Signal() + isDivertChanged = Signal() + def __init__( self, waypoint: FlightWaypoint, @@ -230,28 +271,28 @@ class WaypointJs(QObject): self.flight_plan = flight_plan self.theater = theater - @Property(int) + @Property(int, notify=numberChanged) def number(self) -> int: return self._number - @Property(list) + @Property(list, notify=positionChanged) def position(self) -> LeafletLatLon: ll = self.theater.point_to_ll(self.waypoint.position) return [ll.latitude, ll.longitude] - @Property(int) + @Property(int, notify=altitudeFtChanged) def altitudeFt(self) -> int: return int(self.waypoint.alt.feet) - @Property(str) + @Property(str, notify=altitudeReferenceChanged) def altitudeReference(self) -> str: return "AGL" if self.waypoint.alt_type == "RADIO" else "MSL" - @Property(str) + @Property(str, notify=nameChanged) def name(self) -> str: return self.waypoint.name - @Property(str) + @Property(str, notify=timingChanged) def timing(self) -> str: prefix = "TOT" time = self.flight_plan.tot_for_waypoint(self.waypoint) @@ -262,13 +303,15 @@ class WaypointJs(QObject): return "" return f"{prefix} T+{timedelta(seconds=int(time.total_seconds()))}" - @Property(bool) + @Property(bool, notify=isDivertChanged) def isDivert(self) -> bool: return self.waypoint.waypoint_type is FlightWaypointType.DIVERT class FlightJs(QObject): flightPlanChanged = Signal() + blueChanged = Signal() + selectedChanged = Signal() def __init__( self, flight: Flight, selected: bool, theater: ConflictTheater @@ -298,11 +341,11 @@ class FlightJs(QObject): def flightPlan(self) -> List[WaypointJs]: return self._waypoints - @Property(bool) + @Property(bool, notify=blueChanged) def blue(self) -> bool: return self.flight.departure.captured - @Property(bool) + @Property(bool, notify=selectedChanged) def selected(self) -> bool: return self._selected From 99dc91dcb4678e137805106228f6210132ebc88a Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 13 May 2021 21:00:13 -0700 Subject: [PATCH 5/6] Fix game break when capturing factory. We need to recompute the transit networks after a capture *before* processing transfers. Otherwise units deployed that turn will not be able to find their destination. Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1070 --- game/game.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/game/game.py b/game/game.py index c4ac7250..4c250682 100644 --- a/game/game.py +++ b/game/game.py @@ -282,6 +282,11 @@ class Game: ) self.turn += 1 + # Need to recompute before transfers and deliveries to account for captures. + # This happens in in initialize_turn as well, because cheating doesn't advance a + # turn but can capture bases so we need to recompute there as well. + self.compute_transit_networks() + # Must happen *before* unit deliveries are handled, or else new units will spawn # one hop ahead. ControlPoint.process_turn handles unit deliveries. self.transfers.perform_transfers() From eec56256e80a620f19e2bcd85dd71213b0131273 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 13 May 2021 21:44:07 -0700 Subject: [PATCH 6/6] Add AEW&C aircraft to the faction aircraft list. To avoid confusion, use only the aircraft list for the purchasable aircraft. This fix also caught a faction's Tu-142 that was not actually purchasable. Invalid aircraft in the faction aircraft list will now raise an error. Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1074 --- game/db.py | 2 ++ game/factions/faction.py | 2 ++ .../airfield/QAircraftRecruitmentMenu.py | 28 ++++++++----------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/game/db.py b/game/db.py index 0a46473e..91b1c138 100644 --- a/game/db.py +++ b/game/db.py @@ -117,6 +117,7 @@ from dcs.planes import ( Yak_40, plane_map, I_16, + Tu_142, ) from dcs.ships import ( Boat_Armed_Hi_speed, @@ -456,6 +457,7 @@ PRICES = { Tu_160: 50, Tu_22M3: 40, Tu_95MS: 35, + Tu_142: 35, # special IL_76MD: 30, An_26B: 25, diff --git a/game/factions/faction.py b/game/factions/faction.py index b1cecc55..c72fa343 100644 --- a/game/factions/faction.py +++ b/game/factions/faction.py @@ -154,6 +154,8 @@ class Faction: faction.awacs = load_all_aircraft(json.get("awacs", [])) faction.tankers = load_all_aircraft(json.get("tankers", [])) + faction.aircrafts = list(set(faction.aircrafts + faction.awacs)) + faction.frontline_units = load_all_vehicles(json.get("frontline_units", [])) faction.artillery_units = load_all_vehicles(json.get("artillery_units", [])) faction.infantry_units = load_all_vehicles(json.get("infantry_units", [])) diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py index 46128ca8..3dd2bb0e 100644 --- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py @@ -52,23 +52,19 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): row = 0 unit_types: Set[Type[FlyingType]] = set() - for task in tasks: - units = db.find_unittype(task, self.game_model.game.player_name) - if not units: + for unit_type in self.game_model.game.player_faction.aircrafts: + if not issubclass(unit_type, FlyingType): + raise RuntimeError(f"Non-flying aircraft found in faction: {unit_type}") + if self.cp.is_carrier and unit_type not in db.CARRIER_CAPABLE: continue - for unit in units: - if not issubclass(unit, FlyingType): - continue - if self.cp.is_carrier and unit not in db.CARRIER_CAPABLE: - continue - if self.cp.is_lha and unit not in db.LHA_CAPABLE: - continue - if ( - self.cp.cptype in [ControlPointType.FOB, ControlPointType.FARP] - and unit not in helicopter_map.values() - ): - continue - unit_types.add(unit) + if self.cp.is_lha and unit_type not in db.LHA_CAPABLE: + continue + if ( + self.cp.cptype in [ControlPointType.FOB, ControlPointType.FARP] + and unit_type not in helicopter_map.values() + ): + continue + unit_types.add(unit_type) sorted_units = sorted( unit_types,