Refactor Templates to Layouts, Review and Cleanup

- Fix tgogenerator
- Fix UI for ForceGroup and Layouts
- Fix ammo depot handling
- Split bigger files in smaller meaningful files (TGO, layouts, forces)
- Renamed Template to Layout
- Renamed GroundGroup to TheaterGroup and GroundUnit to TheaterUnit
- Reorganize Layouts and UnitGroups to a ArmedForces class and ForceGroup similar to the AirWing and Squadron
- Reworded the UnitClass, GroupRole, GroupTask (adopted to PEP8) and reworked the connection from Role and Task
- added comments
- added missing unit classes
- added temp workaround for missing classes
- add repariable property to TheaterUnit
- Review and Cleanup

Added serialization for loaded templates

Loading the templates from the .miz files takes a lot of computation time and in the future there will be more templates added to the system. Therefore a local pickle serialization for the loaded templates was re-added:
- The pickle will be created the first time the TemplateLoader will be accessed
- Pickle is stored in Liberation SaveDir
- Added UI option to (re-)import templates
This commit is contained in:
RndName
2022-02-10 12:23:16 +01:00
parent 1ae6503ceb
commit 2c17a9a52e
138 changed files with 1985 additions and 3096 deletions

View File

@@ -1,8 +1,8 @@
from PySide2.QtGui import QStandardItem, QStandardItemModel
from game import Game
from game.theater import ControlPointType, BuildingGroundObject
from game.theater.theatergroundobject import IadsGroundObject
from game.theater.controlpoint import ControlPointType
from game.theater.theatergroundobject import IadsGroundObject, BuildingGroundObject
from game.utils import Distance
from game.missiongenerator.frontlineconflictdescription import (
FrontLineConflictDescription,
@@ -131,7 +131,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
"["
+ str(ground_object.obj_name)
+ "] : "
+ u.type
+ u.name
+ " #"
+ str(j)
)
@@ -140,9 +140,9 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
wpt.obj_name = ground_object.obj_name
wpt.waypoint_type = FlightWaypointType.CUSTOM
if cp.captured:
wpt.description = "Friendly unit : " + u.type
wpt.description = "Friendly unit: " + u.name
else:
wpt.description = "Enemy unit : " + u.type
wpt.description = "Enemy unit: " + u.name
i = add_model_item(i, model, wpt.pretty_name, wpt)
if self.include_airbases:

View File

@@ -49,10 +49,10 @@ class QStrikeTargetInfoView(QGroupBox):
if len(self.strike_target_infos.units) > 0:
dic = {}
for u in self.strike_target_infos.units:
if u.type in dic.keys():
dic[u.type] = dic[u.type] + 1
if u.type.id in dic.keys():
dic[u.type.id] = dic[u.type.id] + 1
else:
dic[u.type] = 1
dic[u.type.id] = 1
for k, v in dic.items():
model.appendRow(QStandardItem(k + " x " + str(v)))
print(k + " x " + str(v))

View File

@@ -18,7 +18,7 @@ from PySide2.QtWidgets import (
)
import qt_ui.uiconstants as CONST
from game import Game, VERSION, persistency
from game import Game, VERSION, persistency, db
from game.debriefing import Debriefing
from game.server import EventStream, GameContext
from game.server.security import ApiKeyManager
@@ -178,6 +178,9 @@ class QLiberationWindow(QMainWindow):
self.openNotesAction.setIcon(CONST.ICONS["Notes"])
self.openNotesAction.triggered.connect(self.showNotesDialog)
self.importTemplatesAction = QAction("Import Layouts", self)
self.importTemplatesAction.triggered.connect(self.import_templates)
self.enable_game_actions(False)
def enable_game_actions(self, enabled: bool):
@@ -220,6 +223,9 @@ class QLiberationWindow(QMainWindow):
file_menu.addSeparator()
file_menu.addAction("E&xit", self.close)
tools_menu = self.menu.addMenu("&Developer tools")
tools_menu.addAction(self.importTemplatesAction)
help_menu = self.menu.addMenu("&Help")
help_menu.addAction(self.openDiscordAction)
help_menu.addAction(self.openGithubAction)
@@ -393,6 +399,9 @@ class QLiberationWindow(QMainWindow):
self.dialog = QNotesWindow(self.game)
self.dialog.show()
def import_templates(self):
db.LAYOUTS.import_templates()
def showLogsDialog(self):
self.dialog = QLogsWindow()
self.dialog.show()

View File

