Files
languard-servers-manager/backend/adapters/arma3/config_generator.py
Tran G. (Revernomad) Khoa bf09a6ed1c fix: fix Arma 3 log discovery and improve config editor UX
- Fix logfiles_router and thread_registry to resolve .rpt log files
  from Path(server["exe_path"]).parent/server/ instead of the languard
  data dir, which never contained log files — log list and live tail
  both now work correctly
- Rewrite get_ui_schema() in config_generator to cover all ~80 fields
  across all 5 sections (server/basic/profile/launch/rcon) with proper
  toggle/select/number/password/tag-list/hidden widgets and labels;
  missions field is hidden (managed by Missions tab)
- Add formatSelectDisplay() to ConfigEditor so select fields show
  descriptive text (e.g. "0 - Never") instead of raw numbers in view mode
- Add ToggleDisplay for boolean fields (Enabled/Disabled with indicator dot)
- Add section tab labels and descriptions to ConfigEditor
- Add MissionList UX hints and dynamic Add/In Rotation button labels
- Add "hidden" to FieldSchema widget union type
- Update API.md, ARCHITECTURE.md, CLAUDE.md, FRONTEND.md, MODULES.md,
  THREADING.md to document log path fix and schema coverage
2026-04-18 15:56:04 +07:00

541 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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
from pydantic import BaseModel, Field
# ─── Pydantic Models (config schema) ─────────────────────────────────────────
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[dict] = Field(default_factory=list)
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)
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)
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=1, 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)
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)
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=0.5, ge=0.0, le=1.0)
precision_ai: float = Field(default=0.5, 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.0.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
raise ConfigMigrationError(
old_version, f"No migration path from {old_version} to {self.get_config_version()}"
)
# ── Config file writers ───────────────────────────────────────────────────
@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"
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,
) -> list[str]:
from adapters.exceptions import LaunchArgsError
launch = LaunchConfig(**config_sections.get("launch", {}))
server = ServerConfig(**config_sections.get("server", {}))
args = [
f"-port={config_sections.get('_port', 2302)}",
"-config=server.cfg",
"-cfg=basic.cfg",
"-profiles=./server",
"-name=server",
f"-world={launch.world}",
f"-limitFPS={launch.limit_fps}",
"-bepath=./battleye",
]
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:
return {
"server": {
# Identity
"hostname": {"widget": "text", "label": "Server Name"},
"max_players": {"widget": "number", "label": "Max Players", "min": 1, "max": 1000},
"password": {"widget": "password", "label": "Join Password"},
"password_admin": {"widget": "password", "label": "Admin Password"},
"server_command_password": {"widget": "password", "label": "Server Command Password"},
# Message of the Day
"motd_lines": {"widget": "textarea", "label": "Message of the Day (one line per row)"},
"motd_interval": {"widget": "number", "label": "MOTD Interval (sec)", "min": 1},
# Mission / Rotation
"forced_difficulty": {"widget": "select", "label": "Forced Difficulty",
"options": ["Recruit", "Regular", "Veteran", "Custom"]},
"auto_select_mission": {"widget": "toggle", "label": "Auto-Select Mission"},
"random_mission_order": {"widget": "toggle", "label": "Random Mission Order"},
# Behaviour
"persistent": {"widget": "toggle", "label": "Persistent (keep running when empty)"},
"kick_duplicate": {"widget": "toggle", "label": "Kick Duplicate Connections"},
"skip_lobby": {"widget": "toggle", "label": "Skip Lobby (go straight to briefing)"},
"drawing_in_map": {"widget": "toggle", "label": "Allow Drawing in Map"},
# Security
"battleye": {"widget": "toggle", "label": "BattlEye Anti-Cheat"},
"verify_signatures": {"widget": "select", "label": "Verify Addon Signatures",
"options": ["0 - Off", "1 - Kick unsigned", "2 - Strict (kick mismatched)"]},
"allowed_file_patching": {"widget": "select", "label": "Allow File Patching",
"options": ["0 - Nobody", "1 - Lobby only", "2 - Everyone"]},
# Voice
"disable_von": {"widget": "toggle", "label": "Disable Voice-over-Network (VoN)"},
"von_codec": {"widget": "toggle", "label": "Use Opus VoN Codec"},
"von_codec_quality": {"widget": "number", "label": "VoN Codec Quality (030)", "min": 0, "max": 30},
# Network / Kick thresholds
"kick_on_ping": {"widget": "toggle", "label": "Kick on High Ping"},
"kick_on_packet_loss": {"widget": "toggle", "label": "Kick on High Packet Loss"},
"kick_on_desync": {"widget": "toggle", "label": "Kick on High Desync"},
"kick_on_timeout": {"widget": "toggle", "label": "Kick on Timeout"},
"max_ping": {"widget": "number", "label": "Max Ping (ms)", "min": 1},
"max_packet_loss": {"widget": "number", "label": "Max Packet Loss (%)", "min": 0, "max": 100},
"max_desync": {"widget": "number", "label": "Max Desync", "min": 0},
"disconnect_timeout": {"widget": "number", "label": "Disconnect Timeout (sec)", "min": 0},
# Voting
"vote_threshold": {"widget": "number", "label": "Vote Threshold (0.01.0)", "min": 0, "max": 1},
"vote_mission_players": {"widget": "number", "label": "Min Players to Start Vote", "min": 0},
"vote_timeout": {"widget": "number", "label": "Vote Timeout (sec)", "min": 0},
# Timeouts
"role_timeout": {"widget": "number", "label": "Role Selection Timeout (sec)", "min": 0},
"briefing_timeout": {"widget": "number", "label": "Briefing Timeout (sec)", "min": 0},
"debriefing_timeout": {"widget": "number", "label": "Debriefing Timeout (sec)", "min": 0},
"lobby_idle_timeout": {"widget": "number", "label": "Lobby Idle Timeout (sec)", "min": 0},
# Misc
"statistics_enabled": {"widget": "toggle", "label": "Enable Steam Statistics"},
"upnp": {"widget": "toggle", "label": "Enable UPnP"},
"loopback": {"widget": "toggle", "label": "Loopback Mode (LAN only)"},
"timestamp_format": {"widget": "select", "label": "Log Timestamp Format",
"options": ["none", "short", "full"]},
"log_file": {"widget": "text", "label": "Log File Name"},
# Admin / Headless
"admin_uids": {"widget": "tag-list", "label": "Admin Steam UIDs",
"placeholder": "76561198000000000"},
"headless_clients": {"widget": "tag-list", "label": "Headless Client IPs",
"placeholder": "127.0.0.1"},
"local_clients": {"widget": "tag-list", "label": "Local Client IPs",
"placeholder": "127.0.0.1"},
# missions managed by the Missions tab — hidden here
"missions": {"widget": "hidden"},
},
"basic": {
"min_bandwidth": {"widget": "number", "label": "Min Bandwidth (bps)", "min": 1},
"max_bandwidth": {"widget": "number", "label": "Max Bandwidth (bps)", "min": 1},
"max_msg_send": {"widget": "number", "label": "Max Messages Sent per Frame", "min": 1},
"max_size_guaranteed": {"widget": "number", "label": "Max Guaranteed Packet Size (bytes)", "min": 1},
"max_size_non_guaranteed": {"widget": "number", "label": "Max Non-Guaranteed Packet Size (bytes)", "min": 1},
"min_error_to_send": {"widget": "number", "label": "Min Error to Send"},
"max_custom_file_size": {"widget": "number", "label": "Max Custom File Size (bytes)", "min": 0},
},
"profile": {
# Damage / health
"reduced_damage": {"widget": "toggle", "label": "Reduced Damage"},
# Indicators (0=Never, 1=Limited distance, 2=Fade out, 3=Always)
"group_indicators": {"widget": "select", "label": "Group Indicators",
"options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"]},
"friendly_tags": {"widget": "select", "label": "Friendly Name Tags",
"options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"]},
"enemy_tags": {"widget": "select", "label": "Enemy Name Tags",
"options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"]},
"detected_mines": {"widget": "select", "label": "Detected Mines",
"options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"]},
"commands": {"widget": "select", "label": "Map Commands",
"options": ["0 - Never", "1 - High command", "2 - Fade out", "3 - Always"]},
"waypoints": {"widget": "select", "label": "Waypoints",
"options": ["0 - Never", "1 - Known positions", "2 - Fade out", "3 - Always"]},
"tactical_ping": {"widget": "toggle", "label": "Tactical Ping"},
"weapon_info": {"widget": "select", "label": "Weapon Info",
"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"},
"weapon_crosshair": {"widget": "toggle", "label": "Weapon Crosshair"},
"vision_aid": {"widget": "toggle", "label": "Vision Aid"},
"third_person_view": {"widget": "toggle", "label": "Third Person View"},
"camera_shake": {"widget": "toggle", "label": "Camera Shake"},
"score_table": {"widget": "toggle", "label": "Show Score Table"},
"death_messages": {"widget": "toggle", "label": "Death Messages"},
"von_id": {"widget": "toggle", "label": "Show VoN Speaker ID"},
"map_content_friendly": {"widget": "select", "label": "Map — Friendly Units",
"options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"]},
"map_content_enemy": {"widget": "select", "label": "Map — Enemy Units",
"options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"]},
"map_content_mines": {"widget": "select", "label": "Map — Mines",
"options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"]},
"auto_report": {"widget": "toggle", "label": "Auto Report (automatic radio reports)"},
"multiple_saves": {"widget": "toggle", "label": "Multiple Saves"},
"ai_level_preset": {"widget": "select", "label": "AI Level Preset",
"options": ["0 - Low", "1 - Normal", "2 - High", "3 - Custom", "4 - Ultra"]},
"skill_ai": {"widget": "number", "label": "AI Skill (0.01.0)", "min": 0, "max": 1},
"precision_ai": {"widget": "number", "label": "AI Precision / Accuracy (0.01.0)", "min": 0, "max": 1},
},
"launch": {
"world": {"widget": "text", "label": "Default World (map name)"},
"limit_fps": {"widget": "number", "label": "FPS Limit", "min": 1, "max": 1000},
"cpu_count": {"widget": "number", "label": "CPU Core Count (0 = auto)", "min": 0},
"ex_threads": {"widget": "number", "label": "Extra Thread Count", "min": 0},
"max_mem": {"widget": "number", "label": "Max RAM (MB, 0 = auto)", "min": 0},
"auto_init": {"widget": "toggle", "label": "Auto-Init (skip mission select)"},
"load_mission_to_memory": {"widget": "toggle", "label": "Load Mission to Memory"},
"enable_ht": {"widget": "toggle", "label": "Enable HyperThreading"},
"huge_pages": {"widget": "toggle", "label": "Enable Huge Pages (performance)"},
"no_logs": {"widget": "toggle", "label": "Disable Server Logging"},
"netlog": {"widget": "toggle", "label": "Enable Network Log"},
"extra_params": {"widget": "tag-list", "label": "Additional Startup Parameters",
"placeholder": "-filePatching"},
},
"rcon": {
"rcon_password": {"widget": "password", "label": "RCon Password"},
"max_ping": {"widget": "number", "label": "Max Ping for RCon (ms)", "min": 1},
"enabled": {"widget": "toggle", "label": "Enable RCon"},
},
}
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),
}