From 1760532168ac87fb28f992ea360ebd3663751014 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Wed, 9 Aug 2023 20:50:01 -0700 Subject: [PATCH] Rename UnitType.name what it is: the variant ID. This property affects safe compat because the ID is what gets preserved in the save, but it's unfortunately also used as the display name, which means changing the display name breaks save compat. It also prevents us from changing display names without breaking faction definitions. This is the first step in fixing that. The next is adding a separate display_name property that can be updated without breaking either of those. --- game/armedforces/forcegroup.py | 2 +- game/dcs/aircrafttype.py | 16 +++++++++++----- game/dcs/groundunittype.py | 16 ++++++++++++++-- game/dcs/shipunittype.py | 18 +++++++++++++++--- game/dcs/unittype.py | 4 ++-- game/factions/faction.py | 2 +- .../aircraft/flightgroupconfigurator.py | 2 +- game/naming.py | 8 ++++---- game/purchaseadapter.py | 2 +- qt_ui/windows/AirWingConfigurationDialog.py | 10 +++++----- qt_ui/windows/AirWingDialog.py | 6 +++--- qt_ui/windows/QDebriefingWindow.py | 4 +++- qt_ui/windows/QUnitInfoWindow.py | 4 ++-- qt_ui/windows/basemenu/DepartingConvoysMenu.py | 4 ++-- .../windows/basemenu/NewUnitTransferDialog.py | 6 +++--- .../airfield/QAircraftRecruitmentMenu.py | 4 +++- .../ground_forces/QArmorRecruitmentMenu.py | 2 +- qt_ui/windows/basemenu/intel/QIntelInfo.py | 4 ++-- .../groundobject/QGroundObjectBuyMenu.py | 2 +- qt_ui/windows/intel.py | 8 ++++---- 20 files changed, 79 insertions(+), 45 deletions(-) diff --git a/game/armedforces/forcegroup.py b/game/armedforces/forcegroup.py index b382b75a..0bafacfe 100644 --- a/game/armedforces/forcegroup.py +++ b/game/armedforces/forcegroup.py @@ -289,7 +289,7 @@ class ForceGroup: unit.id = game.next_unit_id() # Add unit name escaped so that we do not have scripting issues later unit.name = escape_string_for_lua( - unit.unit_type.name if unit.unit_type else unit.type.name + unit.unit_type.variant_id if unit.unit_type else unit.type.name ) unit.position = PointWithHeading.from_point( ground_object.position + unit.position, diff --git a/game/dcs/aircrafttype.py b/game/dcs/aircrafttype.py index 87b136ec..622d5ef6 100644 --- a/game/dcs/aircrafttype.py +++ b/game/dcs/aircrafttype.py @@ -39,6 +39,7 @@ from game.radio.channels import ( ViperChannelNamer, WarthogChannelNamer, ) +from game.savecompat import has_save_compat_for from game.utils import ( Distance, ImperialUnits, @@ -219,7 +220,7 @@ class AircraftType(UnitType[Type[FlyingType]]): @classmethod def register(cls, unit_type: AircraftType) -> None: - cls._by_name[unit_type.name] = unit_type + cls._by_name[unit_type.variant_id] = unit_type cls._by_unit_type[unit_type.dcs_unit_type].append(unit_type) @property @@ -297,7 +298,7 @@ class AircraftType(UnitType[Type[FlyingType]]): else: # Slow like warbirds or helicopters # Use whichever is slowest - mach 0.35 or 50% of max speed - logging.debug(f"{self.name} max_speed * 0.5 is {max_speed * 0.5}") + logging.debug(f"{self.variant_id} max_speed * 0.5 is {max_speed * 0.5}") return min(Speed.from_mach(0.35, altitude), max_speed * 0.5) def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency: @@ -352,9 +353,14 @@ class AircraftType(UnitType[Type[FlyingType]]): def task_priority(self, task: FlightType) -> int: return self.task_priorities[task] + @has_save_compat_for(9) def __setstate__(self, state: dict[str, Any]) -> None: + # Save compat: the `name` field has been renamed `variant_id`. + if "name" in state: + state["variant_id"] = state.pop("name") + # Update any existing models with new data on load. - updated = AircraftType.named(state["name"]) + updated = AircraftType.named(state["variant_id"]) state.update(updated.__dict__) self.__dict__.update(state) @@ -485,7 +491,7 @@ class AircraftType(UnitType[Type[FlyingType]]): for variant in data.get("variants", [aircraft.id]): yield AircraftType( dcs_unit_type=aircraft, - name=variant, + variant_id=variant, description=data.get( "description", f"No data. Google {variant}", @@ -552,4 +558,4 @@ class AircraftType(UnitType[Type[FlyingType]]): AircraftType._custom_weapon_injections(aircraft, data) def __hash__(self) -> int: - return hash(self.name) + return hash(self.variant_id) diff --git a/game/dcs/groundunittype.py b/game/dcs/groundunittype.py index 1030ba14..ba32dff3 100644 --- a/game/dcs/groundunittype.py +++ b/game/dcs/groundunittype.py @@ -12,6 +12,7 @@ from dcs.vehicles import vehicle_map from game.data.units import UnitClass from game.dcs.unittype import UnitType +from game.savecompat import has_save_compat_for @dataclass @@ -65,9 +66,20 @@ class GroundUnitType(UnitType[Type[VehicleType]]): dict[type[VehicleType], list[GroundUnitType]] ] = defaultdict(list) + @has_save_compat_for(9) + def __setstate__(self, state: dict[str, Any]) -> None: + # Save compat: the `name` field has been renamed `variant_id`. + if "name" in state: + state["variant_id"] = state.pop("name") + + # Update any existing models with new data on load. + updated = GroundUnitType.named(state["variant_id"]) + state.update(updated.__dict__) + self.__dict__.update(state) + @classmethod def register(cls, unit_type: GroundUnitType) -> None: - cls._by_name[unit_type.name] = unit_type + cls._by_name[unit_type.variant_id] = unit_type cls._by_unit_type[unit_type.dcs_unit_type].append(unit_type) @classmethod @@ -115,7 +127,7 @@ class GroundUnitType(UnitType[Type[VehicleType]]): dcs_unit_type=vehicle, unit_class=unit_class, spawn_weight=data.get("spawn_weight", 0), - name=variant, + variant_id=variant, description=data.get( "description", f"No data. Google {variant}", diff --git a/game/dcs/shipunittype.py b/game/dcs/shipunittype.py index cf36d1cd..4c739e9c 100644 --- a/game/dcs/shipunittype.py +++ b/game/dcs/shipunittype.py @@ -4,7 +4,7 @@ import logging from collections import defaultdict from dataclasses import dataclass from pathlib import Path -from typing import ClassVar, Iterator, Type +from typing import ClassVar, Iterator, Type, Any import yaml from dcs.ships import ship_map @@ -12,6 +12,7 @@ from dcs.unittype import ShipType from game.data.units import UnitClass from game.dcs.unittype import UnitType +from game.savecompat import has_save_compat_for @dataclass(frozen=True) @@ -21,9 +22,20 @@ class ShipUnitType(UnitType[Type[ShipType]]): list ) + @has_save_compat_for(9) + def __setstate__(self, state: dict[str, Any]) -> None: + # Save compat: the `name` field has been renamed `variant_id`. + if "name" in state: + state["variant_id"] = state.pop("name") + + # Update any existing models with new data on load. + updated = ShipUnitType.named(state["variant_id"]) + state.update(updated.__dict__) + self.__dict__.update(state) + @classmethod def register(cls, unit_type: ShipUnitType) -> None: - cls._by_name[unit_type.name] = unit_type + cls._by_name[unit_type.variant_id] = unit_type cls._by_unit_type[unit_type.dcs_unit_type].append(unit_type) @classmethod @@ -66,7 +78,7 @@ class ShipUnitType(UnitType[Type[ShipType]]): yield ShipUnitType( dcs_unit_type=ship, unit_class=unit_class, - name=variant, + variant_id=variant, description=data.get( "description", f"No data. Google {variant}", diff --git a/game/dcs/unittype.py b/game/dcs/unittype.py index aa977987..e8da39ba 100644 --- a/game/dcs/unittype.py +++ b/game/dcs/unittype.py @@ -16,7 +16,7 @@ DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=Type[DcsUnitType]) @dataclass(frozen=True) class UnitType(ABC, Generic[DcsUnitTypeT]): dcs_unit_type: DcsUnitTypeT - name: str + variant_id: str description: str year_introduced: str country_of_origin: str @@ -28,7 +28,7 @@ class UnitType(ABC, Generic[DcsUnitTypeT]): _loaded: ClassVar[bool] = False def __str__(self) -> str: - return self.name + return self.variant_id @property def dcs_id(self) -> str: diff --git a/game/factions/faction.py b/game/factions/faction.py index c8b6bc09..53593c6e 100644 --- a/game/factions/faction.py +++ b/game/factions/faction.py @@ -169,7 +169,7 @@ class Faction: def air_defenses(self) -> list[str]: """Returns the Air Defense types""" # This is used for the faction overview in NewGameWizard - air_defenses = [a.name for a in self.air_defense_units] + air_defenses = [a.variant_id for a in self.air_defense_units] air_defenses.extend( [ pg.name diff --git a/game/missiongenerator/aircraft/flightgroupconfigurator.py b/game/missiongenerator/aircraft/flightgroupconfigurator.py index d47e65bf..e39cc701 100644 --- a/game/missiongenerator/aircraft/flightgroupconfigurator.py +++ b/game/missiongenerator/aircraft/flightgroupconfigurator.py @@ -209,7 +209,7 @@ class FlightGroupConfigurator: TankerInfo( group_name=str(self.group.name), callsign=callsign, - variant=self.flight.unit_type.name, + variant=self.flight.unit_type.variant_id, freq=channel, tacan=tacan, start_time=self.flight.flight_plan.patrol_start_time, diff --git a/game/naming.py b/game/naming.py index ad9d4693..4a663ab4 100644 --- a/game/naming.py +++ b/game/naming.py @@ -484,13 +484,13 @@ class NameGenerator: else: name_str = "{} {}".format(flight.package.target.name, flight.flight_type) return "{}|{}|{}|{}|".format( - name_str, country.id, cls.aircraft_number, flight.unit_type.name + name_str, country.id, cls.aircraft_number, flight.unit_type.variant_id ) @classmethod def next_unit_name(cls, country: Country, unit_type: UnitType[Any]) -> str: cls.number += 1 - return "unit|{}|{}|{}|".format(country.id, cls.number, unit_type.name) + return "unit|{}|{}|{}|".format(country.id, cls.number, unit_type.variant_id) @classmethod def next_infantry_name(cls, country: Country, unit_type: UnitType[Any]) -> str: @@ -498,7 +498,7 @@ class NameGenerator: return "infantry|{}|{}|{}|".format( country.id, cls.infantry_number, - unit_type.name, + unit_type.variant_id, ) @classmethod @@ -509,7 +509,7 @@ class NameGenerator: @classmethod def next_tanker_name(cls, country: Country, unit_type: AircraftType) -> str: cls.number += 1 - return "tanker|{}|{}|0|{}".format(country.id, cls.number, unit_type.name) + return "tanker|{}|{}|0|{}".format(country.id, cls.number, unit_type.variant_id) @classmethod def next_carrier_name(cls, country: Country) -> str: diff --git a/game/purchaseadapter.py b/game/purchaseadapter.py index aeeff043..5f2a801e 100644 --- a/game/purchaseadapter.py +++ b/game/purchaseadapter.py @@ -142,7 +142,7 @@ class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]): separator = "
" else: separator = " " - return separator.join([item.aircraft.name, str(item)]) + return separator.join([item.aircraft.variant_id, str(item)]) def unit_type_of(self, item: Squadron) -> AircraftType: return item.aircraft diff --git a/qt_ui/windows/AirWingConfigurationDialog.py b/qt_ui/windows/AirWingConfigurationDialog.py index 6411c259..68367f39 100644 --- a/qt_ui/windows/AirWingConfigurationDialog.py +++ b/qt_ui/windows/AirWingConfigurationDialog.py @@ -611,12 +611,12 @@ class AircraftTypeList(QListView): self.add_aircraft_type(aircraft) def remove_aircraft_type(self, aircraft: AircraftType): - for item in self.item_model.findItems(aircraft.name): + for item in self.item_model.findItems(aircraft.variant_id): self.item_model.removeRow(item.row()) self.page_index_changed.emit(self.selectionModel().currentIndex().row()) def add_aircraft_type(self, aircraft: AircraftType): - aircraft_item = QStandardItem(aircraft.name) + aircraft_item = QStandardItem(aircraft.variant_id) icon = self.icon_for(aircraft) if icon is not None: aircraft_item.setIcon(icon) @@ -728,7 +728,7 @@ class AirWingConfigurationTab(QWidget): ) # Add Squadron - if not self.type_list.item_model.findItems(selected_type.name): + if not self.type_list.item_model.findItems(selected_type.variant_id): self.type_list.add_aircraft_type(selected_type) # TODO Select the newly added type self.squadrons_panel.add_squadron_to_panel(squadron) @@ -814,8 +814,8 @@ class SquadronAircraftTypeSelector(QComboBox): super().__init__() self.setSizeAdjustPolicy(self.AdjustToContents) - for type in sorted(types, key=lambda type: type.name): - self.addItem(type.name, type) + for type in sorted(types, key=lambda type: type.variant_id): + self.addItem(type.variant_id, type) if selected_aircraft: self.setCurrentText(selected_aircraft) diff --git a/qt_ui/windows/AirWingDialog.py b/qt_ui/windows/AirWingDialog.py index 41ab20df..42477acc 100644 --- a/qt_ui/windows/AirWingDialog.py +++ b/qt_ui/windows/AirWingDialog.py @@ -43,7 +43,7 @@ class SquadronDelegate(TwoColumnRowDelegate): nickname = "" return f"{squadron.name}{nickname}" elif (row, column) == (0, 1): - return squadron.aircraft.name + return squadron.aircraft.variant_id elif (row, column) == (1, 0): return squadron.location.name elif (row, column) == (1, 1): @@ -130,7 +130,7 @@ class AircraftInventoryData: player = "Player" if pilot.player else "AI" yield AircraftInventoryData( flight.departure.name, - flight.unit_type.name, + flight.unit_type.variant_id, flight_type, target, pilot_name, @@ -143,7 +143,7 @@ class AircraftInventoryData: ) -> Iterator[AircraftInventoryData]: for _ in range(0, squadron.untasked_aircraft): yield AircraftInventoryData( - squadron.name, squadron.aircraft.name, "Idle", "N/A", "N/A", "N/A" + squadron.name, squadron.aircraft.variant_id, "Idle", "N/A", "N/A", "N/A" ) diff --git a/qt_ui/windows/QDebriefingWindow.py b/qt_ui/windows/QDebriefingWindow.py index 6729ebb6..e4aefc64 100644 --- a/qt_ui/windows/QDebriefingWindow.py +++ b/qt_ui/windows/QDebriefingWindow.py @@ -23,7 +23,9 @@ class LossGrid(QGridLayout): def __init__(self, debriefing: Debriefing, player: bool) -> None: super().__init__() - self.add_loss_rows(debriefing.air_losses.by_type(player), lambda u: u.name) + self.add_loss_rows( + debriefing.air_losses.by_type(player), lambda u: u.variant_id + ) self.add_loss_rows( debriefing.front_line_losses_by_type(player), lambda u: str(u) ) diff --git a/qt_ui/windows/QUnitInfoWindow.py b/qt_ui/windows/QUnitInfoWindow.py index fbc7e405..904cb146 100644 --- a/qt_ui/windows/QUnitInfoWindow.py +++ b/qt_ui/windows/QUnitInfoWindow.py @@ -66,7 +66,7 @@ class QUnitInfoWindow(QDialog): self.setModal(True) self.game = game self.unit_type = unit_type - self.name = unit_type.name + self.name = unit_type.variant_id self.setWindowTitle(f"Unit Info: {self.name}") self.setWindowIcon(QIcon("./resources/icon.png")) self.setMinimumHeight(570) @@ -93,7 +93,7 @@ class QUnitInfoWindow(QDialog): self.details_grid_layout.setMargin(0) self.name_box = QLabel( - f"Name: {unit_type.manufacturer} {unit_type.name}" + f"Name: {unit_type.manufacturer} {unit_type.variant_id}" ) self.name_box.setProperty("style", "info-element") diff --git a/qt_ui/windows/basemenu/DepartingConvoysMenu.py b/qt_ui/windows/basemenu/DepartingConvoysMenu.py index c334f0bb..c1da87e3 100644 --- a/qt_ui/windows/basemenu/DepartingConvoysMenu.py +++ b/qt_ui/windows/basemenu/DepartingConvoysMenu.py @@ -33,11 +33,11 @@ class DepartingConvoyInfo(QGroupBox): if unit_type.dcs_id in VEHICLES_ICONS.keys(): icon.setPixmap(VEHICLES_ICONS[unit_type.dcs_id]) else: - icon.setText("" + unit_type.name + "") + icon.setText("" + unit_type.variant_id + "") icon.setProperty("style", "icon-armor") unit_layout.addWidget(icon, idx, 0) unit_layout.addWidget( - QLabel(f"{count} x {unit_type.name}"), + QLabel(f"{count} x {unit_type.variant_id}"), idx, 1, ) diff --git a/qt_ui/windows/basemenu/NewUnitTransferDialog.py b/qt_ui/windows/basemenu/NewUnitTransferDialog.py index 77274fa1..b7a2c299 100644 --- a/qt_ui/windows/basemenu/NewUnitTransferDialog.py +++ b/qt_ui/windows/basemenu/NewUnitTransferDialog.py @@ -64,7 +64,7 @@ class UnitTransferList(QFrame): task_box_layout = QGridLayout() scroll_content.setLayout(task_box_layout) - units_column = sorted(cp.base.armor, key=lambda u: u.name) + units_column = sorted(cp.base.armor, key=lambda u: u.variant_id) count = 0 for count, unit_type in enumerate(units_column): @@ -173,7 +173,7 @@ class ScrollingUnitTransferGrid(QFrame): unit_types = set(self.game_model.game.faction_for(player=True).ground_units) sorted_units = sorted( {u for u in unit_types if self.cp.base.total_units_of_type(u)}, - key=lambda u: u.name, + key=lambda u: u.variant_id, ) for row, unit_type in enumerate(sorted_units): self.add_unit_row(unit_type, task_box_layout, row) @@ -205,7 +205,7 @@ class ScrollingUnitTransferGrid(QFrame): origin_inventory = self.cp.base.total_units_of_type(unit_type) - unit_name = QLabel(f"{unit_type.name}") + unit_name = QLabel(f"{unit_type.variant_id}") unit_name.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) ) diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py index 14b2b633..95d487d4 100644 --- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py @@ -44,7 +44,9 @@ class QAircraftRecruitmentMenu(UnitTransactionFrame[Squadron]): for squadron in cp.squadrons: unit_types.add(squadron.aircraft) - sorted_squadrons = sorted(cp.squadrons, key=lambda s: (s.aircraft.name, s.name)) + sorted_squadrons = sorted( + cp.squadrons, key=lambda s: (s.aircraft.variant_id, s.name) + ) for row, squadron in enumerate(sorted_squadrons): self.add_purchase_row(squadron, task_box_layout, row) diff --git a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py index 6149ce8e..e420db3b 100644 --- a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py @@ -32,7 +32,7 @@ class QArmorRecruitmentMenu(UnitTransactionFrame[GroundUnitType]): unit_types = list( set(self.game_model.game.faction_for(player=True).ground_units) ) - unit_types.sort(key=lambda u: u.name) + unit_types.sort(key=lambda u: u.variant_id) for row, unit_type in enumerate(unit_types): self.add_purchase_row(unit_type, task_box_layout, row) stretch = QVBoxLayout() diff --git a/qt_ui/windows/basemenu/intel/QIntelInfo.py b/qt_ui/windows/basemenu/intel/QIntelInfo.py index 795fd73d..fe9f9c85 100644 --- a/qt_ui/windows/basemenu/intel/QIntelInfo.py +++ b/qt_ui/windows/basemenu/intel/QIntelInfo.py @@ -32,7 +32,7 @@ class QIntelInfo(QFrame): ).present.items(): if count: task_type = unit_type.dcs_unit_type.task_default.name - units_by_task[task_type][unit_type.name] += count + units_by_task[task_type][unit_type.variant_id] += count units_by_task = { task: units_by_task[task] for task in sorted(units_by_task.keys()) @@ -41,7 +41,7 @@ class QIntelInfo(QFrame): front_line_units = defaultdict(int) for unit_type, count in self.cp.base.armor.items(): if count: - front_line_units[unit_type.name] += count + front_line_units[unit_type.variant_id] += count units_by_task["Front line units"] = front_line_units for task, unit_types in units_by_task.items(): diff --git a/qt_ui/windows/groundobject/QGroundObjectBuyMenu.py b/qt_ui/windows/groundobject/QGroundObjectBuyMenu.py index b0dc5505..c790287b 100644 --- a/qt_ui/windows/groundobject/QGroundObjectBuyMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectBuyMenu.py @@ -76,7 +76,7 @@ class QTgoLayoutGroupRow(QWidget): # Add all possible units with the price for unit_type in force_group.unit_types_for_group(group): self.unit_selector.addItem( - f"{unit_type.name} [${unit_type.price}M]", + f"{unit_type.variant_id} [${unit_type.price}M]", userData=(unit_type.dcs_unit_type, unit_type.price), ) # Add all possible statics with price = 0 diff --git a/qt_ui/windows/intel.py b/qt_ui/windows/intel.py index cb8e3b27..d8742273 100644 --- a/qt_ui/windows/intel.py +++ b/qt_ui/windows/intel.py @@ -88,11 +88,11 @@ class AircraftIntelLayout(IntelTableLayout): continue self.add_header(f"{control_point.name} ({base_total})") - for airframe in sorted(allocation.present, key=lambda k: k.name): + for airframe in sorted(allocation.present, key=lambda k: k.variant_id): count = allocation.present[airframe] if not count: continue - self.add_row(f" {airframe.name}", count) + self.add_row(f" {airframe.variant_id}", count) self.add_row("") self.add_row("Total", total) @@ -117,11 +117,11 @@ class ArmyIntelLayout(IntelTableLayout): continue self.add_header(f"{control_point.name} ({base.total_armor})") - for vehicle in sorted(base.armor, key=lambda k: k.name): + for vehicle in sorted(base.armor, key=lambda k: k.variant_id): count = base.armor[vehicle] if not count: continue - self.add_row(f" {vehicle.name}", count) + self.add_row(f" {vehicle.variant_id}", count) self.add_row("") self.add_row("Total", total)