mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Generate settings pages automatically.
This adds metadata to settings fields that can be used to automatically generate the settings window. For now I have replaced the Difficulty page. Will follow up to replace the others.
This commit is contained in:
parent
39fae9effc
commit
7bec4c62f7
119
game/settings.py
119
game/settings.py
@ -1,119 +0,0 @@
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from enum import Enum, unique
|
||||
from typing import Dict, Optional, Any
|
||||
|
||||
from dcs.forcedoptions import ForcedOptions
|
||||
|
||||
|
||||
@unique
|
||||
class AutoAtoBehavior(Enum):
|
||||
Disabled = "Disabled"
|
||||
Never = "Never assign player pilots"
|
||||
Default = "No preference"
|
||||
Prefer = "Prefer player pilots"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
version: Optional[str] = None
|
||||
|
||||
# Difficulty settings
|
||||
# AI Difficulty
|
||||
player_skill: str = "Good"
|
||||
enemy_skill: str = "Average"
|
||||
enemy_vehicle_skill: str = "Average"
|
||||
player_income_multiplier: float = 1.0
|
||||
enemy_income_multiplier: float = 1.0
|
||||
invulnerable_player_pilots: bool = True
|
||||
# Mission Difficulty
|
||||
night_disabled: bool = False
|
||||
manpads: bool = True
|
||||
# Mission Restrictions
|
||||
labels: str = "Full"
|
||||
map_coalition_visibility: ForcedOptions.Views = ForcedOptions.Views.All
|
||||
external_views_allowed: bool = True
|
||||
battle_damage_assessment: Optional[bool] = None
|
||||
|
||||
# Campaign management
|
||||
# General
|
||||
restrict_weapons_by_date: bool = False
|
||||
disable_legacy_aewc: bool = True
|
||||
disable_legacy_tanker: bool = True
|
||||
# Pilots and Squadrons
|
||||
ai_pilot_levelling: bool = True
|
||||
#: Feature flag for squadron limits.
|
||||
enable_squadron_pilot_limits: bool = False
|
||||
#: The maximum number of pilots a squadron can have at one time. Changing this after
|
||||
#: the campaign has started will have no immediate effect; pilots already in the
|
||||
#: squadron will not be removed if the limit is lowered and pilots will not be
|
||||
#: immediately created if the limit is raised.
|
||||
squadron_pilot_limit: int = 12
|
||||
#: The number of pilots a squadron can replace per turn.
|
||||
squadron_replenishment_rate: int = 4
|
||||
# HQ Automation
|
||||
automate_runway_repair: bool = False
|
||||
automate_front_line_reinforcements: bool = False
|
||||
automate_aircraft_reinforcements: bool = False
|
||||
auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default
|
||||
auto_ato_player_missions_asap: bool = True
|
||||
automate_front_line_stance: bool = True
|
||||
reserves_procurement_target: int = 10
|
||||
|
||||
# Mission Generator
|
||||
# Gameplay
|
||||
supercarrier: bool = False
|
||||
generate_marks: bool = True
|
||||
generate_dark_kneeboard: bool = False
|
||||
never_delay_player_flights: bool = False
|
||||
default_start_type: str = "Cold"
|
||||
# Mission specific
|
||||
desired_player_mission_duration: timedelta = timedelta(minutes=60)
|
||||
# Performance
|
||||
perf_smoke_gen: bool = True
|
||||
perf_smoke_spacing = 1600
|
||||
perf_red_alert_state: bool = True
|
||||
perf_artillery: bool = True
|
||||
perf_moving_units: bool = True
|
||||
perf_infantry: bool = True
|
||||
perf_destroyed_units: bool = True
|
||||
# Performance culling
|
||||
perf_culling: bool = False
|
||||
perf_culling_distance: int = 100
|
||||
perf_do_not_cull_carrier = True
|
||||
|
||||
# Cheating
|
||||
show_red_ato: bool = False
|
||||
enable_frontline_cheats: bool = False
|
||||
enable_base_capture_cheat: bool = False
|
||||
|
||||
# LUA Plugins system
|
||||
plugins: Dict[str, bool] = field(default_factory=dict)
|
||||
|
||||
only_player_takeoff: bool = True # Legacy parameter do not use
|
||||
|
||||
@staticmethod
|
||||
def plugin_settings_key(identifier: str) -> str:
|
||||
return f"plugins.{identifier}"
|
||||
|
||||
def initialize_plugin_option(self, identifier: str, default_value: bool) -> None:
|
||||
try:
|
||||
self.plugin_option(identifier)
|
||||
except KeyError:
|
||||
self.set_plugin_option(identifier, default_value)
|
||||
|
||||
def plugin_option(self, identifier: str) -> bool:
|
||||
return self.plugins[self.plugin_settings_key(identifier)]
|
||||
|
||||
def set_plugin_option(self, identifier: str, enabled: bool) -> None:
|
||||
self.plugins[self.plugin_settings_key(identifier)] = enabled
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
# __setstate__ is called with the dict of the object being unpickled. We
|
||||
# can provide save compatibility for new settings options (which
|
||||
# normally would not be present in the unpickled object) by creating a
|
||||
# new settings object, updating it with the unpickled state, and
|
||||
# updating our dict with that.
|
||||
new_state = Settings().__dict__
|
||||
new_state.update(state)
|
||||
self.__dict__.update(new_state)
|
||||
5
game/settings/__init__.py
Normal file
5
game/settings/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from .booleanoption import BooleanOption
|
||||
from .boundedfloatoption import BoundedFloatOption
|
||||
from .choicesoption import ChoicesOption
|
||||
from .optiondescription import OptionDescription
|
||||
from .settings import AutoAtoBehavior, Settings
|
||||
22
game/settings/booleanoption.py
Normal file
22
game/settings/booleanoption.py
Normal file
@ -0,0 +1,22 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional
|
||||
|
||||
from .optiondescription import OptionDescription, SETTING_DESCRIPTION_KEY
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BooleanOption(OptionDescription):
|
||||
pass
|
||||
|
||||
|
||||
def boolean_option(
|
||||
text: str,
|
||||
page: str,
|
||||
section: str,
|
||||
detail: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> bool:
|
||||
return field(
|
||||
metadata={SETTING_DESCRIPTION_KEY: BooleanOption(page, section, text, detail)},
|
||||
**kwargs,
|
||||
)
|
||||
31
game/settings/boundedfloatoption.py
Normal file
31
game/settings/boundedfloatoption.py
Normal file
@ -0,0 +1,31 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional
|
||||
|
||||
from .optiondescription import OptionDescription, SETTING_DESCRIPTION_KEY
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BoundedFloatOption(OptionDescription):
|
||||
min: float
|
||||
max: float
|
||||
divisor: int
|
||||
|
||||
|
||||
def bounded_float_option(
|
||||
text: str,
|
||||
page: str,
|
||||
section: str,
|
||||
min: float,
|
||||
max: float,
|
||||
divisor: int,
|
||||
detail: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> float:
|
||||
return field(
|
||||
metadata={
|
||||
SETTING_DESCRIPTION_KEY: BoundedFloatOption(
|
||||
page, section, text, detail, min, max, divisor
|
||||
)
|
||||
},
|
||||
**kwargs,
|
||||
)
|
||||
41
game/settings/choicesoption.py
Normal file
41
game/settings/choicesoption.py
Normal file
@ -0,0 +1,41 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Generic, Iterable, Mapping, Optional, TypeVar, Union
|
||||
|
||||
from .optiondescription import OptionDescription, SETTING_DESCRIPTION_KEY
|
||||
|
||||
ValueT = TypeVar("ValueT")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChoicesOption(OptionDescription, Generic[ValueT]):
|
||||
choices: dict[str, ValueT]
|
||||
|
||||
def text_for_value(self, value: ValueT) -> str:
|
||||
for text, _value in self.choices.items():
|
||||
if value == _value:
|
||||
return text
|
||||
raise ValueError(f"{self} does not contain {value}")
|
||||
|
||||
|
||||
def choices_option(
|
||||
text: str,
|
||||
page: str,
|
||||
section: str,
|
||||
choices: Union[Iterable[str], Mapping[str, ValueT]],
|
||||
detail: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> ValueT:
|
||||
if not isinstance(choices, Mapping):
|
||||
choices = {c: c for c in choices}
|
||||
return field(
|
||||
metadata={
|
||||
SETTING_DESCRIPTION_KEY: ChoicesOption(
|
||||
page,
|
||||
section,
|
||||
text,
|
||||
detail,
|
||||
dict(choices),
|
||||
)
|
||||
},
|
||||
**kwargs,
|
||||
)
|
||||
13
game/settings/optiondescription.py
Normal file
13
game/settings/optiondescription.py
Normal file
@ -0,0 +1,13 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
SETTING_DESCRIPTION_KEY = "DCS_LIBERATION_SETTING_DESCRIPTION_KEY"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OptionDescription:
|
||||
page: str
|
||||
section: str
|
||||
text: str
|
||||
detail: Optional[str]
|
||||
247
game/settings/settings.py
Normal file
247
game/settings/settings.py
Normal file
@ -0,0 +1,247 @@
|
||||
from collections import Iterator
|
||||
from dataclasses import Field, dataclass, field, fields
|
||||
from datetime import timedelta
|
||||
from enum import Enum, unique
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from dcs.forcedoptions import ForcedOptions
|
||||
|
||||
from .booleanoption import boolean_option
|
||||
from .boundedfloatoption import bounded_float_option
|
||||
from .choicesoption import choices_option
|
||||
from .optiondescription import OptionDescription, SETTING_DESCRIPTION_KEY
|
||||
from .skilloption import skill_option
|
||||
|
||||
|
||||
@unique
|
||||
class AutoAtoBehavior(Enum):
|
||||
Disabled = "Disabled"
|
||||
Never = "Never assign player pilots"
|
||||
Default = "No preference"
|
||||
Prefer = "Prefer player pilots"
|
||||
|
||||
|
||||
DIFFICULTY_PAGE = "Difficulty"
|
||||
|
||||
AI_DIFFICULTY_SECTION = "AI Difficulty"
|
||||
MISSION_DIFFICULTY_SECTION = "Mission Difficulty"
|
||||
MISSION_RESTRICTIONS_SECTION = "Mission Restrictions"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
version: Optional[str] = None
|
||||
|
||||
# Difficulty settings
|
||||
# AI Difficulty
|
||||
player_skill: str = skill_option(
|
||||
"Player coalition skill",
|
||||
page=DIFFICULTY_PAGE,
|
||||
section=AI_DIFFICULTY_SECTION,
|
||||
default="Good",
|
||||
)
|
||||
enemy_skill: str = skill_option(
|
||||
"Enemy coalition skill",
|
||||
page=DIFFICULTY_PAGE,
|
||||
section=AI_DIFFICULTY_SECTION,
|
||||
default="Average",
|
||||
)
|
||||
enemy_vehicle_skill: str = skill_option(
|
||||
"Enemy AA and vehicles skill",
|
||||
page=DIFFICULTY_PAGE,
|
||||
section=AI_DIFFICULTY_SECTION,
|
||||
default="Average",
|
||||
)
|
||||
player_income_multiplier: float = bounded_float_option(
|
||||
"Player income multiplier",
|
||||
page=DIFFICULTY_PAGE,
|
||||
section=AI_DIFFICULTY_SECTION,
|
||||
min=0,
|
||||
max=5,
|
||||
divisor=10,
|
||||
default=1.0,
|
||||
)
|
||||
enemy_income_multiplier: float = bounded_float_option(
|
||||
"Enemy income multiplier",
|
||||
page=DIFFICULTY_PAGE,
|
||||
section=AI_DIFFICULTY_SECTION,
|
||||
min=0,
|
||||
max=5,
|
||||
divisor=10,
|
||||
default=1.0,
|
||||
)
|
||||
invulnerable_player_pilots: bool = boolean_option(
|
||||
"Player pilots cannot be killed",
|
||||
page=DIFFICULTY_PAGE,
|
||||
section=AI_DIFFICULTY_SECTION,
|
||||
detail=(
|
||||
"Aircraft are vulnerable, but the player's pilot will be returned to the "
|
||||
"squadron at the end of the mission"
|
||||
),
|
||||
default=True,
|
||||
)
|
||||
# Mission Difficulty
|
||||
manpads: bool = boolean_option(
|
||||
"Manpads on frontlines",
|
||||
page=DIFFICULTY_PAGE,
|
||||
section=MISSION_DIFFICULTY_SECTION,
|
||||
default=True,
|
||||
)
|
||||
night_disabled: bool = boolean_option(
|
||||
"No night missions",
|
||||
page=DIFFICULTY_PAGE,
|
||||
section=MISSION_DIFFICULTY_SECTION,
|
||||
default=False,
|
||||
)
|
||||
# Mission Restrictions
|
||||
labels: str = choices_option(
|
||||
"In game labels",
|
||||
page=DIFFICULTY_PAGE,
|
||||
section=MISSION_RESTRICTIONS_SECTION,
|
||||
choices=["Full", "Abbreviated", "Dot Only", "Neutral Dot", "Off"],
|
||||
default="Full",
|
||||
)
|
||||
map_coalition_visibility: ForcedOptions.Views = choices_option(
|
||||
"Map visibility options",
|
||||
page=DIFFICULTY_PAGE,
|
||||
section=MISSION_RESTRICTIONS_SECTION,
|
||||
choices={
|
||||
"All": ForcedOptions.Views.All,
|
||||
"Fog of war": ForcedOptions.Views.Allies,
|
||||
"Allies only": ForcedOptions.Views.OnlyAllies,
|
||||
"Own aircraft only": ForcedOptions.Views.MyAircraft,
|
||||
"Map only": ForcedOptions.Views.OnlyMap,
|
||||
},
|
||||
default=ForcedOptions.Views.All,
|
||||
)
|
||||
external_views_allowed: bool = boolean_option(
|
||||
"Allow external views",
|
||||
DIFFICULTY_PAGE,
|
||||
MISSION_RESTRICTIONS_SECTION,
|
||||
default=True,
|
||||
)
|
||||
battle_damage_assessment: Optional[bool] = choices_option(
|
||||
"Battle damage assessment",
|
||||
page=DIFFICULTY_PAGE,
|
||||
section=MISSION_RESTRICTIONS_SECTION,
|
||||
choices={"Player preference": None, "Enforced on": True, "Enforced off": False},
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Campaign management
|
||||
# General
|
||||
restrict_weapons_by_date: bool = False
|
||||
disable_legacy_aewc: bool = True
|
||||
disable_legacy_tanker: bool = True
|
||||
# Pilots and Squadrons
|
||||
ai_pilot_levelling: bool = True
|
||||
#: Feature flag for squadron limits.
|
||||
enable_squadron_pilot_limits: bool = False
|
||||
#: The maximum number of pilots a squadron can have at one time. Changing this after
|
||||
#: the campaign has started will have no immediate effect; pilots already in the
|
||||
#: squadron will not be removed if the limit is lowered and pilots will not be
|
||||
#: immediately created if the limit is raised.
|
||||
squadron_pilot_limit: int = 12
|
||||
#: The number of pilots a squadron can replace per turn.
|
||||
squadron_replenishment_rate: int = 4
|
||||
# HQ Automation
|
||||
automate_runway_repair: bool = False
|
||||
automate_front_line_reinforcements: bool = False
|
||||
automate_aircraft_reinforcements: bool = False
|
||||
auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default
|
||||
auto_ato_player_missions_asap: bool = True
|
||||
automate_front_line_stance: bool = True
|
||||
reserves_procurement_target: int = 10
|
||||
|
||||
# Mission Generator
|
||||
# Gameplay
|
||||
supercarrier: bool = False
|
||||
generate_marks: bool = True
|
||||
generate_dark_kneeboard: bool = False
|
||||
never_delay_player_flights: bool = False
|
||||
default_start_type: str = "Cold"
|
||||
# Mission specific
|
||||
desired_player_mission_duration: timedelta = timedelta(minutes=60)
|
||||
# Performance
|
||||
perf_smoke_gen: bool = True
|
||||
perf_smoke_spacing = 1600
|
||||
perf_red_alert_state: bool = True
|
||||
perf_artillery: bool = True
|
||||
perf_moving_units: bool = True
|
||||
perf_infantry: bool = True
|
||||
perf_destroyed_units: bool = True
|
||||
# Performance culling
|
||||
perf_culling: bool = False
|
||||
perf_culling_distance: int = 100
|
||||
perf_do_not_cull_carrier = True
|
||||
|
||||
# Cheating
|
||||
show_red_ato: bool = False
|
||||
enable_frontline_cheats: bool = False
|
||||
enable_base_capture_cheat: bool = False
|
||||
|
||||
# LUA Plugins system
|
||||
plugins: Dict[str, bool] = field(default_factory=dict)
|
||||
|
||||
only_player_takeoff: bool = True # Legacy parameter do not use
|
||||
|
||||
@staticmethod
|
||||
def plugin_settings_key(identifier: str) -> str:
|
||||
return f"plugins.{identifier}"
|
||||
|
||||
def initialize_plugin_option(self, identifier: str, default_value: bool) -> None:
|
||||
try:
|
||||
self.plugin_option(identifier)
|
||||
except KeyError:
|
||||
self.set_plugin_option(identifier, default_value)
|
||||
|
||||
def plugin_option(self, identifier: str) -> bool:
|
||||
return self.plugins[self.plugin_settings_key(identifier)]
|
||||
|
||||
def set_plugin_option(self, identifier: str, enabled: bool) -> None:
|
||||
self.plugins[self.plugin_settings_key(identifier)] = enabled
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
# __setstate__ is called with the dict of the object being unpickled. We
|
||||
# can provide save compatibility for new settings options (which
|
||||
# normally would not be present in the unpickled object) by creating a
|
||||
# new settings object, updating it with the unpickled state, and
|
||||
# updating our dict with that.
|
||||
new_state = Settings().__dict__
|
||||
new_state.update(state)
|
||||
self.__dict__.update(new_state)
|
||||
|
||||
@classmethod
|
||||
def _field_description(cls, settings_field: Field[Any]) -> OptionDescription:
|
||||
return settings_field.metadata[SETTING_DESCRIPTION_KEY]
|
||||
|
||||
@classmethod
|
||||
def pages(cls) -> Iterator[str]:
|
||||
seen: set[str] = set()
|
||||
for settings_field in cls._user_fields():
|
||||
description = cls._field_description(settings_field)
|
||||
if description.page not in seen:
|
||||
yield description.page
|
||||
seen.add(description.page)
|
||||
|
||||
@classmethod
|
||||
def sections(cls, page: str) -> Iterator[str]:
|
||||
seen: set[str] = set()
|
||||
for settings_field in cls._user_fields():
|
||||
description = cls._field_description(settings_field)
|
||||
if description.page == page and description.section not in seen:
|
||||
yield description.section
|
||||
seen.add(description.section)
|
||||
|
||||
@classmethod
|
||||
def fields(cls, page: str, section: str) -> Iterator[tuple[str, OptionDescription]]:
|
||||
for settings_field in cls._user_fields():
|
||||
description = cls._field_description(settings_field)
|
||||
if description.page == page and description.section == section:
|
||||
yield settings_field.name, description
|
||||
|
||||
@classmethod
|
||||
def _user_fields(cls) -> Iterator[Field[Any]]:
|
||||
for settings_field in fields(cls):
|
||||
if SETTING_DESCRIPTION_KEY in settings_field.metadata:
|
||||
yield settings_field
|
||||
20
game/settings/skilloption.py
Normal file
20
game/settings/skilloption.py
Normal file
@ -0,0 +1,20 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
from .choicesoption import choices_option
|
||||
|
||||
|
||||
def skill_option(
|
||||
text: str,
|
||||
page: str,
|
||||
section: str,
|
||||
detail: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
return choices_option(
|
||||
text,
|
||||
page,
|
||||
section,
|
||||
["Average", "Good", "High", "Excellent"],
|
||||
detail=detail,
|
||||
**kwargs,
|
||||
)
|
||||
@ -100,6 +100,7 @@ def load_icons():
|
||||
ICONS["Missile"] = QPixmap(
|
||||
"./resources/ui/misc/" + get_theme_icons() + "/missile.png"
|
||||
)
|
||||
ICONS["Difficulty"] = ICONS["Missile"]
|
||||
ICONS["Cheat"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/cheat.png")
|
||||
ICONS["Plugins"] = QPixmap(
|
||||
"./resources/ui/misc/" + get_theme_icons() + "/plugins.png"
|
||||
|
||||
@ -3,21 +3,27 @@ from typing import Optional
|
||||
from PySide2.QtWidgets import QSpinBox
|
||||
|
||||
|
||||
class TenthsSpinner(QSpinBox):
|
||||
class FloatSpinner(QSpinBox):
|
||||
def __init__(
|
||||
self,
|
||||
minimum: Optional[int] = None,
|
||||
maximum: Optional[int] = None,
|
||||
initial: Optional[int] = None,
|
||||
divisor: int,
|
||||
minimum: Optional[float] = None,
|
||||
maximum: Optional[float] = None,
|
||||
initial: Optional[float] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.divisor = divisor
|
||||
|
||||
if minimum is not None:
|
||||
self.setMinimum(minimum)
|
||||
self.setMinimum(int(minimum * divisor))
|
||||
if maximum is not None:
|
||||
self.setMaximum(maximum)
|
||||
self.setMaximum(int(maximum * divisor))
|
||||
if initial is not None:
|
||||
self.setValue(initial)
|
||||
self.setValue(int(initial * divisor))
|
||||
|
||||
def textFromValue(self, val: int) -> str:
|
||||
return f"X {val / 10:.1f}"
|
||||
return f"X {val / self.divisor:.1f}"
|
||||
|
||||
@property
|
||||
def real_value(self) -> float:
|
||||
return self.value() / self.divisor
|
||||
|
||||
@ -1,30 +1,33 @@
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
|
||||
from PySide2 import QtWidgets
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import QGridLayout, QLabel, QSlider
|
||||
from typing import Optional
|
||||
from qt_ui.widgets.floatspinners import TenthsSpinner
|
||||
from datetime import timedelta
|
||||
from PySide2.QtWidgets import QSlider, QHBoxLayout
|
||||
|
||||
from qt_ui.widgets.floatspinners import FloatSpinner
|
||||
|
||||
|
||||
class TenthsSpinSlider(QGridLayout):
|
||||
def __init__(self, label: str, minimum: int, maximum: int, initial: int) -> None:
|
||||
class FloatSpinSlider(QHBoxLayout):
|
||||
def __init__(
|
||||
self, minimum: float, maximum: float, initial: float, divisor: int
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.addWidget(QLabel(label), 0, 0)
|
||||
|
||||
slider = QSlider(Qt.Horizontal)
|
||||
slider.setMinimum(minimum)
|
||||
slider.setMaximum(maximum)
|
||||
slider.setMinimum(int(minimum * divisor))
|
||||
slider.setMaximum(int(maximum * divisor))
|
||||
slider.setValue(initial)
|
||||
self.spinner = TenthsSpinner(minimum, maximum, initial)
|
||||
self.spinner = FloatSpinner(divisor, minimum, maximum, initial)
|
||||
slider.valueChanged.connect(lambda x: self.spinner.setValue(x))
|
||||
self.spinner.valueChanged.connect(lambda x: slider.setValue(x))
|
||||
|
||||
self.addWidget(slider, 1, 0)
|
||||
self.addWidget(self.spinner, 1, 1)
|
||||
self.addWidget(slider)
|
||||
self.addWidget(self.spinner)
|
||||
|
||||
@property
|
||||
def value(self) -> float:
|
||||
return self.spinner.value() / 10
|
||||
return self.spinner.real_value
|
||||
|
||||
|
||||
class TimeInputs(QtWidgets.QGridLayout):
|
||||
|
||||
@ -15,7 +15,7 @@ from game.settings import Settings
|
||||
from game.theater.start_generator import GameGenerator, GeneratorSettings, ModSettings
|
||||
from game.factions.faction import Faction
|
||||
from qt_ui.widgets.QLiberationCalendar import QLiberationCalendar
|
||||
from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs, CurrencySpinner
|
||||
from qt_ui.widgets.spinsliders import FloatSpinSlider, TimeInputs, CurrencySpinner
|
||||
from qt_ui.windows.AirWingConfigurationDialog import AirWingConfigurationDialog
|
||||
from qt_ui.windows.newgame.QCampaignList import QCampaignList
|
||||
|
||||
@ -472,11 +472,12 @@ class DifficultyAndAutomationOptions(QtWidgets.QWizardPage):
|
||||
economy_layout = QtWidgets.QVBoxLayout()
|
||||
economy_group.setLayout(economy_layout)
|
||||
|
||||
player_income = TenthsSpinSlider("Player income multiplier", 0, 50, 10)
|
||||
# TODO: Put labels back.
|
||||
player_income = FloatSpinSlider(0, 5, 1, divisor=10)
|
||||
self.registerField("player_income_multiplier", player_income.spinner)
|
||||
economy_layout.addLayout(player_income)
|
||||
|
||||
enemy_income = TenthsSpinSlider("Enemy income multiplier", 0, 50, 10)
|
||||
enemy_income = FloatSpinSlider(0, 5, 1, divisor=10)
|
||||
self.registerField("enemy_income_multiplier", enemy_income.spinner)
|
||||
economy_layout.addLayout(enemy_income)
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import textwrap
|
||||
from typing import Callable
|
||||
|
||||
from PySide2.QtCore import QItemSelectionModel, QPoint, QSize, Qt
|
||||
@ -18,13 +19,19 @@ from PySide2.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from dcs.forcedoptions import ForcedOptions
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game.game import Game
|
||||
from game.settings import Settings, AutoAtoBehavior
|
||||
from game.settings import (
|
||||
Settings,
|
||||
AutoAtoBehavior,
|
||||
OptionDescription,
|
||||
BooleanOption,
|
||||
ChoicesOption,
|
||||
BoundedFloatOption,
|
||||
)
|
||||
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
||||
from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs
|
||||
from qt_ui.widgets.spinsliders import FloatSpinSlider, TimeInputs
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.finances.QFinancesMenu import QHorizontalSeparationLine
|
||||
from qt_ui.windows.settings.plugins import PluginOptionsPage, PluginsPage
|
||||
@ -321,6 +328,86 @@ class StartTypeComboBox(QComboBox):
|
||||
self.settings.default_start_type = value
|
||||
|
||||
|
||||
class AutoSettingsLayout(QGridLayout):
|
||||
def __init__(self, page: str, section: str, settings: Settings) -> None:
|
||||
super().__init__()
|
||||
self.settings = settings
|
||||
|
||||
for row, (name, description) in enumerate(Settings.fields(page, section)):
|
||||
self.add_label(row, description)
|
||||
if isinstance(description, BooleanOption):
|
||||
self.add_checkbox_for(row, name)
|
||||
elif isinstance(description, ChoicesOption):
|
||||
self.add_combobox_for(row, name, description)
|
||||
elif isinstance(description, BoundedFloatOption):
|
||||
self.add_float_spin_slider_for(row, name, description)
|
||||
else:
|
||||
raise TypeError(f"Unhandled option type: {description}")
|
||||
|
||||
def add_label(self, row: int, description: OptionDescription) -> None:
|
||||
text = description.text
|
||||
if description.detail is not None:
|
||||
wrapped = "<br />".join(textwrap.wrap(description.detail, width=55))
|
||||
text += f"<br /><strong>{wrapped}</strong>"
|
||||
label = QLabel(text)
|
||||
self.addWidget(label, row, 0)
|
||||
|
||||
def add_checkbox_for(self, row: int, name: str) -> None:
|
||||
def on_toggle(value: bool) -> None:
|
||||
self.settings.__dict__[name] = value
|
||||
|
||||
checkbox = QCheckBox()
|
||||
checkbox.setChecked(self.settings.__dict__[name])
|
||||
checkbox.toggled.connect(on_toggle)
|
||||
self.addWidget(checkbox, row, 1, Qt.AlignRight)
|
||||
|
||||
def add_combobox_for(self, row: int, name: str, description: ChoicesOption) -> None:
|
||||
combobox = QComboBox()
|
||||
|
||||
def on_changed(index: int) -> None:
|
||||
self.settings.__dict__[name] = combobox.itemData(index)
|
||||
|
||||
for text, value in description.choices.items():
|
||||
combobox.addItem(text, value)
|
||||
combobox.setCurrentText(
|
||||
description.text_for_value(self.settings.__dict__[name])
|
||||
)
|
||||
combobox.currentIndexChanged.connect(on_changed)
|
||||
self.addWidget(combobox, row, 1, Qt.AlignRight)
|
||||
|
||||
def add_float_spin_slider_for(
|
||||
self, row: int, name: str, description: BoundedFloatOption
|
||||
) -> None:
|
||||
spinner = FloatSpinSlider(
|
||||
description.min,
|
||||
description.max,
|
||||
self.settings.__dict__[name],
|
||||
divisor=description.divisor,
|
||||
)
|
||||
self.addLayout(spinner, row, 1, Qt.AlignRight)
|
||||
|
||||
|
||||
class AutoSettingsGroup(QGroupBox):
|
||||
def __init__(self, page: str, section: str, settings: Settings) -> None:
|
||||
super().__init__(section)
|
||||
self.setLayout(AutoSettingsLayout(page, section, settings))
|
||||
|
||||
|
||||
class AutoSettingsPageLayout(QVBoxLayout):
|
||||
def __init__(self, page: str, settings: Settings) -> None:
|
||||
super().__init__()
|
||||
self.setAlignment(Qt.AlignTop)
|
||||
|
||||
for section in Settings.sections(page):
|
||||
self.addWidget(AutoSettingsGroup(page, section, settings))
|
||||
|
||||
|
||||
class AutoSettingsPage(QWidget):
|
||||
def __init__(self, page: str, settings: Settings) -> None:
|
||||
super().__init__()
|
||||
self.setLayout(AutoSettingsPageLayout(page, settings))
|
||||
|
||||
|
||||
class QSettingsWindow(QDialog):
|
||||
def __init__(self, game: Game):
|
||||
super().__init__()
|
||||
@ -330,6 +417,10 @@ class QSettingsWindow(QDialog):
|
||||
self.pluginsOptionsPage = None
|
||||
self.campaign_management_page = QWidget()
|
||||
|
||||
self.pages: dict[str, AutoSettingsPage] = {}
|
||||
for page in Settings.pages():
|
||||
self.pages[page] = AutoSettingsPage(page, game.settings)
|
||||
|
||||
self.setModal(True)
|
||||
self.setWindowTitle("Settings")
|
||||
self.setWindowIcon(CONST.ICONS["Settings"])
|
||||
@ -349,13 +440,16 @@ class QSettingsWindow(QDialog):
|
||||
|
||||
self.categoryList.setIconSize(QSize(32, 32))
|
||||
|
||||
self.initDifficultyLayout()
|
||||
difficulty = QStandardItem("Difficulty")
|
||||
difficulty.setIcon(CONST.ICONS["Missile"])
|
||||
difficulty.setEditable(False)
|
||||
difficulty.setSelectable(True)
|
||||
self.categoryModel.appendRow(difficulty)
|
||||
self.right_layout.addWidget(self.difficultyPage)
|
||||
for name, page in self.pages.items():
|
||||
page_item = QStandardItem(name)
|
||||
if name in CONST.ICONS:
|
||||
page_item.setIcon(CONST.ICONS[name])
|
||||
else:
|
||||
page_item.setIcon(CONST.ICONS["Generator"])
|
||||
page_item.setEditable(False)
|
||||
page_item.setSelectable(True)
|
||||
self.categoryModel.appendRow(page_item)
|
||||
self.right_layout.addWidget(page)
|
||||
|
||||
self.init_campaign_management_layout()
|
||||
campaign_management = QStandardItem("Campaign Management")
|
||||
@ -414,182 +508,6 @@ class QSettingsWindow(QDialog):
|
||||
def init(self):
|
||||
pass
|
||||
|
||||
def initDifficultyLayout(self):
|
||||
|
||||
self.difficultyPage = QWidget()
|
||||
self.difficultyLayout = QVBoxLayout()
|
||||
self.difficultyLayout.setAlignment(Qt.AlignTop)
|
||||
self.difficultyPage.setLayout(self.difficultyLayout)
|
||||
|
||||
# DCS AI difficulty settings
|
||||
self.aiDifficultySettings = QGroupBox("AI Difficulty")
|
||||
self.aiDifficultyLayout = QGridLayout()
|
||||
self.playerCoalitionSkill = QComboBox()
|
||||
self.enemyCoalitionSkill = QComboBox()
|
||||
self.enemyAASkill = QComboBox()
|
||||
for skill in CONST.SKILL_OPTIONS:
|
||||
self.playerCoalitionSkill.addItem(skill)
|
||||
self.enemyCoalitionSkill.addItem(skill)
|
||||
self.enemyAASkill.addItem(skill)
|
||||
|
||||
self.playerCoalitionSkill.setCurrentIndex(
|
||||
CONST.SKILL_OPTIONS.index(self.game.settings.player_skill)
|
||||
)
|
||||
self.enemyCoalitionSkill.setCurrentIndex(
|
||||
CONST.SKILL_OPTIONS.index(self.game.settings.enemy_skill)
|
||||
)
|
||||
self.enemyAASkill.setCurrentIndex(
|
||||
CONST.SKILL_OPTIONS.index(self.game.settings.enemy_vehicle_skill)
|
||||
)
|
||||
|
||||
self.player_income = TenthsSpinSlider(
|
||||
"Player income multiplier",
|
||||
0,
|
||||
50,
|
||||
int(self.game.settings.player_income_multiplier * 10),
|
||||
)
|
||||
self.player_income.spinner.valueChanged.connect(self.applySettings)
|
||||
self.enemy_income = TenthsSpinSlider(
|
||||
"Enemy income multiplier",
|
||||
0,
|
||||
50,
|
||||
int(self.game.settings.enemy_income_multiplier * 10),
|
||||
)
|
||||
self.enemy_income.spinner.valueChanged.connect(self.applySettings)
|
||||
|
||||
self.playerCoalitionSkill.currentIndexChanged.connect(self.applySettings)
|
||||
self.enemyCoalitionSkill.currentIndexChanged.connect(self.applySettings)
|
||||
self.enemyAASkill.currentIndexChanged.connect(self.applySettings)
|
||||
|
||||
# Mission generation settings related to difficulty
|
||||
self.missionSettings = QGroupBox("Mission Difficulty")
|
||||
self.missionLayout = QGridLayout()
|
||||
|
||||
self.manpads = QCheckBox()
|
||||
self.manpads.setChecked(self.game.settings.manpads)
|
||||
self.manpads.toggled.connect(self.applySettings)
|
||||
|
||||
self.noNightMission = QCheckBox()
|
||||
self.noNightMission.setChecked(self.game.settings.night_disabled)
|
||||
self.noNightMission.toggled.connect(self.applySettings)
|
||||
|
||||
# DCS Mission options
|
||||
self.missionRestrictionsSettings = QGroupBox("Mission Restrictions")
|
||||
self.missionRestrictionsLayout = QGridLayout()
|
||||
|
||||
self.difficultyLabel = QComboBox()
|
||||
[self.difficultyLabel.addItem(t) for t in CONST.LABELS_OPTIONS]
|
||||
self.difficultyLabel.setCurrentIndex(
|
||||
CONST.LABELS_OPTIONS.index(self.game.settings.labels)
|
||||
)
|
||||
self.difficultyLabel.currentIndexChanged.connect(self.applySettings)
|
||||
|
||||
self.mapVisibiitySelection = QComboBox()
|
||||
self.mapVisibiitySelection.addItem("All", ForcedOptions.Views.All)
|
||||
if self.game.settings.map_coalition_visibility == ForcedOptions.Views.All:
|
||||
self.mapVisibiitySelection.setCurrentIndex(0)
|
||||
self.mapVisibiitySelection.addItem("Fog of War", ForcedOptions.Views.Allies)
|
||||
if self.game.settings.map_coalition_visibility == ForcedOptions.Views.Allies:
|
||||
self.mapVisibiitySelection.setCurrentIndex(1)
|
||||
self.mapVisibiitySelection.addItem(
|
||||
"Allies Only", ForcedOptions.Views.OnlyAllies
|
||||
)
|
||||
if (
|
||||
self.game.settings.map_coalition_visibility
|
||||
== ForcedOptions.Views.OnlyAllies
|
||||
):
|
||||
self.mapVisibiitySelection.setCurrentIndex(2)
|
||||
self.mapVisibiitySelection.addItem(
|
||||
"Own Aircraft Only", ForcedOptions.Views.MyAircraft
|
||||
)
|
||||
if (
|
||||
self.game.settings.map_coalition_visibility
|
||||
== ForcedOptions.Views.MyAircraft
|
||||
):
|
||||
self.mapVisibiitySelection.setCurrentIndex(3)
|
||||
self.mapVisibiitySelection.addItem("Map Only", ForcedOptions.Views.OnlyMap)
|
||||
if self.game.settings.map_coalition_visibility == ForcedOptions.Views.OnlyMap:
|
||||
self.mapVisibiitySelection.setCurrentIndex(4)
|
||||
self.mapVisibiitySelection.currentIndexChanged.connect(self.applySettings)
|
||||
|
||||
self.ext_views = QCheckBox()
|
||||
self.ext_views.setChecked(self.game.settings.external_views_allowed)
|
||||
self.ext_views.toggled.connect(self.applySettings)
|
||||
|
||||
self.battleDamageAssessment = QComboBox()
|
||||
self.battleDamageAssessment.addItem("Player preference", None)
|
||||
self.battleDamageAssessment.addItem("Enforced on", True)
|
||||
self.battleDamageAssessment.addItem("Enforced off", False)
|
||||
if self.game.settings.battle_damage_assessment is None:
|
||||
self.battleDamageAssessment.setCurrentIndex(0)
|
||||
elif self.game.settings.battle_damage_assessment is True:
|
||||
self.battleDamageAssessment.setCurrentIndex(1)
|
||||
else:
|
||||
self.battleDamageAssessment.setCurrentIndex(2)
|
||||
self.battleDamageAssessment.currentIndexChanged.connect(self.applySettings)
|
||||
|
||||
def set_invulnerable_player_pilots(checked: bool) -> None:
|
||||
self.game.settings.invulnerable_player_pilots = checked
|
||||
|
||||
invulnerable_player_pilots_label = QLabel(
|
||||
"Player pilots cannot be killed<br />"
|
||||
"<strong>Aircraft are vulnerable, but the player's pilot will be<br />"
|
||||
"returned to the squadron at the end of the mission</strong>"
|
||||
)
|
||||
|
||||
invulnerable_player_pilots_checkbox = QCheckBox()
|
||||
invulnerable_player_pilots_checkbox.setChecked(
|
||||
self.game.settings.invulnerable_player_pilots
|
||||
)
|
||||
invulnerable_player_pilots_checkbox.toggled.connect(
|
||||
set_invulnerable_player_pilots
|
||||
)
|
||||
|
||||
self.aiDifficultyLayout.addWidget(QLabel("Player coalition skill"), 0, 0)
|
||||
self.aiDifficultyLayout.addWidget(
|
||||
self.playerCoalitionSkill, 0, 1, Qt.AlignRight
|
||||
)
|
||||
self.aiDifficultyLayout.addWidget(QLabel("Enemy coalition skill"), 1, 0)
|
||||
self.aiDifficultyLayout.addWidget(self.enemyCoalitionSkill, 1, 1, Qt.AlignRight)
|
||||
self.aiDifficultyLayout.addWidget(QLabel("Enemy AA and vehicles skill"), 2, 0)
|
||||
self.aiDifficultyLayout.addWidget(self.enemyAASkill, 2, 1, Qt.AlignRight)
|
||||
self.aiDifficultyLayout.addLayout(self.player_income, 3, 0)
|
||||
self.aiDifficultyLayout.addLayout(self.enemy_income, 4, 0)
|
||||
self.aiDifficultyLayout.addWidget(invulnerable_player_pilots_label, 5, 0)
|
||||
self.aiDifficultyLayout.addWidget(
|
||||
invulnerable_player_pilots_checkbox, 5, 1, Qt.AlignRight
|
||||
)
|
||||
self.aiDifficultySettings.setLayout(self.aiDifficultyLayout)
|
||||
self.difficultyLayout.addWidget(self.aiDifficultySettings)
|
||||
|
||||
self.missionLayout.addWidget(QLabel("Manpads on frontlines"), 0, 0)
|
||||
self.missionLayout.addWidget(self.manpads, 0, 1, Qt.AlignRight)
|
||||
self.missionLayout.addWidget(QLabel("No night missions"), 1, 0)
|
||||
self.missionLayout.addWidget(self.noNightMission, 1, 1, Qt.AlignRight)
|
||||
self.missionSettings.setLayout(self.missionLayout)
|
||||
self.difficultyLayout.addWidget(self.missionSettings)
|
||||
|
||||
self.missionRestrictionsLayout.addWidget(QLabel("In Game Labels"), 0, 0)
|
||||
self.missionRestrictionsLayout.addWidget(
|
||||
self.difficultyLabel, 0, 1, Qt.AlignRight
|
||||
)
|
||||
self.missionRestrictionsLayout.addWidget(QLabel("Map visibility options"), 1, 0)
|
||||
self.missionRestrictionsLayout.addWidget(
|
||||
self.mapVisibiitySelection, 1, 1, Qt.AlignRight
|
||||
)
|
||||
self.missionRestrictionsLayout.addWidget(QLabel("Allow external views"), 2, 0)
|
||||
self.missionRestrictionsLayout.addWidget(self.ext_views, 2, 1, Qt.AlignRight)
|
||||
|
||||
self.missionRestrictionsLayout.addWidget(
|
||||
QLabel("Battle damage assessment"), 3, 0
|
||||
)
|
||||
self.missionRestrictionsLayout.addWidget(
|
||||
self.battleDamageAssessment, 3, 1, Qt.AlignRight
|
||||
)
|
||||
|
||||
self.missionRestrictionsSettings.setLayout(self.missionRestrictionsLayout)
|
||||
self.difficultyLayout.addWidget(self.missionRestrictionsSettings)
|
||||
|
||||
def init_campaign_management_layout(self) -> None:
|
||||
campaign_layout = QVBoxLayout()
|
||||
campaign_layout.setAlignment(Qt.AlignTop)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user