@@ -2,22 +2,22 @@ import os
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QLabel, QVBoxLayout
from game.theater import GroundUnit
from game.theater import TheaterUnit
from game.config import REWARDS
class QBuildingInfo(QGroupBox):
def __init__(self, building, ground_object):
def __init__(self, building: TheaterUnit, ground_object):
super(QBuildingInfo, self).__init__()
self.building: GroundUnit = building
self.building = building
self.ground_object = ground_object
self.init_ui()
def init_ui(self):
self.header = QLabel()
path = os.path.join(
"./resources/ui/units/buildings/" + self.building.type + ".png"
"./resources/ui/units/buildings/" + self.building.icon + ".png"
)
if not self.building.alive:
pixmap = QPixmap("./resources/ui/units/buildings/dead.png")
@@ -26,17 +26,13 @@ class QBuildingInfo(QGroupBox):
else:
pixmap = QPixmap("./resources/ui/units/buildings/missing.png")
self.header.setPixmap(pixmap)
name = "<b>{}</b> {}".format(
self.building.type[0:18],
"[DEAD]" if not self.building.alive else "",
)
self.name = QLabel(name)
self.name = QLabel(self.building.short_name)
self.name.setProperty("style", "small")
layout = QVBoxLayout()
layout.addWidget(self.header)
layout.addWidget(self.name)
if self.ground_object.category in REWARDS.keys():
if self.ground_object.category in REWARDS:
income_label_text = (
"Value: " + str(REWARDS[self.ground_object.category]) + "M"
)

View File

