""" Arma 3 config generator. Merged protocol: Pydantic models (schema) + file generation + launch args. """ from __future__ import annotations import os from pathlib import Path 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 password_admin: str = "" server_command_password: str | None = None max_players: int = Field(default=40, gt=0, le=1000) kick_duplicate: int = Field(default=1, ge=0, le=1) persistent: int = Field(default=1, ge=0, le=1) vote_threshold: float = Field(default=0.33, ge=0.0, le=1.0) vote_mission_players: int = Field(default=1, ge=0) vote_timeout: int = Field(default=60, ge=0) role_timeout: int = Field(default=90, ge=0) briefing_timeout: int = Field(default=60, ge=0) debriefing_timeout: int = Field(default=45, ge=0) lobby_idle_timeout: int = Field(default=300, ge=0) disable_von: int = Field(default=0, ge=0, le=1) von_codec: int = Field(default=1, ge=0, le=1) von_codec_quality: int = Field(default=20, ge=0, le=30) max_ping: int = Field(default=250, gt=0) max_packet_loss: int = Field(default=50, ge=0, le=100) max_desync: int = Field(default=200, ge=0) disconnect_timeout: int = Field(default=15, ge=0) kick_on_ping: int = Field(default=1, ge=0, le=1) kick_on_packet_loss: int = Field(default=1, ge=0, le=1) kick_on_desync: int = Field(default=1, ge=0, le=1) kick_on_timeout: int = Field(default=1, ge=0, le=1) battleye: int = Field(default=1, ge=0, le=1) verify_signatures: int = Field(default=2, ge=0, le=2) allowed_file_patching: int = Field(default=0, ge=0, le=2) forced_difficulty: str = "Regular" timestamp_format: str = "short" auto_select_mission: int = Field(default=0, ge=0, le=1) random_mission_order: int = Field(default=0, ge=0, le=1) log_file: str = "server_console.log" skip_lobby: int = Field(default=0, ge=0, le=1) drawing_in_map: int = Field(default=1, ge=0, le=1) upnp: int = Field(default=0, ge=0, le=1) loopback: int = Field(default=0, ge=0, le=1) statistics_enabled: int = Field(default=1, ge=0, le=1) motd_lines: list[str] = Field(default_factory=lambda: ["Welcome!", "Have fun"]) motd_interval: float = Field(default=5.0, gt=0) 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[MissionRotationItem] = Field(default_factory=list) default_mission_params: dict[str, MissionParamValue] = Field(default_factory=dict) class BasicConfig(BaseModel): 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.001, gt=0) max_custom_file_size: int = Field(default=0, ge=0) class ProfileConfig(BaseModel): reduced_damage: int = Field(default=0, ge=0, le=1) group_indicators: int = Field(default=0, ge=0, le=3) friendly_tags: int = Field(default=0, ge=0, le=3) 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=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=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=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) map_content_enemy: int = Field(default=0, ge=0, le=3) map_content_mines: int = Field(default=0, ge=0, le=3) 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=1.0, ge=0.0, le=1.0) precision_ai: float = Field(default=0.2, ge=0.0, le=1.0) class LaunchConfig(BaseModel): world: str = "empty" extra_params: str = "" limit_fps: int = Field(default=50, gt=0, le=1000) auto_init: int = Field(default=0, ge=0, le=1) load_mission_to_memory: int = Field(default=0, ge=0, le=1) enable_ht: int = Field(default=0, ge=0, le=1) huge_pages: int = Field(default=0, ge=0, le=1) cpu_count: int | None = None ex_threads: int = Field(default=7, ge=0) max_mem: int | None = None no_logs: int = Field(default=0, ge=0, le=1) netlog: int = Field(default=0, ge=0, le=1) class RConConfig(BaseModel): rcon_password: str = "" max_ping: int = Field(default=200, gt=0) enabled: int = Field(default=1, ge=0, le=1) # ─── Config Generator ───────────────────────────────────────────────────────── class Arma3ConfigGenerator: game_type = "arma3" SECTIONS: dict[str, type[BaseModel]] = { "server": ServerConfig, "basic": BasicConfig, "profile": ProfileConfig, "launch": LaunchConfig, "rcon": RConConfig, } SENSITIVE_FIELDS: dict[str, list[str]] = { "server": ["password", "password_admin", "server_command_password"], "rcon": ["rcon_password"], } def get_sections(self) -> dict[str, type[BaseModel]]: return self.SECTIONS def get_defaults(self, section: str) -> dict[str, Any]: model_cls = self.SECTIONS.get(section) if model_cls is None: return {} return model_cls().model_dump() def get_sensitive_fields(self, section: str) -> list[str]: return self.SENSITIVE_FIELDS.get(section, []) def get_config_version(self) -> str: return "1.1.0" def migrate_config(self, old_version: str, config_json: dict) -> dict: 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: """ Escape a string for use inside Arma 3 double-quoted config values. Order matters: escape backslashes FIRST. """ value = value.replace("\\", "\\\\") value = value.replace('"', '\\"') value = value.replace('\n', '\\n') return value @staticmethod def _atomic_write(path: Path, content: str) -> None: """Write content to path atomically via tmp file + os.replace().""" from adapters.exceptions import ConfigWriteError tmp_path = path.with_suffix(path.suffix + ".tmp") try: path.parent.mkdir(parents=True, exist_ok=True) tmp_path.write_text(content, encoding="utf-8") os.replace(str(tmp_path), str(path)) except OSError as e: # Clean up tmp file if it exists try: tmp_path.unlink(missing_ok=True) except OSError as exc: logger.debug("Could not clean up temp file %s: %s", tmp_path, exc) raise ConfigWriteError(str(path), str(e)) from e def _render_server_cfg(self, cfg: ServerConfig) -> str: """Render server.cfg content string.""" motd_items = ", ".join(f'"{self._escape(l)}"' for l in cfg.motd_lines) headless = ", ".join(f'"{h}"' for h in cfg.headless_clients) local = ", ".join(f'"{l}"' for l in cfg.local_clients) admin_uids = ", ".join(f'"{u}"' for u in cfg.admin_uids) lines = [ f'hostname = "{self._escape(cfg.hostname)}";', ] if cfg.password: lines.append(f'password = "{self._escape(cfg.password)}";') if cfg.password_admin: lines.append(f'passwordAdmin = "{self._escape(cfg.password_admin)}";') if cfg.server_command_password: lines.append(f'serverCommandPassword = "{self._escape(cfg.server_command_password)}";') lines += [ f"maxPlayers = {cfg.max_players};", f"kickDuplicate = {cfg.kick_duplicate};", f"persistent = {cfg.persistent};", f"voteThreshold = {cfg.vote_threshold};", f"voteMissionPlayers = {cfg.vote_mission_players};", f"voteTimeout = {cfg.vote_timeout};", f"roleTimeout = {cfg.role_timeout};", f"briefingTimeOut = {cfg.briefing_timeout};", f"debriefingTimeOut = {cfg.debriefing_timeout};", f"lobbyIdleTimeout = {cfg.lobby_idle_timeout};", f"disableVoN = {cfg.disable_von};", f"vonCodec = {cfg.von_codec};", f"vonCodecQuality = {cfg.von_codec_quality};", f"maxPing = {cfg.max_ping};", f"maxPacketLoss = {cfg.max_packet_loss};", f"maxDesync = {cfg.max_desync};", f"disconnectTimeout = {cfg.disconnect_timeout};", f"kickOnPing = {cfg.kick_on_ping};", f"kickOnPacketLoss = {cfg.kick_on_packet_loss};", f"kickOnDesync = {cfg.kick_on_desync};", f"kickOnTimeout = {cfg.kick_on_timeout};", f"BattlEye = {cfg.battleye};", f"verifySignatures = {cfg.verify_signatures};", f"allowedFilePatching = {cfg.allowed_file_patching};", f'forcedDifficulty = "{cfg.forced_difficulty}";', f'timeStampFormat = "{cfg.timestamp_format}";', f"autoSelectMission = {cfg.auto_select_mission};", f"randomMissionOrder = {cfg.random_mission_order};", f'logFile = "{cfg.log_file}";', f"skipLobby = {cfg.skip_lobby};", f"drawingInMap = {cfg.drawing_in_map};", f"upnp = {cfg.upnp};", f"loopback = {cfg.loopback};", f"statisticsEnabled = {cfg.statistics_enabled};", f"motd[] = {{{motd_items}}};", f"motdInterval = {cfg.motd_interval};", ] if cfg.headless_clients: lines.append(f"headlessClients[] = {{{headless}}};") if cfg.local_clients: lines.append(f"localClient[] = {{{local}}};") if cfg.admin_uids: lines.append(f"admins[] = {{{admin_uids}}};") return "\n".join(lines) + "\n" + self._render_missions_block(cfg) def _render_basic_cfg(self, cfg: BasicConfig) -> str: return ( f"MinBandwidth = {cfg.min_bandwidth};\n" f"MaxBandwidth = {cfg.max_bandwidth};\n" f"MaxMsgSend = {cfg.max_msg_send};\n" f"MaxSizeGuaranteed = {cfg.max_size_guaranteed};\n" f"MaxSizeNonguaranteed = {cfg.max_size_non_guaranteed};\n" f"MinErrorToSend = {cfg.min_error_to_send};\n" f"MaxCustomFileSize = {cfg.max_custom_file_size};\n" ) def _render_arma3profile(self, cfg: ProfileConfig) -> str: return ( "class DifficultyPresets {\n" " class CustomDifficulty {\n" " class Options {\n" f" reducedDamage = {cfg.reduced_damage};\n" f" groupIndicators = {cfg.group_indicators};\n" f" friendlyTags = {cfg.friendly_tags};\n" f" enemyTags = {cfg.enemy_tags};\n" f" detectedMines = {cfg.detected_mines};\n" f" commands = {cfg.commands};\n" f" waypoints = {cfg.waypoints};\n" f" tacticalPing = {cfg.tactical_ping};\n" f" weaponInfo = {cfg.weapon_info};\n" f" stanceIndicator = {cfg.stance_indicator};\n" f" staminaBar = {cfg.stamina_bar};\n" f" weaponCrosshair = {cfg.weapon_crosshair};\n" f" visionAid = {cfg.vision_aid};\n" f" thirdPersonView = {cfg.third_person_view};\n" f" cameraShake = {cfg.camera_shake};\n" f" scoreTable = {cfg.score_table};\n" f" deathMessages = {cfg.death_messages};\n" f" vonID = {cfg.von_id};\n" f" mapContentFriendly = {cfg.map_content_friendly};\n" f" mapContentEnemy = {cfg.map_content_enemy};\n" f" mapContentMines = {cfg.map_content_mines};\n" f" autoReport = {cfg.auto_report};\n" f" multipleSaves = {cfg.multiple_saves};\n" " };\n" f" aiLevelPreset = {cfg.ai_level_preset};\n" " };\n" " class CustomAILevel {\n" f" skillAI = {cfg.skill_ai};\n" f" precisionAI = {cfg.precision_ai};\n" " };\n" "};\n" ) def _render_beserver_cfg(self, cfg: RConConfig) -> str: return ( f"RConPassword {cfg.rcon_password}\n" f"MaxPing {cfg.max_ping}\n" ) # ── Public interface ────────────────────────────────────────────────────── def write_configs( self, server_id: int, server_dir: Path, config_sections: dict[str, dict], ) -> list[Path]: server_cfg = ServerConfig(**config_sections.get("server", {})) basic_cfg = BasicConfig(**config_sections.get("basic", {})) profile_cfg = ProfileConfig(**config_sections.get("profile", {})) rcon_cfg = RConConfig(**config_sections.get("rcon", {})) written = [] pairs = [ (server_dir / "server.cfg", self._render_server_cfg(server_cfg)), (server_dir / "basic.cfg", self._render_basic_cfg(basic_cfg)), (server_dir / "server" / "server.Arma3Profile", self._render_arma3profile(profile_cfg)), (server_dir / "battleye" / "beserver.cfg", self._render_beserver_cfg(rcon_cfg)), ] for path, content in pairs: self._atomic_write(path, content) written.append(path) # Restrict permissions on files containing passwords (Unix only) if os.name != "nt": for path in [server_dir / "server.cfg", server_dir / "battleye" / "beserver.cfg"]: if path.exists(): os.chmod(path, 0o600) return written def build_launch_args( self, config_sections: dict[str, dict], mod_args: list[str] | None = None, server_dir: Path | None = None, ) -> list[str]: from adapters.exceptions import LaunchArgsError launch = LaunchConfig(**config_sections.get("launch", {})) server = ServerConfig(**config_sections.get("server", {})) # Arma 3 changes its own cwd to the exe directory at startup, so relative # paths in launch args resolve against the exe dir, not server_dir. # Use absolute paths when server_dir is provided so configs are always found. if server_dir is not None: d = Path(server_dir) config_arg = f"-config={d / 'server.cfg'}" cfg_arg = f"-cfg={d / 'basic.cfg'}" profiles_arg = f"-profiles={d / 'server'}" bepath_arg = f"-bepath={d / 'battleye'}" else: config_arg = "-config=server.cfg" cfg_arg = "-cfg=basic.cfg" profiles_arg = "-profiles=./server" bepath_arg = "-bepath=./battleye" args = [ f"-port={config_sections.get('_port', 2302)}", config_arg, cfg_arg, profiles_arg, "-name=server", f"-world={launch.world}", f"-limitFPS={launch.limit_fps}", bepath_arg, ] if launch.auto_init: args.append("-autoInit") if launch.enable_ht: args.append("-enableHT") if launch.huge_pages: args.append("-hugePages") if launch.cpu_count is not None: args.append(f"-cpuCount={launch.cpu_count}") if launch.max_mem is not None: args.append(f"-maxMem={launch.max_mem}") if launch.no_logs: args.append("-noLogs") if launch.netlog: args.append("-netlog") if launch.extra_params: args.extend(launch.extra_params.split()) if mod_args: args.extend(mod_args) return args def get_ui_schema(self) -> dict: B, A = False, True # basic / advanced shorthand return { "server": { # Identity — basic "hostname": {"widget": "text", "label": "Server Name", "advanced": B}, "max_players": {"widget": "number", "label": "Max Players", "min": 1, "max": 1000, "advanced": B}, "password": {"widget": "password", "label": "Join Password", "advanced": B}, "password_admin": {"widget": "password", "label": "Admin Password", "advanced": B}, "server_command_password": {"widget": "password", "label": "Server Command Password", "advanced": A}, # Message of the Day — basic "motd_lines": {"widget": "textarea", "label": "Message of the Day (one line per row)", "advanced": B}, "motd_interval": {"widget": "number", "label": "MOTD Interval (sec)", "min": 1, "advanced": B}, # Mission / Rotation — basic "forced_difficulty": {"widget": "select", "label": "Forced Difficulty", "options": ["Recruit", "Regular", "Veteran", "Custom"], "advanced": B}, "auto_select_mission": {"widget": "toggle", "label": "Auto-Select Mission", "advanced": B}, "random_mission_order": {"widget": "toggle", "label": "Random Mission Order", "advanced": B}, # Behaviour — mixed "persistent": {"widget": "toggle", "label": "Persistent (keep running when empty)", "advanced": B}, "kick_duplicate": {"widget": "toggle", "label": "Kick Duplicate Connections", "advanced": A}, "skip_lobby": {"widget": "toggle", "label": "Skip Lobby (go straight to briefing)", "advanced": B}, "drawing_in_map": {"widget": "toggle", "label": "Allow Drawing in Map", "advanced": B}, # Security — basic "battleye": {"widget": "toggle", "label": "BattlEye Anti-Cheat", "advanced": B}, "verify_signatures": {"widget": "select", "label": "Verify Addon Signatures", "options": ["0 - Off", "1 - Kick unsigned", "2 - Strict (kick mismatched)"], "advanced": B}, "allowed_file_patching": {"widget": "select", "label": "Allow File Patching", "options": ["0 - Nobody", "1 - Lobby only", "2 - Everyone"], "advanced": B}, # Voice — basic "disable_von": {"widget": "toggle", "label": "Disable Voice-over-Network (VoN)", "advanced": B}, "von_codec": {"widget": "toggle", "label": "Use Opus VoN Codec", "advanced": B}, "von_codec_quality": {"widget": "number", "label": "VoN Codec Quality (0–30)", "min": 0, "max": 30, "advanced": A}, # Network / Kick thresholds — advanced "kick_on_ping": {"widget": "toggle", "label": "Kick on High Ping", "advanced": A}, "kick_on_packet_loss": {"widget": "toggle", "label": "Kick on High Packet Loss", "advanced": A}, "kick_on_desync": {"widget": "toggle", "label": "Kick on High Desync", "advanced": A}, "kick_on_timeout": {"widget": "toggle", "label": "Kick on Timeout", "advanced": A}, "max_ping": {"widget": "number", "label": "Max Ping (ms)", "min": 1, "advanced": A}, "max_packet_loss": {"widget": "number", "label": "Max Packet Loss (%)", "min": 0, "max": 100, "advanced": A}, "max_desync": {"widget": "number", "label": "Max Desync", "min": 0, "advanced": A}, "disconnect_timeout": {"widget": "number", "label": "Disconnect Timeout (sec)", "min": 0, "advanced": A}, # Voting — advanced "vote_threshold": {"widget": "number", "label": "Vote Threshold (0.0–1.0)", "min": 0, "max": 1, "advanced": A}, "vote_mission_players": {"widget": "number", "label": "Min Players to Start Vote", "min": 0, "advanced": A}, "vote_timeout": {"widget": "number", "label": "Vote Timeout (sec)", "min": 0, "advanced": A}, # Timeouts — advanced "role_timeout": {"widget": "number", "label": "Role Selection Timeout (sec)", "min": 0, "advanced": A}, "briefing_timeout": {"widget": "number", "label": "Briefing Timeout (sec)", "min": 0, "advanced": A}, "debriefing_timeout": {"widget": "number", "label": "Debriefing Timeout (sec)", "min": 0, "advanced": A}, "lobby_idle_timeout": {"widget": "number", "label": "Lobby Idle Timeout (sec)", "min": 0, "advanced": A}, # Misc — advanced "statistics_enabled": {"widget": "toggle", "label": "Enable Steam Statistics", "advanced": A}, "upnp": {"widget": "toggle", "label": "Enable UPnP", "advanced": A}, "loopback": {"widget": "toggle", "label": "Loopback Mode (LAN only)", "advanced": A}, "timestamp_format": {"widget": "select", "label": "Log Timestamp Format", "options": ["none", "short", "full"], "advanced": A}, "log_file": {"widget": "text", "label": "Log File Name", "advanced": A}, # Admin / Headless — advanced "admin_uids": {"widget": "tag-list", "label": "Admin Steam UIDs", "placeholder": "76561198000000000", "advanced": A}, "headless_clients": {"widget": "tag-list", "label": "Headless Client IPs", "placeholder": "127.0.0.1", "advanced": A}, "local_clients": {"widget": "tag-list", "label": "Local Client IPs", "placeholder": "127.0.0.1", "advanced": A}, # missions managed by the Missions tab — hidden here "missions": {"widget": "hidden"}, # default params — advanced "default_mission_params": {"widget": "key-value", "label": "Default Mission Parameters", "help": "Applied to all missions without custom params.", "advanced": A}, }, "basic": { # All network tuning fields are advanced "min_bandwidth": {"widget": "number", "label": "Min Bandwidth (bps)", "min": 1, "advanced": A}, "max_bandwidth": {"widget": "number", "label": "Max Bandwidth (bps)", "min": 1, "advanced": A}, "max_msg_send": {"widget": "number", "label": "Max Messages Sent per Frame", "min": 1, "advanced": A}, "max_size_guaranteed": {"widget": "number", "label": "Max Guaranteed Packet Size (bytes)", "min": 1, "advanced": A}, "max_size_non_guaranteed": {"widget": "number", "label": "Max Non-Guaranteed Packet Size (bytes)", "min": 1, "advanced": A}, "min_error_to_send": {"widget": "number", "label": "Min Error to Send", "advanced": A}, "max_custom_file_size": {"widget": "number", "label": "Max Custom File Size (bytes)", "min": 0, "advanced": A}, }, "profile": { # Basic difficulty options "reduced_damage": {"widget": "toggle", "label": "Reduced Damage", "advanced": A}, "group_indicators": {"widget": "select", "label": "Group Indicators", "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"], "advanced": B}, "friendly_tags": {"widget": "select", "label": "Friendly Name Tags", "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"], "advanced": B}, "enemy_tags": {"widget": "select", "label": "Enemy Name Tags", "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"], "advanced": B}, "detected_mines": {"widget": "select", "label": "Detected Mines", "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"], "advanced": A}, "commands": {"widget": "select", "label": "Map Commands", "options": ["0 - Never", "1 - High command", "2 - Fade out", "3 - Always"], "advanced": B}, "waypoints": {"widget": "select", "label": "Waypoints", "options": ["0 - Never", "1 - Known positions", "2 - Fade out", "3 - Always"], "advanced": B}, "tactical_ping": {"widget": "toggle", "label": "Tactical Ping", "advanced": A}, "weapon_info": {"widget": "select", "label": "Weapon Info", "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"], "advanced": B}, "stance_indicator": {"widget": "select", "label": "Stance Indicator", "options": ["0 - Never", "1 - Experimental", "2 - Always", "3 - Always (soldier)"], "advanced": B}, "stamina_bar": {"widget": "select", "label": "Stamina Bar", "options": ["0 - Never", "1 - Low stamina only", "2 - Always"], "advanced": A}, "weapon_crosshair": {"widget": "toggle", "label": "Weapon Crosshair", "advanced": A}, "vision_aid": {"widget": "toggle", "label": "Vision Aid", "advanced": A}, "third_person_view": {"widget": "toggle", "label": "Third Person View", "advanced": A}, "camera_shake": {"widget": "toggle", "label": "Camera Shake", "advanced": A}, "score_table": {"widget": "toggle", "label": "Show Score Table", "advanced": A}, "death_messages": {"widget": "toggle", "label": "Death Messages", "advanced": A}, "von_id": {"widget": "toggle", "label": "Show VoN Speaker ID", "advanced": A}, "map_content_friendly": {"widget": "select", "label": "Map — Friendly Units", "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"], "advanced": A}, "map_content_enemy": {"widget": "select", "label": "Map — Enemy Units", "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"], "advanced": A}, "map_content_mines": {"widget": "select", "label": "Map — Mines", "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"], "advanced": A}, "auto_report": {"widget": "toggle", "label": "Auto Report (automatic radio reports)", "advanced": A}, "multiple_saves": {"widget": "toggle", "label": "Multiple Saves", "advanced": A}, "ai_level_preset": {"widget": "select", "label": "AI Level Preset", "options": ["0 - Low", "1 - Normal", "2 - High", "3 - Custom", "4 - Ultra"], "advanced": B}, "skill_ai": {"widget": "number", "label": "AI Skill (0.0–1.0)", "min": 0, "max": 1, "advanced": B}, "precision_ai": {"widget": "number", "label": "AI Precision / Accuracy (0.0–1.0)", "min": 0, "max": 1, "advanced": B}, }, "launch": { # All launch/startup fields are advanced "world": {"widget": "text", "label": "Default World (map name)", "advanced": A}, "limit_fps": {"widget": "number", "label": "FPS Limit", "min": 1, "max": 1000, "advanced": A}, "cpu_count": {"widget": "number", "label": "CPU Core Count (0 = auto)", "min": 0, "advanced": A}, "ex_threads": {"widget": "number", "label": "Extra Thread Count", "min": 0, "advanced": A}, "max_mem": {"widget": "number", "label": "Max RAM (MB, 0 = auto)", "min": 0, "advanced": A}, "auto_init": {"widget": "toggle", "label": "Auto-Init (skip mission select)", "advanced": A}, "load_mission_to_memory": {"widget": "toggle", "label": "Load Mission to Memory", "advanced": A}, "enable_ht": {"widget": "toggle", "label": "Enable HyperThreading", "advanced": A}, "huge_pages": {"widget": "toggle", "label": "Enable Huge Pages (performance)", "advanced": A}, "no_logs": {"widget": "toggle", "label": "Disable Server Logging", "advanced": A}, "netlog": {"widget": "toggle", "label": "Enable Network Log", "advanced": A}, "extra_params": {"widget": "tag-list", "label": "Additional Startup Parameters", "placeholder": "-filePatching", "advanced": A}, }, "rcon": { "rcon_password": {"widget": "password", "label": "RCon Password", "advanced": B}, "max_ping": {"widget": "number", "label": "Max Ping for RCon (ms)", "min": 1, "advanced": A}, "enabled": {"widget": "toggle", "label": "Enable RCon", "advanced": B}, }, } def preview_config( self, server_id: int, server_dir: Path, config_sections: dict[str, dict], ) -> dict[str, str]: server_cfg = ServerConfig(**config_sections.get("server", {})) basic_cfg = BasicConfig(**config_sections.get("basic", {})) profile_cfg = ProfileConfig(**config_sections.get("profile", {})) rcon_cfg = RConConfig(**config_sections.get("rcon", {})) return { "server.cfg": self._render_server_cfg(server_cfg), "basic.cfg": self._render_basic_cfg(basic_cfg), "server/server.Arma3Profile": self._render_arma3profile(profile_cfg), "battleye/beserver.cfg": self._render_beserver_cfg(rcon_cfg), }