|
|
|
|
@@ -6,13 +6,21 @@ from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Any
|
|
|
|
|
from typing import Any, Union
|
|
|
|
|
|
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
|
|
|
|
MissionParamValue = Union[int, float, str, bool]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── Pydantic Models (config schema) ─────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
class MissionRotationItem(BaseModel):
|
|
|
|
|
name: str
|
|
|
|
|
difficulty: str = ""
|
|
|
|
|
params: dict[str, MissionParamValue] = Field(default_factory=dict)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ServerConfig(BaseModel):
|
|
|
|
|
hostname: str = "My Arma 3 Server"
|
|
|
|
|
password: str | None = None
|
|
|
|
|
@@ -57,17 +65,18 @@ class ServerConfig(BaseModel):
|
|
|
|
|
headless_clients: list[str] = Field(default_factory=list)
|
|
|
|
|
local_clients: list[str] = Field(default_factory=list)
|
|
|
|
|
admin_uids: list[str] = Field(default_factory=list)
|
|
|
|
|
missions: list[dict] = Field(default_factory=list)
|
|
|
|
|
missions: list[MissionRotationItem] = Field(default_factory=list)
|
|
|
|
|
default_mission_params: dict[str, MissionParamValue] = Field(default_factory=dict)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BasicConfig(BaseModel):
|
|
|
|
|
min_bandwidth: int = Field(default=800000, gt=0)
|
|
|
|
|
max_bandwidth: int = Field(default=25000000, gt=0)
|
|
|
|
|
max_msg_send: int = Field(default=384, gt=0)
|
|
|
|
|
min_bandwidth: int = Field(default=131072, gt=0)
|
|
|
|
|
max_bandwidth: int = Field(default=10000000000, gt=0)
|
|
|
|
|
max_msg_send: int = Field(default=128, gt=0)
|
|
|
|
|
max_size_guaranteed: int = Field(default=512, gt=0)
|
|
|
|
|
max_size_non_guaranteed: int = Field(default=256, gt=0)
|
|
|
|
|
min_error_to_send: float = Field(default=0.003, gt=0)
|
|
|
|
|
max_custom_file_size: int = Field(default=100000, ge=0)
|
|
|
|
|
min_error_to_send: float = Field(default=0.001, gt=0)
|
|
|
|
|
max_custom_file_size: int = Field(default=0, ge=0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ProfileConfig(BaseModel):
|
|
|
|
|
@@ -77,16 +86,16 @@ class ProfileConfig(BaseModel):
|
|
|
|
|
enemy_tags: int = Field(default=0, ge=0, le=3)
|
|
|
|
|
detected_mines: int = Field(default=0, ge=0, le=3)
|
|
|
|
|
commands: int = Field(default=1, ge=0, le=3)
|
|
|
|
|
waypoints: int = Field(default=1, ge=0, le=3)
|
|
|
|
|
waypoints: int = Field(default=0, ge=0, le=3)
|
|
|
|
|
tactical_ping: int = Field(default=0, ge=0, le=1)
|
|
|
|
|
weapon_info: int = Field(default=2, ge=0, le=3)
|
|
|
|
|
stance_indicator: int = Field(default=2, ge=0, le=3)
|
|
|
|
|
stamina_bar: int = Field(default=0, ge=0, le=1)
|
|
|
|
|
stamina_bar: int = Field(default=2, ge=0, le=2)
|
|
|
|
|
weapon_crosshair: int = Field(default=0, ge=0, le=1)
|
|
|
|
|
vision_aid: int = Field(default=0, ge=0, le=1)
|
|
|
|
|
third_person_view: int = Field(default=0, ge=0, le=1)
|
|
|
|
|
camera_shake: int = Field(default=1, ge=0, le=1)
|
|
|
|
|
score_table: int = Field(default=1, ge=0, le=1)
|
|
|
|
|
score_table: int = Field(default=0, ge=0, le=1)
|
|
|
|
|
death_messages: int = Field(default=1, ge=0, le=1)
|
|
|
|
|
von_id: int = Field(default=1, ge=0, le=1)
|
|
|
|
|
map_content_friendly: int = Field(default=0, ge=0, le=3)
|
|
|
|
|
@@ -95,8 +104,8 @@ class ProfileConfig(BaseModel):
|
|
|
|
|
auto_report: int = Field(default=0, ge=0, le=1)
|
|
|
|
|
multiple_saves: int = Field(default=0, ge=0, le=1)
|
|
|
|
|
ai_level_preset: int = Field(default=3, ge=0, le=4)
|
|
|
|
|
skill_ai: float = Field(default=0.5, ge=0.0, le=1.0)
|
|
|
|
|
precision_ai: float = Field(default=0.5, ge=0.0, le=1.0)
|
|
|
|
|
skill_ai: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
|
|
|
precision_ai: float = Field(default=0.2, ge=0.0, le=1.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LaunchConfig(BaseModel):
|
|
|
|
|
@@ -151,20 +160,64 @@ class Arma3ConfigGenerator:
|
|
|
|
|
return self.SENSITIVE_FIELDS.get(section, [])
|
|
|
|
|
|
|
|
|
|
def get_config_version(self) -> str:
|
|
|
|
|
return "1.0.0"
|
|
|
|
|
return "1.1.0"
|
|
|
|
|
|
|
|
|
|
def migrate_config(self, old_version: str, config_json: dict) -> dict:
|
|
|
|
|
"""
|
|
|
|
|
For version 1.0.0 there is nothing to migrate.
|
|
|
|
|
Future versions: add migration logic here.
|
|
|
|
|
"""
|
|
|
|
|
from adapters.exceptions import ConfigMigrationError
|
|
|
|
|
if old_version == "1.0.0":
|
|
|
|
|
server = config_json.get("server", {})
|
|
|
|
|
for m in server.get("missions", []):
|
|
|
|
|
if isinstance(m, dict):
|
|
|
|
|
m.setdefault("params", {})
|
|
|
|
|
server.setdefault("default_mission_params", {})
|
|
|
|
|
return config_json
|
|
|
|
|
raise ConfigMigrationError(
|
|
|
|
|
old_version, f"No migration path from {old_version} to {self.get_config_version()}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def normalize_section(self, section: str, data: dict) -> dict:
|
|
|
|
|
"""Backfill new optional fields on server section for pre-1.1.0 stored data."""
|
|
|
|
|
if section == "server":
|
|
|
|
|
for m in data.get("missions", []):
|
|
|
|
|
if isinstance(m, dict):
|
|
|
|
|
m.setdefault("params", {})
|
|
|
|
|
data.setdefault("default_mission_params", {})
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
# ── Config file writers ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def _render_param_value(self, val: MissionParamValue) -> str:
|
|
|
|
|
if isinstance(val, bool):
|
|
|
|
|
return "1" if val else "0"
|
|
|
|
|
if isinstance(val, (int, float)):
|
|
|
|
|
return str(val)
|
|
|
|
|
return f'"{self._escape(str(val))}"'
|
|
|
|
|
|
|
|
|
|
def _render_missions_block(self, cfg: ServerConfig) -> str:
|
|
|
|
|
"""Render the class Missions { ... } block for server.cfg.
|
|
|
|
|
|
|
|
|
|
Per-mission params take priority; falls back to default_mission_params;
|
|
|
|
|
if both are empty the class Params block is omitted entirely.
|
|
|
|
|
"""
|
|
|
|
|
if not cfg.missions:
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
lines = ["class Missions {"]
|
|
|
|
|
for idx, entry in enumerate(cfg.missions):
|
|
|
|
|
effective = entry.params if entry.params else cfg.default_mission_params
|
|
|
|
|
lines.append(f" class Mission_{idx} {{")
|
|
|
|
|
lines.append(f' template = "{self._escape(entry.name)}";')
|
|
|
|
|
if entry.difficulty:
|
|
|
|
|
lines.append(f' difficulty = "{self._escape(entry.difficulty)}";')
|
|
|
|
|
if effective:
|
|
|
|
|
lines.append(" class Params {")
|
|
|
|
|
for key, val in effective.items():
|
|
|
|
|
lines.append(f" {key} = {self._render_param_value(val)};")
|
|
|
|
|
lines.append(" };")
|
|
|
|
|
lines.append(" };")
|
|
|
|
|
lines.append("};")
|
|
|
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _escape(value: str) -> str:
|
|
|
|
|
"""
|
|
|
|
|
@@ -255,7 +308,7 @@ class Arma3ConfigGenerator:
|
|
|
|
|
if cfg.admin_uids:
|
|
|
|
|
lines.append(f"admins[] = {{{admin_uids}}};")
|
|
|
|
|
|
|
|
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
return "\n".join(lines) + "\n" + self._render_missions_block(cfg)
|
|
|
|
|
|
|
|
|
|
def _render_basic_cfg(self, cfg: BasicConfig) -> str:
|
|
|
|
|
return (
|
|
|
|
|
@@ -449,6 +502,9 @@ class Arma3ConfigGenerator:
|
|
|
|
|
"placeholder": "127.0.0.1"},
|
|
|
|
|
# missions managed by the Missions tab — hidden here
|
|
|
|
|
"missions": {"widget": "hidden"},
|
|
|
|
|
# default params applied to every mission without custom params
|
|
|
|
|
"default_mission_params": {"widget": "key-value", "label": "Default Mission Parameters",
|
|
|
|
|
"help": "Applied to all missions without custom params. Empty = no Params block."},
|
|
|
|
|
},
|
|
|
|
|
"basic": {
|
|
|
|
|
"min_bandwidth": {"widget": "number", "label": "Min Bandwidth (bps)", "min": 1},
|
|
|
|
|
@@ -480,7 +536,8 @@ class Arma3ConfigGenerator:
|
|
|
|
|
"options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"]},
|
|
|
|
|
"stance_indicator": {"widget": "select", "label": "Stance Indicator",
|
|
|
|
|
"options": ["0 - Never", "1 - Experimental", "2 - Always", "3 - Always (soldier)"]},
|
|
|
|
|
"stamina_bar": {"widget": "toggle", "label": "Stamina Bar"},
|
|
|
|
|
"stamina_bar": {"widget": "select", "label": "Stamina Bar",
|
|
|
|
|
"options": ["0 - Never", "1 - Low stamina only", "2 - Always"]},
|
|
|
|
|
"weapon_crosshair": {"widget": "toggle", "label": "Weapon Crosshair"},
|
|
|
|
|
"vision_aid": {"widget": "toggle", "label": "Vision Aid"},
|
|
|
|
|
"third_person_view": {"widget": "toggle", "label": "Third Person View"},
|
|
|
|
|
|