@@ -1,5 +1,6 @@
import logging
from typing import Optional
from dataclasses import dataclass, field
from typing import Type
from PySide2.QtCore import Signal
from PySide2.QtGui import Qt
@@ -9,95 +10,116 @@ from PySide2.QtWidgets import (
QGridLayout,
QGroupBox,
QLabel,
QMessageBox,
QPushButton,
QSpinBox,
QVBoxLayout,
QCheckBox,
)
from dcs.unittype import UnitType
from game import Game
from game.data.groups import GroupRole, ROLE_TASKINGS, GroupTask
from game.armedforces.forcegroup import ForceGroup
from game.data.groups import GroupRole, GroupTask
from game.point_with_heading import PointWithHeading
from game.theater import TheaterGroundObject
from game.theater.theatergroundobject import (
VehicleGroupGroundObject,
SamGroundObject,
EwrGroundObject,
GroundGroup,
)
from gen.templates import (
GroundObjectTemplate,
GroupTemplate,
from game.theater.theatergroup import TheaterGroup
from game.layout.layout import (
TheaterLayout,
GroupLayout,
)
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
@dataclass
class QGroupLayout:
layout: GroupLayout
dcs_unit_type: Type[UnitType]
amount: int
unit_price: int
enabled: bool = True
@property
def price(self) -> int:
return self.amount * self.unit_price if self.enabled else 0
@dataclass
class QLayout:
layout: TheaterLayout
force_group: ForceGroup
group_layouts: list[QGroupLayout] = field(default_factory=list)
@property
def price(self) -> int:
return sum(group.price for group in self.group_layouts)
class QGroundObjectGroupTemplate(QGroupBox):
group_template_changed = Signal(GroupTemplate)
# UI to show one GroupTemplate and configure the TemplateRandomizer for it
# one row: [Required | Unit Selector | Amount | Price]
# If the group is not randomizable: Just view labels instead of edit fields
group_template_changed = Signal()
def __init__(self, group_id: int, group_template: GroupTemplate) -> None:
super(QGroundObjectGroupTemplate, self).__init__(
f"{group_id + 1}: {group_template.name}"
)
self.group_template = group_template
self.group_layout = QGridLayout()
self.setLayout(self.group_layout)
def __init__(
self, group_id: int, force_group: ForceGroup, group_layout: GroupLayout
) -> None:
super().__init__(f"{group_id + 1}: {group_layout.name}")
self.grid_layout = QGridLayout()
self.setLayout(self.grid_layout)
self.amount_selector = QSpinBox()
self.unit_selector = QComboBox()
self.group_selector = QCheckBox()
self.group_selector.setChecked(self.group_template.should_be_generated)
self.group_selector.setEnabled(self.group_template.optional)
if self.group_template.can_be_modified:
# Group can be modified (more than 1 possible unit_type for the group)
for unit in self.group_template.possible_units:
self.unit_selector.addItem(f"{unit} [${unit.price}M]", userData=unit)
self.group_layout.addWidget(
self.unit_selector, 0, 0, alignment=Qt.AlignRight
# Add all possible units with the price
for unit_type in force_group.unit_types_for_group(group_layout):
self.unit_selector.addItem(
f"{unit_type.name} [${unit_type.price}M]",
userData=(unit_type.dcs_unit_type, unit_type.price),
)
self.group_layout.addWidget(
self.amount_selector, 0, 1, alignment=Qt.AlignRight
# Add all possible statics with price = 0
for static_type in force_group.statics_for_group(group_layout):
self.unit_selector.addItem(
f"{static_type} (Static)", userData=(static_type, 0)
)
self.unit_selector.setEnabled(self.unit_selector.count() > 1)
self.grid_layout.addWidget(self.unit_selector, 0, 0, alignment=Qt.AlignRight)
self.grid_layout.addWidget(self.amount_selector, 0, 1, alignment=Qt.AlignRight)
self.amount_selector.setMinimum(1)
self.amount_selector.setMaximum(self.group_template.max_size)
self.amount_selector.setValue(self.group_template.size)
unit_type, price = self.unit_selector.itemData(
self.unit_selector.currentIndex()
)
self.on_group_changed()
else:
# Group can not be randomized so just show the group info
group_info = QVBoxLayout()
try:
unit_name = next(self.group_template.possible_units)
except StopIteration:
unit_name = self.group_template.unit_type
group_info.addWidget(
QLabel(f"{self.group_template.size}x {unit_name}"),
alignment=Qt.AlignLeft,
)
self.group_layout.addLayout(group_info, 0, 0, 1, 2)
self.group_layout = QGroupLayout(
group_layout, unit_type, group_layout.unit_counter, price
)
self.group_layout.addWidget(self.group_selector, 0, 2, alignment=Qt.AlignRight)
self.group_selector.setChecked(self.group_layout.enabled)
self.group_selector.setEnabled(self.group_layout.layout.optional)
self.amount_selector.setMinimum(1)
self.amount_selector.setMaximum(self.group_layout.layout.max_size)
self.amount_selector.setValue(self.group_layout.amount)
self.amount_selector.setEnabled(self.group_layout.layout.max_size > 1)
self.grid_layout.addWidget(self.group_selector, 0, 2, alignment=Qt.AlignRight)
self.amount_selector.valueChanged.connect(self.on_group_changed)
self.unit_selector.currentIndexChanged.connect(self.on_group_changed)
self.group_selector.stateChanged.connect(self.on_group_changed)
def on_group_changed(self) -> None:
self.group_template.set_enabled(self.group_selector.isChecked())
if self.group_template.can_be_modified:
unit_type = self.unit_selector.itemData(self.unit_selector.currentIndex())
self.group_template.unit_count = [self.amount_selector.value()]
self.group_template.set_unit_type(unit_type.dcs_id)
self.group_template_changed.emit(self.group_template)
self.group_layout.enabled = self.group_selector.isChecked()
unit_type, price = self.unit_selector.itemData(
self.unit_selector.currentIndex()
)
self.group_layout.dcs_unit_type = unit_type
self.group_layout.unit_price = price
self.group_layout.amount = self.amount_selector.value()
self.group_template_changed.emit()
class QGroundObjectTemplateLayout(QGroupBox):
@@ -105,20 +127,22 @@ class QGroundObjectTemplateLayout(QGroupBox):
self,
game: Game,
ground_object: TheaterGroundObject,
template_changed_signal: Signal(GroundObjectTemplate),
layout: QLayout,
layout_changed_signal: Signal(QLayout),
current_group_value: int,
):
super(QGroundObjectTemplateLayout, self).__init__("Groups:")
super().__init__("Groups:")
# Connect to the signal to handle template updates
self.game = game
self.ground_object = ground_object
self.template_changed_signal = template_changed_signal
self.template_changed_signal.connect(self.load_for_template)
self.template: Optional[GroundObjectTemplate] = None
self.layout_changed_signal = layout_changed_signal
self.layout_model = layout
self.layout_changed_signal.connect(self.load_for_layout)
self.current_group_value = current_group_value
self.buy_button = QPushButton("Buy")
self.buy_button.setEnabled(False)
self.buy_button.clicked.connect(self.buy_group)
self.template_layout = QGridLayout()
@@ -131,78 +155,73 @@ class QGroundObjectTemplateLayout(QGroupBox):
stretch.addStretch()
self.template_layout.addLayout(stretch, 2, 0)
def load_for_template(self, template: GroundObjectTemplate) -> None:
self.template = template
# Load Layout
self.load_for_layout(self.layout_model)
def load_for_layout(self, layout: QLayout) -> None:
self.layout_model = layout
# Clean the current grid
for id in range(self.template_grid.count()):
self.template_grid.itemAt(id).widget().deleteLater()
for g_id, group in enumerate(template.groups):
group_row = QGroundObjectGroupTemplate(g_id, group)
for g_id, layout_group in enumerate(self.layout_model.layout.groups):
group_row = QGroundObjectGroupTemplate(
g_id, self.layout_model.force_group, layout_group
)
self.layout_model.group_layouts.append(group_row.group_layout)
group_row.group_template_changed.connect(self.group_template_changed)
self.template_grid.addWidget(group_row)
self.update_price()
self.group_template_changed()
def group_template_changed(self, group_template: GroupTemplate) -> None:
self.update_price()
def update_price(self) -> None:
price = "$" + str(self.template.estimated_price_for(self.ground_object))
self.buy_button.setText(f"Buy [{price}M][-${self.current_group_value}M]")
def group_template_changed(self) -> None:
price = self.layout_model.price
self.buy_button.setText(f"Buy [${price}M][-${self.current_group_value}M]")
self.buy_button.setEnabled(price <= self.game.blue.budget)
if self.buy_button.isEnabled():
self.buy_button.setToolTip("Buy the group")
else:
self.buy_button.setToolTip("Not enough money to buy this group")
def buy_group(self):
if not self.template:
return
groups = self.generate_groups()
price = 0
for group in groups:
for unit in group.units:
if unit.unit_type:
price += unit.unit_type.price
price -= self.current_group_value
if not self.layout:
raise RuntimeError("No template selected. GroundObject can not be bought.")
price = self.layout_model.price
if price > self.game.blue.budget:
self.error_money()
self.close()
# Somethin went wrong. Buy button should be disabled!
logging.error("Not enough money to buy the group")
return
else:
self.game.blue.budget -= price
self.ground_object.groups = groups
self.game.blue.budget -= price - self.current_group_value
self.ground_object.groups = self.generate_groups()
# Replan redfor missions
self.game.initialize_turn(for_red=True, for_blue=False)
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()
def generate_groups(self) -> list[GroundGroup]:
go = self.template.generate(
def generate_groups(self) -> list[TheaterGroup]:
go = self.layout_model.layout.create_ground_object(
self.ground_object.name,
PointWithHeading.from_point(
self.ground_object.position, self.ground_object.heading
),
self.ground_object.control_point,
self.game,
)
for group in self.layout_model.group_layouts:
self.layout_model.force_group.create_theater_group_for_tgo(
go,
group.layout,
self.ground_object.name,
self.game,
group.dcs_unit_type, # Forced Type
group.amount, # Forced Amount
)
return go.groups
class QGroundObjectBuyMenu(QDialog):
template_changed_signal = Signal(GroundObjectTemplate)
layout_changed_signal = Signal(QLayout)
def __init__(
self,
@@ -211,7 +230,7 @@ class QGroundObjectBuyMenu(QDialog):
game: Game,
current_group_value: int,
):
super(QGroundObjectBuyMenu, self).__init__(parent)
super().__init__(parent)
self.setMinimumWidth(350)
@@ -221,66 +240,90 @@ class QGroundObjectBuyMenu(QDialog):
self.mainLayout = QGridLayout()
self.setLayout(self.mainLayout)
self.unit_group_selector = QComboBox()
self.template_selector = QComboBox()
self.template_selector.setEnabled(False)
self.force_group_selector = QComboBox()
self.layout_selector = QComboBox()
self.layout_selector.setEnabled(False)
# Get the templates and fill the combobox
template_sub_category = None
# Get the layouts and fill the combobox
tasks = []
if isinstance(ground_object, SamGroundObject):
role = GroupRole.AntiAir
role = GroupRole.AIR_DEFENSE
elif isinstance(ground_object, VehicleGroupGroundObject):
role = GroupRole.GroundForce
role = GroupRole.GROUND_FORCE
elif isinstance(ground_object, EwrGroundObject):
role = GroupRole.AntiAir
tasks.append(GroupTask.EWR)
role = GroupRole.AIR_DEFENSE
tasks.append(GroupTask.EARLY_WARNING_RADAR)
else:
raise RuntimeError
raise NotImplementedError(f"Unhandled TGO type {ground_object.__class__}")
if not tasks:
tasks = ROLE_TASKINGS[role]
tasks = role.tasks
for unit_group in game.blue.faction.groups_for_role_and_tasks(role, tasks):
self.unit_group_selector.addItem(unit_group.name, userData=unit_group)
for group in game.blue.armed_forces.groups_for_tasks(tasks):
self.force_group_selector.addItem(group.name, userData=group)
self.force_group_selector.setEnabled(self.force_group_selector.count() > 1)
self.template_selector.currentIndexChanged.connect(self.template_changed)
self.unit_group_selector.currentIndexChanged.connect(self.unit_group_changed)
force_group = self.force_group_selector.itemData(
self.force_group_selector.currentIndex()
)
for layout in force_group.layouts:
self.layout_selector.addItem(layout.name, userData=layout)
selected_template = self.layout_selector.itemData(
self.layout_selector.currentIndex()
)
self.layout_model = QLayout(selected_template, force_group)
self.layout_selector.currentIndexChanged.connect(self.layout_changed)
self.force_group_selector.currentIndexChanged.connect(self.force_group_changed)
template_selector_layout = QGridLayout()
template_selector_layout.addWidget(QLabel("UnitGroup :"), 0, 0, Qt.AlignLeft)
template_selector_layout.addWidget(
self.unit_group_selector, 0, 1, alignment=Qt.AlignRight
QLabel("Armed Forces Group:"), 0, 0, Qt.AlignLeft
)
template_selector_layout.addWidget(QLabel("Template :"), 1, 0, Qt.AlignLeft)
template_selector_layout.addWidget(
self.template_selector, 1, 1, alignment=Qt.AlignRight
self.force_group_selector, 0, 1, alignment=Qt.AlignRight
)
template_selector_layout.addWidget(QLabel("Layout:"), 1, 0, Qt.AlignLeft)
template_selector_layout.addWidget(
self.layout_selector, 1, 1, alignment=Qt.AlignRight
)
self.mainLayout.addLayout(template_selector_layout, 0, 0)
self.template_layout = QGroundObjectTemplateLayout(
game, ground_object, self.template_changed_signal, current_group_value
game,
ground_object,
self.layout_model,
self.layout_changed_signal,
current_group_value,
)
self.mainLayout.addWidget(self.template_layout, 1, 0)
self.setLayout(self.mainLayout)
# Update UI
self.unit_group_changed()
def unit_group_changed(self) -> None:
unit_group = self.unit_group_selector.itemData(
self.unit_group_selector.currentIndex()
def force_group_changed(self) -> None:
# Prevent ComboBox from firing change Events
self.layout_selector.blockSignals(True)
unit_group = self.force_group_selector.itemData(
self.force_group_selector.currentIndex()
)
self.template_selector.clear()
if unit_group.templates:
for template in unit_group.templates:
self.template_selector.addItem(template.name, userData=template)
self.layout_selector.clear()
for layout in unit_group.layouts:
self.layout_selector.addItem(layout.name, userData=layout)
# Enable if more than one template is available
self.template_selector.setEnabled(len(unit_group.templates) > 1)
self.layout_selector.setEnabled(len(unit_group.layouts) > 1)
# Enable Combobox Signals again
self.layout_selector.blockSignals(False)
self.layout_changed()
def template_changed(self):
template = self.template_selector.itemData(
self.template_selector.currentIndex()
def layout_changed(self):
self.layout()
self.layout_model.layout = self.layout_selector.itemData(
self.layout_selector.currentIndex()
)
if template is not None:
self.template_changed_signal.emit(template)
self.layout_model.force_group = self.force_group_selector.itemData(
self.force_group_selector.currentIndex()
)
self.layout_model.group_layouts = []
self.layout_changed_signal.emit(self.layout_model)

View File

@@ -9,13 +9,11 @@ from PySide2.QtWidgets import (
QPushButton,
QVBoxLayout,
)
from dcs import Point, vehicles
from dcs import Point
from game import Game
from game.config import REWARDS
from game.data.building_data import FORTIFICATION_BUILDINGS
from game.data.units import UnitClass
from game.dcs.groundunittype import GroundUnitType
from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import (
BuildingGroundObject,
@@ -101,7 +99,7 @@ class QGroundObjectMenu(QDialog):
QLabel(f"<b>Unit {str(unit.display_name)}</b>"), i, 0
)
if not unit.alive and self.cp.captured:
if not unit.alive and unit.repairable and self.cp.captured:
price = unit.unit_type.price if unit.unit_type else 0
repair = QPushButton(f"Repair [{price}M]")
repair.setProperty("style", "btn-success")
@@ -176,19 +174,13 @@ class QGroundObjectMenu(QDialog):
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.
if u.type in vehicles.vehicle_map:
unit_type = next(
GroundUnitType.for_dcs_type(vehicles.vehicle_map[u.type])
)
total_value += unit_type.price
self.total_value = sum(
u.unit_type.price for u in self.ground_object.units if u.unit_type
)
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, unit, price):
if self.game.blue.budget > price:
@@ -203,7 +195,7 @@ class QGroundObjectMenu(QDialog):
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))
logging.info(f"Repaired unit: {unit.unit_name}")
self.do_refresh_layout()