dcs-retribution/qt_ui/windows/groundobject/QGroundObjectMenu.py
Dan Albert 4d0fb67c53 Fix crash when buying or selling TGO units.
Updating the game destroys this window so we cannot continue with the
calls. It worked in my initial testing, so presumably it's partly
dependent on when the finalizers run.

Since the windows will be destroyed there's nothing for us to actually
update, so just remove that signal and the explicit close calls.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1344
2021-06-25 16:30:18 -07:00

475 lines
17 KiB
Python

import logging
from typing import List, Optional
from PySide2 import QtCore
from PySide2.QtGui import Qt
from PySide2.QtWidgets import (
QComboBox,
QDialog,
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QMessageBox,
QPushButton,
QSpinBox,
QVBoxLayout,
)
from dcs import Point
from dcs import vehicles
from game import Game
from game.data.building_data import FORTIFICATION_BUILDINGS
from game.db import REWARDS
from game.dcs.groundunittype import GroundUnitType
from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import (
VehicleGroupGroundObject,
SamGroundObject,
EwrGroundObject,
BuildingGroundObject,
)
from gen.defenses.armor_group_generator import generate_armor_group_of_type_and_size
from gen.sam.ewr_group_generator import get_faction_possible_ewrs_generator
from gen.sam.sam_group_generator import get_faction_possible_sams_generator
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.QBudgetBox import QBudgetBox
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.groundobject.QBuildingInfo import QBuildingInfo
class QGroundObjectMenu(QDialog):
def __init__(
self,
parent,
ground_object: TheaterGroundObject,
buildings: Optional[List[TheaterGroundObject]],
cp: ControlPoint,
game: Game,
):
super().__init__(parent)
self.setMinimumWidth(350)
self.ground_object = ground_object
if buildings is None:
self.buildings = []
else:
self.buildings = buildings
self.cp = cp
self.game = game
self.setWindowTitle("Location " + self.ground_object.obj_name)
self.setWindowIcon(EVENT_ICONS["capture"])
self.intelBox = QGroupBox("Units :")
self.buildingBox = QGroupBox("Buildings :")
self.intelLayout = QGridLayout()
self.buildingsLayout = QGridLayout()
self.sell_all_button = None
self.total_value = 0
self.init_ui()
def init_ui(self):
self.mainLayout = QVBoxLayout()
self.budget = QBudgetBox(self.game)
self.budget.setGame(self.game)
self.doLayout()
if isinstance(self.ground_object, BuildingGroundObject):
self.mainLayout.addWidget(self.buildingBox)
if self.cp.captured:
self.mainLayout.addWidget(self.financesBox)
else:
self.mainLayout.addWidget(self.intelBox)
self.actionLayout = QHBoxLayout()
self.sell_all_button = QPushButton("Disband (+" + str(self.total_value) + "M)")
self.sell_all_button.clicked.connect(self.sell_all)
self.sell_all_button.setProperty("style", "btn-danger")
self.buy_replace = QPushButton("Buy/Replace")
self.buy_replace.clicked.connect(self.buy_group)
self.buy_replace.setProperty("style", "btn-success")
if self.ground_object.purchasable:
if self.total_value > 0:
self.actionLayout.addWidget(self.sell_all_button)
self.actionLayout.addWidget(self.buy_replace)
if self.cp.captured and self.ground_object.purchasable:
self.mainLayout.addLayout(self.actionLayout)
self.setLayout(self.mainLayout)
def doLayout(self):
self.update_total_value()
self.intelBox = QGroupBox("Units :")
self.intelLayout = QGridLayout()
i = 0
for g in self.ground_object.groups:
if not hasattr(g, "units_losts"):
g.units_losts = []
for unit in g.units:
unit_display_name = unit.type
dcs_unit_type = vehicles.vehicle_map.get(unit.type)
if dcs_unit_type is not None:
# Hack: Don't know which variant is used.
try:
unit_display_name = next(
GroundUnitType.for_dcs_type(dcs_unit_type)
).name
except StopIteration:
pass
self.intelLayout.addWidget(
QLabel(
"<b>Unit #"
+ str(unit.id)
+ " - "
+ str(unit_display_name)
+ "</b>"
),
i,
0,
)
i = i + 1
for unit in g.units_losts:
dcs_unit_type = vehicles.vehicle_map.get(unit.type)
if dcs_unit_type is None:
continue
# Hack: Don't know which variant is used.
try:
unit_type = next(GroundUnitType.for_dcs_type(dcs_unit_type))
name = unit_type.name
price = unit_type.price
except StopIteration:
name = dcs_unit_type.name
price = 0
self.intelLayout.addWidget(
QLabel(f"<b>Unit #{unit.id} - {name}</b> [DEAD]"), i, 0
)
if self.cp.captured:
repair = QPushButton(f"Repair [{price}M]")
repair.setProperty("style", "btn-success")
repair.clicked.connect(
lambda u=unit, g=g, p=unit_type.price: self.repair_unit(g, u, p)
)
self.intelLayout.addWidget(repair, i, 1)
i = i + 1
stretch = QVBoxLayout()
stretch.addStretch()
self.intelLayout.addLayout(stretch, i, 0)
self.buildingBox = QGroupBox("Buildings :")
self.buildingsLayout = QGridLayout()
j = 0
total_income = 0
received_income = 0
for i, building in enumerate(self.buildings):
if building.dcs_identifier not in FORTIFICATION_BUILDINGS:
self.buildingsLayout.addWidget(
QBuildingInfo(building, self.ground_object), j / 3, j % 3
)
j = j + 1
if building.category in REWARDS.keys():
total_income = total_income + REWARDS[building.category]
if not building.is_dead:
received_income = received_income + REWARDS[building.category]
else:
logging.warning(building.category + " not in REWARDS")
self.financesBox = QGroupBox("Finances: ")
self.financesBoxLayout = QGridLayout()
self.financesBoxLayout.addWidget(
QLabel("Available: " + str(total_income) + "M"), 2, 1
)
self.financesBoxLayout.addWidget(
QLabel("Receiving: " + str(received_income) + "M"), 2, 2
)
self.financesBox.setLayout(self.financesBoxLayout)
self.buildingBox.setLayout(self.buildingsLayout)
self.intelBox.setLayout(self.intelLayout)
def do_refresh_layout(self):
try:
for i in range(self.mainLayout.count()):
item = self.mainLayout.itemAt(i)
if item is not None and item.widget() is not None:
item.widget().setParent(None)
self.sell_all_button.setParent(None)
self.buy_replace.setParent(None)
self.actionLayout.setParent(None)
self.doLayout()
if isinstance(self.ground_object, BuildingGroundObject):
self.mainLayout.addWidget(self.buildingBox)
else:
self.mainLayout.addWidget(self.intelBox)
self.actionLayout = QHBoxLayout()
if self.total_value > 0:
self.actionLayout.addWidget(self.sell_all_button)
self.actionLayout.addWidget(self.buy_replace)
if self.cp.captured and self.ground_object.purchasable:
self.mainLayout.addLayout(self.actionLayout)
except Exception as e:
logging.exception(e)
self.update_total_value()
def update_total_value(self):
total_value = 0
if not self.ground_object.purchasable:
return
for u in self.ground_object.units:
# Hack: Unknown variant.
unit_type = next(GroundUnitType.for_dcs_type(vehicles.vehicle_map[u.type]))
total_value += unit_type.price
if self.sell_all_button is not None:
self.sell_all_button.setText("Disband (+$" + str(self.total_value) + "M)")
self.total_value = total_value
def repair_unit(self, group, unit, price):
if self.game.budget > price:
self.game.budget -= price
group.units_losts = [u for u in group.units_losts if u.id != unit.id]
group.units.append(unit)
GameUpdateSignal.get_instance().updateGame(self.game)
# Remove destroyed units in the vicinity
destroyed_units = self.game.get_destroyed_units()
for d in destroyed_units:
p = Point(d["x"], d["z"])
if p.distance_to_point(unit.position) < 15:
destroyed_units.remove(d)
logging.info("Removed destroyed units " + str(d))
logging.info("Repaired unit : " + str(unit.id) + " " + str(unit.type))
self.do_refresh_layout()
def sell_all(self):
self.update_total_value()
self.game.budget = self.game.budget + self.total_value
self.ground_object.groups = []
self.do_refresh_layout()
GameUpdateSignal.get_instance().updateGame(self.game)
def buy_group(self):
self.subwindow = QBuyGroupForGroundObjectDialog(
self, self.ground_object, self.cp, self.game, self.total_value
)
self.subwindow.show()
class QBuyGroupForGroundObjectDialog(QDialog):
def __init__(
self,
parent,
ground_object: TheaterGroundObject,
cp: ControlPoint,
game: Game,
current_group_value: int,
):
super(QBuyGroupForGroundObjectDialog, self).__init__(parent)
self.setMinimumWidth(350)
self.ground_object = ground_object
self.cp = cp
self.game = game
self.current_group_value = current_group_value
self.setWindowTitle("Buy units @ " + self.ground_object.obj_name)
self.setWindowIcon(EVENT_ICONS["capture"])
self.buySamButton = QPushButton("Buy")
self.buyArmorButton = QPushButton("Buy")
self.buySamLayout = QGridLayout()
self.buyArmorLayout = QGridLayout()
self.amount = QSpinBox()
self.buyArmorCombo = QComboBox()
self.samCombo = QComboBox()
self.buySamBox = QGroupBox("Buy SAM site :")
self.buyArmorBox = QGroupBox("Buy defensive position :")
faction = self.game.player_faction
# Sams
possible_sams = get_faction_possible_sams_generator(faction)
for sam in possible_sams:
self.samCombo.addItem(
sam.name + " [$" + str(sam.price) + "M]", userData=sam
)
self.samCombo.currentIndexChanged.connect(self.samComboChanged)
self.buySamLayout.addWidget(QLabel("Site Type :"), 0, 0, Qt.AlignLeft)
self.buySamLayout.addWidget(self.samCombo, 0, 1, alignment=Qt.AlignRight)
self.buySamLayout.addWidget(self.buySamButton, 1, 1, alignment=Qt.AlignRight)
stretch = QVBoxLayout()
stretch.addStretch()
self.buySamLayout.addLayout(stretch, 2, 0)
self.buySamButton.clicked.connect(self.buySam)
# EWRs
buy_ewr_box = QGroupBox("Buy EWR:")
buy_ewr_layout = QGridLayout()
buy_ewr_box.setLayout(buy_ewr_layout)
buy_ewr_layout.addWidget(QLabel("Radar type:"), 0, 0, Qt.AlignLeft)
self.ewr_selector = QComboBox()
buy_ewr_layout.addWidget(self.ewr_selector, 0, 1, alignment=Qt.AlignRight)
ewr_types = get_faction_possible_ewrs_generator(faction)
for ewr_type in ewr_types:
self.ewr_selector.addItem(
f"{ewr_type.name()} [${ewr_type.price()}M]", ewr_type
)
self.ewr_selector.currentIndexChanged.connect(self.on_ewr_selection_changed)
self.buy_ewr_button = QPushButton("Buy")
self.buy_ewr_button.clicked.connect(self.buy_ewr)
buy_ewr_layout.addWidget(self.buy_ewr_button, 1, 1, alignment=Qt.AlignRight)
stretch = QVBoxLayout()
stretch.addStretch()
buy_ewr_layout.addLayout(stretch, 2, 0)
# Armored units
for unit in set(faction.ground_units):
self.buyArmorCombo.addItem(f"{unit} [${unit.price}M]", userData=unit)
self.buyArmorCombo.currentIndexChanged.connect(self.armorComboChanged)
self.amount.setMinimum(2)
self.amount.setMaximum(8)
self.amount.setValue(2)
self.amount.valueChanged.connect(self.amountComboChanged)
self.buyArmorLayout.addWidget(QLabel("Unit type :"), 0, 0, Qt.AlignLeft)
self.buyArmorLayout.addWidget(self.buyArmorCombo, 0, 1, alignment=Qt.AlignRight)
self.buyArmorLayout.addWidget(
QLabel("Group size :"), 1, 0, alignment=Qt.AlignLeft
)
self.buyArmorLayout.addWidget(self.amount, 1, 1, alignment=Qt.AlignRight)
self.buyArmorLayout.addWidget(
self.buyArmorButton, 2, 1, alignment=Qt.AlignRight
)
stretch2 = QVBoxLayout()
stretch2.addStretch()
self.buyArmorLayout.addLayout(stretch2, 3, 0)
self.buyArmorButton.clicked.connect(self.buyArmor)
# Do layout
self.buySamBox.setLayout(self.buySamLayout)
self.buyArmorBox.setLayout(self.buyArmorLayout)
self.mainLayout = QHBoxLayout()
if isinstance(self.ground_object, SamGroundObject):
self.mainLayout.addWidget(self.buySamBox)
elif isinstance(self.ground_object, VehicleGroupGroundObject):
self.mainLayout.addWidget(self.buyArmorBox)
elif isinstance(self.ground_object, EwrGroundObject):
self.mainLayout.addWidget(buy_ewr_box)
self.setLayout(self.mainLayout)
try:
self.samComboChanged(0)
self.armorComboChanged(0)
self.on_ewr_selection_changed(0)
except:
pass
def samComboChanged(self, index):
self.buySamButton.setText(
"Buy [$"
+ str(self.samCombo.itemData(index).price)
+ "M] [-$"
+ str(self.current_group_value)
+ "M]"
)
def on_ewr_selection_changed(self, index):
ewr = self.ewr_selector.itemData(index)
self.buy_ewr_button.setText(
f"Buy [${ewr.price()}M][-${self.current_group_value}M]"
)
def armorComboChanged(self, index):
unit_type = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())
price = unit_type.price * self.amount.value()
self.buyArmorButton.setText(f"Buy [${price}M][-${self.current_group_value}M]")
def amountComboChanged(self):
unit_type = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())
price = unit_type.price * self.amount.value()
self.buyArmorButton.setText(f"Buy [${price}M][-${self.current_group_value}M]")
def buyArmor(self):
logging.info("Buying Armor ")
utype = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())
price = utype.price * self.amount.value() - self.current_group_value
if price > self.game.budget:
self.error_money()
self.close()
return
else:
self.game.budget -= price
# Generate Armor
group = generate_armor_group_of_type_and_size(
self.game, self.ground_object, utype, int(self.amount.value())
)
self.ground_object.groups = [group]
GameUpdateSignal.get_instance().updateGame(self.game)
def buySam(self):
sam_generator = self.samCombo.itemData(self.samCombo.currentIndex())
price = sam_generator.price - self.current_group_value
if price > self.game.budget:
self.error_money()
return
else:
self.game.budget -= price
# Generate SAM
generator = sam_generator(self.game, self.ground_object)
generator.generate()
self.ground_object.groups = list(generator.groups)
GameUpdateSignal.get_instance().updateGame(self.game)
def buy_ewr(self):
ewr_generator = self.ewr_selector.itemData(self.ewr_selector.currentIndex())
price = ewr_generator.price() - self.current_group_value
if price > self.game.budget:
self.error_money()
return
else:
self.game.budget -= price
generator = ewr_generator(self.game, self.ground_object)
generator.generate()
self.ground_object.groups = [generator.vg]
GameUpdateSignal.get_instance().updateGame(self.game)
def error_money(self):
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setText("Not enough money to buy these units !")
msg.setWindowTitle("Not enough money")
msg.setStandardButtons(QMessageBox.Ok)
msg.setWindowFlags(Qt.WindowStaysOnTopHint)
msg.exec_()
self.close()