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.
This commit is contained in:
Dan Albert 2023-08-09 20:50:01 -07:00
parent 09f1af37fd
commit 0be6952a93
22 changed files with 88 additions and 53 deletions

View File

@ -10,19 +10,18 @@ import yaml
from dcs.unittype import ShipType, StaticType, UnitType as DcsUnitType, VehicleType
from game.data.groups import GroupTask
from game.data.radar_db import UNITS_WITH_RADAR
from game.dcs.groundunittype import GroundUnitType
from game.dcs.helpers import static_type_from_name
from game.dcs.shipunittype import ShipUnitType
from game.dcs.unittype import UnitType
from game.layout import LAYOUTS
from game.layout.layout import TgoLayout, TgoLayoutUnitGroup
from game.point_with_heading import PointWithHeading
from game.theater.theatergroundobject import (
IadsGroundObject,
IadsBuildingGroundObject,
NavalGroundObject,
)
from game.layout import LAYOUTS
from game.layout.layout import TgoLayout, TgoLayoutUnitGroup
from game.point_with_heading import PointWithHeading
from game.theater.theatergroup import IadsGroundGroup, IadsRole, TheaterGroup
from game.utils import escape_string_for_lua
@ -288,7 +287,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,

View File

@ -138,7 +138,9 @@ class Loadout:
}
except KeyError:
logging.exception(
"Ignoring %s loadout with invalid weapons: %s", aircraft.name, name
"Ignoring %s loadout with invalid weapons: %s",
aircraft.variant_id,
name,
)
continue

View File

@ -36,6 +36,7 @@ from game.radio.channels import (
ViperChannelNamer,
WarthogChannelNamer,
)
from game.savecompat import has_save_compat_for
from game.utils import (
Distance,
ImperialUnits,
@ -217,7 +218,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
@ -291,7 +292,7 @@ class AircraftType(UnitType[Type[FlyingType]]):
else:
# Slow like warbirds or helicopters
# Use whichever is slowest - mach 0.35 or 70% of max speed
logging.debug(f"{self.name} max_speed * 0.7 is {max_speed * 0.7}")
logging.debug(f"{self.variant_id} max_speed * 0.7 is {max_speed * 0.7}")
return min(Speed.from_mach(0.35, altitude), max_speed * 0.7)
def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
@ -346,9 +347,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)
@ -471,7 +477,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. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",
@ -508,4 +514,4 @@ class AircraftType(UnitType[Type[FlyingType]]):
)
def __hash__(self) -> int:
return hash(self.name)
return hash(self.variant_id)

View File

@ -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. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",

View File

@ -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. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",

View File

@ -15,7 +15,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
@ -27,7 +27,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:

View File

@ -159,7 +159,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

View File

@ -164,7 +164,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.mission_begin_on_station_time,

View File

@ -152,7 +152,7 @@ class AirSupportGenerator:
TankerInfo(
group_name=str(tanker_group.name),
callsign=callsign,
variant=tanker_unit_type.name,
variant=tanker_unit_type.variant_id,
freq=freq,
tacan=tacan,
start_time=None,

View File

@ -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:

View File

@ -140,7 +140,7 @@ class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]):
separator = "<br />"
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

View File

@ -594,12 +594,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)
@ -767,7 +767,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)
@ -893,8 +893,8 @@ class SquadronAircraftTypeSelector(QComboBox):
super().__init__()
self.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.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)

View File

@ -44,7 +44,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):
@ -134,7 +134,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,
@ -147,7 +147,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"
)

View File

@ -22,7 +22,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)
)

View File

@ -51,7 +51,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)
@ -78,7 +78,7 @@ class QUnitInfoWindow(QDialog):
self.details_grid_layout.setContentsMargins(0, 0, 0, 0)
self.name_box = QLabel(
f"<b>Name:</b> {unit_type.manufacturer} {unit_type.name}"
f"<b>Name:</b> {unit_type.manufacturer} {unit_type.variant_id}"
)
self.name_box.setProperty("style", "info-element")

View File

@ -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("<b>" + unit_type.name + "</b>")
icon.setText("<b>" + unit_type.variant_id + "</b>")
icon.setProperty("style", "icon-armor")
unit_layout.addWidget(icon, idx, 0)
unit_layout.addWidget(
QLabel(f"{count} x <strong>{unit_type.name}</strong>"),
QLabel(f"{count} x <strong>{unit_type.variant_id}</strong>"),
idx,
1,
)

View File

@ -62,7 +62,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):
@ -171,7 +171,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)
@ -203,7 +203,7 @@ class ScrollingUnitTransferGrid(QFrame):
origin_inventory = self.cp.base.total_units_of_type(unit_type)
unit_name = QLabel(f"<b>{unit_type.name}</b>")
unit_name = QLabel(f"<b>{unit_type.variant_id}</b>")
unit_name.setSizePolicy(
QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
)

View File

@ -11,12 +11,12 @@ from PySide6.QtWidgets import (
)
from game.dcs.aircrafttype import AircraftType
from game.purchaseadapter import AircraftPurchaseAdapter
from game.squadrons import Squadron
from game.theater import ControlPoint
from qt_ui.models import GameModel
from qt_ui.uiconstants import ICONS
from qt_ui.windows.basemenu.UnitTransactionFrame import UnitTransactionFrame
from game.purchaseadapter import AircraftPurchaseAdapter
class QAircraftRecruitmentMenu(UnitTransactionFrame[Squadron]):
@ -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)

View File

@ -2,10 +2,10 @@ from PySide6.QtCore import Qt
from PySide6.QtWidgets import QGridLayout, QScrollArea, QVBoxLayout, QWidget
from game.dcs.groundunittype import GroundUnitType
from game.purchaseadapter import GroundUnitPurchaseAdapter
from game.theater import ControlPoint
from qt_ui.models import GameModel
from qt_ui.windows.basemenu.UnitTransactionFrame import UnitTransactionFrame
from game.purchaseadapter import GroundUnitPurchaseAdapter
class QArmorRecruitmentMenu(UnitTransactionFrame[GroundUnitType]):
@ -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()

View File

@ -27,7 +27,7 @@ class QIntelInfo(QFrame):
for unit_type, count in self.cp.allocated_aircraft().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())
@ -36,7 +36,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():

View File

@ -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

View File

@ -84,11 +84,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("<b>Total</b>", total)
@ -113,11 +113,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("<b>Total</b>", total)