#!/usr/bin/env python3 """ Comprehensive Game Server Documentation Generator for GSP This script generates PHP documentation files for game servers in the GSP billing module. USAGE: # Generate docs for all incomplete games: python3 generate_game_docs.py # Generate docs for specific game(s): python3 generate_game_docs.py --games minecraft csgo rust # Regenerate docs (overwrite existing): python3 generate_game_docs.py --games minecraft --force # Process all "todo" category games: python3 generate_game_docs.py --todo-only OPTIONS: --games GAME1 GAME2 ... Generate docs for specific game folder names --force Overwrite existing documentation files --todo-only Only process games with category="todo" --help Show this help message The generator extracts information from: - XML configurations (modules/config_games/server_configs/*.xml) - YAML knowledgepack (modules/billing/docs/gameserver_knowledgepack_v2.yaml) - Existing metadata.json files Generated documentation includes: - Quick info (ports, RAM, Steam App ID) - Network ports and firewall configuration - Installation steps with SteamCMD commands - Server configuration files and settings - Detailed startup parameters from XML - Comprehensive troubleshooting guide - Performance optimization tips - Security best practices """ import os import sys import json import yaml import re import argparse import html from pathlib import Path from datetime import datetime import xml.etree.ElementTree as ET class GameDocGenerator: def __init__(self, docs_dir, config_dir, knowledgepack_path): self.docs_dir = Path(docs_dir) self.config_dir = Path(config_dir) self.knowledgepack_path = Path(knowledgepack_path) self.knowledgepack_data = None self.xml_configs = {} def load_knowledgepack(self): """Load the YAML knowledgepack with game information""" try: with open(self.knowledgepack_path, 'r', encoding='utf-8') as f: data = yaml.safe_load(f) self.knowledgepack_data = data.get('games', []) print(f"Loaded knowledgepack with {len(self.knowledgepack_data)} games") return True except Exception as e: print(f"Error loading knowledgepack: {e}") return False def load_xml_configs(self): """Load all XML configuration files""" xml_files = list(self.config_dir.glob("*.xml")) for xml_file in xml_files: try: tree = ET.parse(xml_file) root = tree.getroot() game_key = root.find('game_key') if game_key is not None and game_key.text: self.xml_configs[game_key.text] = { 'file': xml_file.name, 'tree': root } except Exception as e: print(f"Error parsing {xml_file}: {e}") print(f"Loaded {len(self.xml_configs)} XML configurations") def get_game_info_from_knowledgepack(self, game_name): """Find game info in knowledgepack by name""" if not self.knowledgepack_data: return None # Try exact match first for game in self.knowledgepack_data: if game.get('name', '').lower() == game_name.lower(): return game # Try partial match for game in self.knowledgepack_data: if game_name.lower() in game.get('name', '').lower(): return game return None def get_xml_config(self, folder_name): """Find matching XML config for a folder""" # Try exact match for key, config in self.xml_configs.items(): if key.lower() == folder_name.lower() or key.lower().replace('_', '') == folder_name.lower(): return config['tree'] # Try partial match for key, config in self.xml_configs.items(): if folder_name.lower() in key.lower() or key.lower() in folder_name.lower(): return config['tree'] return None def extract_ports_from_xml(self, xml_root): """Extract port information from XML config""" ports = [] # Look for replace_texts with port keys replace_texts = xml_root.find('replace_texts') if replace_texts is not None: for text in replace_texts.findall('text'): key = text.get('key', '') if 'port' in key.lower(): filepath = text.find('filepath') if filepath is not None: ports.append({ 'key': key, 'file': filepath.text }) # Look for custom_fields with port information custom_fields = xml_root.find('custom_fields') if custom_fields is not None: for field in custom_fields.findall('field'): key = field.get('key', '') if 'port' in key.lower(): default_value = field.find('default_value') desc = field.find('desc') ports.append({ 'key': key, 'default': default_value.text if default_value is not None else None, 'description': desc.text if desc is not None else None }) return ports def extract_startup_parameters_from_xml(self, xml_root): """Extract detailed startup parameters from XML server_params section""" startup_params = [] # Get CLI template and params cli_template = xml_root.find('cli_template') cli_template_text = cli_template.text if cli_template is not None else "" cli_params_elem = xml_root.find('cli_params') cli_params_info = {} if cli_params_elem is not None: for param in cli_params_elem.findall('cli_param'): param_id = param.get('id', '') cli_string = param.get('cli_string', '') options = param.get('options', '') if param_id: cli_params_info[param_id] = { 'cli_string': cli_string, 'options': options } # Extract server_params - these are the configurable startup parameters server_params = xml_root.find('server_params') if server_params is not None: for param in server_params.findall('param'): param_key = param.get('key', '') param_id = param.get('id', '') param_type = param.get('type', 'text') # Get caption and description caption_elem = param.find('caption') desc_elem = param.find('desc') default_elem = param.find('default') caption = caption_elem.text if caption_elem is not None else param_key description = desc_elem.text if desc_elem is not None else "No description available" default_value = default_elem.text if default_elem is not None else None # For select type, get options options = [] if param_type == 'select': for option in param.findall('option'): opt_value = option.get('value', '') opt_text = option.text if option.text is not None else opt_value options.append({'value': opt_value, 'text': opt_text}) startup_params.append({ 'key': param_key, 'id': param_id, 'type': param_type, 'caption': caption, 'description': description, 'default': default_value, 'options': options }) return { 'cli_template': cli_template_text, 'cli_params': cli_params_info, 'server_params': startup_params } def extract_config_files_from_xml(self, xml_root): """Extract configuration file paths from XML""" config_files = [] config_files_elem = xml_root.find('configuration_files') if config_files_elem is not None: for file_elem in config_files_elem.findall('file'): desc = file_elem.get('description', 'Configuration file') path = file_elem.text if file_elem.text else '' config_files.append({ 'description': desc, 'path': path }) return config_files def generate_php_doc(self, folder_name, metadata): """Generate comprehensive PHP documentation for a game""" game_name = metadata.get('name', folder_name.replace('_', ' ').title()) # Get additional data kb_info = self.get_game_info_from_knowledgepack(game_name) xml_config = self.get_xml_config(folder_name) # Extract ports, configs, and startup parameters ports_info = [] config_files = [] startup_params_data = {} if xml_config is not None: ports_info = self.extract_ports_from_xml(xml_config) config_files = self.extract_config_files_from_xml(xml_config) startup_params_data = self.extract_startup_parameters_from_xml(xml_config) # Build the PHP document php_content = self.build_php_content(game_name, folder_name, kb_info, xml_config, ports_info, config_files, startup_params_data) return php_content def get_steam_app_id(self, game_name, folder_name): """Get Steam App ID for a game""" # Common Steam App IDs app_ids = { '7daystodie': '294420', 'ark': '376030', 'arkse': '376030', 'arma3': '233780', 'arma2oa': '33930', 'csgo': '740', 'css': '232330', 'dayz': '221100', 'garrysmod': '4020', 'gmod': '4020', 'killingfloor': '215350', 'killingfloor2': '232130', 'left4dead': '222840', 'left4dead2': '222860', 'rust': '258550', 'squad': '403240', 'tf2': '232250', 'terraria': '105600', 'theforest': '556450', 'unturned': '1110390', 'valheim': '896660', 'insurgency': '237410', 'insurgencysandstorm': '581320', 'conanexiles': '443030', 'dontstarvetogether': '343050', 'lifeisfeudal': '320850', 'mordhau': '629760', } # Try folder name if folder_name.lower() in app_ids: return app_ids[folder_name.lower()] # Try game name game_lower = game_name.lower().replace(' ', '').replace(':', '').replace('-', '') for key, appid in app_ids.items(): if key in game_lower or game_lower in key: return appid return 'N/A' def build_php_content(self, game_name, folder_name, kb_info, xml_config, ports_info, config_files, startup_params_data): """Build the complete PHP documentation content""" # Extract data from various sources default_port = "Varies (see configuration)" protocol = "TCP/UDP" min_ram = "1GB" engine = "Various" startup_cmd = "" app_id = self.get_steam_app_id(game_name, folder_name) # Try to get port from XML first if xml_config is not None: # Check for default port in mods section mods = xml_config.find('mods') if mods is not None: mod = mods.find('mod') if mod is not None: installer_name = mod.find('installer_name') if installer_name is not None and installer_name.text and app_id == 'N/A': app_id = installer_name.text if kb_info: network = kb_info.get('network', {}) default_ports = network.get('default_ports', []) if default_ports: port_info = default_ports[0] port_str = port_info.get('port', '') if '/' in port_str: default_port = port_str.split('/')[0] protocol = port_str.split('/')[1].upper() else: default_port = port_str requirements = kb_info.get('requirements', {}) min_ram = requirements.get('ram', '1GB') engine = kb_info.get('engine', 'Various') startup = kb_info.get('typical_startup', {}) startup_cmd = startup.get('linux', '') or startup.get('windows', '') php_doc = '''
''' + game_name + ''' is a multiplayer game server that can be hosted on a VPS or dedicated server. This comprehensive guide covers everything you need to know about hosting a ''' + game_name + ''' server for your community.
''' + default_port + '''''' + app_id + '''{cf["path"]} - {cf["description"]}| Port | Protocol | Purpose |
|---|---|---|
{port_num} |
{proto} | {purpose} |
{port_num} |
{proto} | {purpose} (Optional) |
The ''' + game_name + ''' server typically uses a configurable port. Check your server configuration files for the specific port settings.
''' php_doc += '''Allow server ports through your firewall:
# UFW (Ubuntu/Debian)
sudo ufw allow [PORT]/tcp
sudo ufw allow [PORT]/udp
sudo ufw reload
# FirewallD (CentOS/RHEL)
sudo firewall-cmd --permanent --add-port=[PORT]/tcp
sudo firewall-cmd --permanent --add-port=[PORT]/udp
sudo firewall-cmd --reload
# Windows Firewall
netsh advfirewall firewall add rule name="''' + game_name + ''' Server" dir=in action=allow protocol=TCP localport=[PORT]
netsh advfirewall firewall add rule name="''' + game_name + ''' Server" dir=in action=allow protocol=UDP localport=[PORT]
# Update system packages
sudo apt update && sudo apt upgrade -y
# Create server directory
mkdir -p ~/gameserver
cd ~/gameserver
# Download server files (method varies by game)
# Check official documentation for download links
'''
if startup_cmd:
php_doc += f'''
{startup_cmd}
'''
php_doc += '''
Download the server files from the official game website or through Steam (if applicable). Extract to a dedicated folder and run the server executable.
''' # Add SteamCMD section with actual App ID if app_id != 'N/A': php_doc += f'''This game can be installed via SteamCMD using App ID: {app_id}
# Update package list
sudo apt update
# Enable 32-bit architecture
sudo dpkg --add-architecture i386
sudo apt update
# Install SteamCMD
sudo apt install -y lib32gcc-s1 steamcmd
# Create directory for game server
mkdir -p ~/gameservers/{folder_name}
# Run SteamCMD and download
steamcmd +login anonymous \\
+force_install_dir ~/gameservers/{folder_name} \\
+app_update {app_id} validate \\
+quit
# Server files are now in ~/gameservers/{folder_name}/
cd ~/gameservers/{folder_name}
ls -la
C:\\steamcmd\\cd C:\\steamcmd
steamcmd.exe +login anonymous ^
+force_install_dir C:\\gameservers\\{folder_name} ^
+app_update {app_id} validate ^
+quit
'''
else:
php_doc += '''
This game requires manual download. Check the official game website or Steam store page for dedicated server downloads.
''' php_doc += '''After installation, you'll need to configure your server. Here's where to find the configuration files and what settings you can change.
Important configuration files for this server:
{cf["path"]} - {cf["description"]}Common administrative commands (access via console or RCON):
# Kick player
kick [player_name]
# Ban player
ban [player_name]
# Change map/level (syntax varies by game)
changelevel [map_name]
# Set admin password (if supported)
setadminpassword [password]
The server uses the following command line template:
{cli_template}
'''
php_doc += '''
The following parameters can be configured when starting the server:
{param_key}
- {caption}
{description_clean}
''' if param_type == 'select' and options: php_doc += '''Options:
{opt['value']} - {opt['text']}Default: {default}
{startup_cmd}
'''
else:
php_doc += '''# Generic startup command structure
./server_executable [parameters]
'''
php_doc += '''
-port [number] - Set the server port-maxplayers [number] - Maximum player slots-map [name] - Starting map/level-console - Enable console output-nographics - Run without graphics (headless mode)Linux (start.sh):
#!/bin/bash
cd /path/to/server
./server_executable [parameters] 2>&1 | tee server.log
chmod +x start.sh
./start.sh
Windows (start.bat):
@echo off
cd /d "%~dp0"
server_executable.exe [parameters]
pause
Linux (systemd):
# Create service file: /etc/systemd/system/gameserver.service
[Unit]
Description=''' + game_name + ''' Server
After=network.target
[Service]
Type=simple
User=gameserver
WorkingDirectory=/home/gameserver/server
ExecStart=/home/gameserver/server/start.sh
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
# Enable and start service
sudo systemctl daemon-reload
sudo systemctl enable gameserver
sudo systemctl start gameserver
sudo systemctl status gameserver
{fix}
''' else: php_doc += '''# View recent log entries
tail -f server.log
# Or check system logs
journalctl -u gameserver -f
# Find what's using the port
sudo lsof -i :[PORT]
sudo netstat -tulpn | grep [PORT]
# Kill the process or change server port
Ensure all required dependencies are installed. Check the error messages for missing libraries or packages.
''' php_doc += '''ps aux | grep servernetstat -an | grep [PORT]htop or topiotop# Monitor memory usage
free -h
top -p $(pgrep -f server)
# Restart server regularly via cron if needed
0 4 * * * /home/gameserver/restart.sh
# Increase file descriptor limits
echo "* soft nofile 65536" >> /etc/security/limits.conf
echo "* hard nofile 65536" >> /etc/security/limits.conf
# Network tuning
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
sysctl -w net.ipv4.tcp_rmem="4096 87380 16777216"
sysctl -w net.ipv4.tcp_wmem="4096 87380 16777216"
Set up monitoring to track server health:
#!/bin/bash
# backup.sh - Run via cron
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups/gameserver"
SERVER_DIR="/home/gameserver/server"
# Create backup
tar -czf $BACKUP_DIR/backup_$DATE.tar.gz -C $SERVER_DIR .
# Keep only last 7 days
find $BACKUP_DIR -name "backup_*.tar.gz" -mtime +7 -delete
# Minimal firewall - only allow necessary ports
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow [SERVER_PORT]/tcp
sudo ufw allow [SERVER_PORT]/udp
sudo ufw allow 22/tcp # SSH
sudo ufw enable
Last updated: ''' + datetime.now().strftime("%B %Y") + ''' | For ''' + game_name + ''' server hosting
''' return php_doc def process_incomplete_games(self): """Process all games with complete=false or generic text""" processed = 0 errors = [] skipped = 0 # Find all game folders for folder in self.docs_dir.iterdir(): if not folder.is_dir(): continue # Skip special folders if folder.name.startswith('.') or folder.name.startswith('_') or folder.name in ['common-issues', 'getting-started']: continue metadata_file = folder / 'metadata.json' index_file = folder / 'index.php' if not metadata_file.exists(): continue try: # Read metadata with open(metadata_file, 'r', encoding='utf-8') as f: content = f.read() # Remove BOM if present content = content.lstrip('\ufeff') metadata = json.loads(content) # Skip if already complete (unless it has generic text) is_complete = metadata.get('complete', False) has_generic_text = False if index_file.exists(): with open(index_file, 'r', encoding='utf-8') as f: index_content = f.read() if 'Check server configuration' in index_content or 'check your server configuration' in index_content.lower(): has_generic_text = True if is_complete and not has_generic_text: skipped += 1 continue print(f"Processing: {folder.name} (complete={is_complete}, has_generic={has_generic_text})") # Generate new documentation php_content = self.generate_php_doc(folder.name, metadata) # Write the new index.php with open(index_file, 'w', encoding='utf-8') as f: f.write(php_content) # Update metadata category from 'todo' to 'game' if needed if metadata.get('category', '').lower() == 'todo': metadata['category'] = 'game' # Mark as complete metadata['complete'] = True with open(metadata_file, 'w', encoding='utf-8') as f: json.dump(metadata, f, indent=4, ensure_ascii=False) processed += 1 print(f" ✓ Generated comprehensive documentation") except Exception as e: error_msg = f"Error processing {folder.name}: {e}" print(f" ✗ {error_msg}") errors.append(error_msg) return processed, skipped, errors def process_todo_folders(self): """Process all folders with category 'todo' """ processed = 0 errors = [] # Find all todo folders for folder in self.docs_dir.iterdir(): if not folder.is_dir(): continue metadata_file = folder / 'metadata.json' index_file = folder / 'index.php' if not metadata_file.exists(): continue try: # Read metadata with open(metadata_file, 'r', encoding='utf-8') as f: content = f.read() # Remove BOM if present content = content.lstrip('\ufeff') metadata = json.loads(content) # Check if it's a todo category if metadata.get('category', '').lower() != 'todo': continue print(f"Processing: {folder.name}") # Generate new documentation php_content = self.generate_php_doc(folder.name, metadata) # Write the new index.php with open(index_file, 'w', encoding='utf-8') as f: f.write(php_content) # Update metadata category from 'todo' to 'game' metadata['category'] = 'game' with open(metadata_file, 'w', encoding='utf-8') as f: json.dump(metadata, f, indent=4, ensure_ascii=False) processed += 1 print(f" ✓ Generated documentation for {folder.name}") except Exception as e: error_msg = f"Error processing {folder.name}: {e}" print(f" ✗ {error_msg}") errors.append(error_msg) return processed, errors def process_specific_games(self, game_names, force_overwrite=False): """Process specific game folders by name""" processed = 0 skipped = 0 errors = [] for game_name in game_names: folder = self.docs_dir / game_name if not folder.is_dir(): error_msg = f"Game folder not found: {game_name}" print(f" ✗ {error_msg}") errors.append(error_msg) continue metadata_file = folder / 'metadata.json' index_file = folder / 'index.php' if not metadata_file.exists(): error_msg = f"metadata.json not found for {game_name}" print(f" ✗ {error_msg}") errors.append(error_msg) continue try: # Read metadata with open(metadata_file, 'r', encoding='utf-8') as f: content = f.read() content = content.lstrip('\ufeff') metadata = json.loads(content) # Check if we should skip if index_file.exists() and not force_overwrite: print(f"Skipping {game_name} (file exists, use --force to overwrite)") skipped += 1 continue print(f"Processing: {game_name}") # Generate new documentation php_content = self.generate_php_doc(folder.name, metadata) # Write the new index.php with open(index_file, 'w', encoding='utf-8') as f: f.write(php_content) # Update metadata if it was todo if metadata.get('category', '').lower() == 'todo': metadata['category'] = 'game' # Mark as complete metadata['complete'] = True with open(metadata_file, 'w', encoding='utf-8') as f: json.dump(metadata, f, indent=4, ensure_ascii=False) processed += 1 print(f" ✓ Generated documentation for {game_name}") except Exception as e: error_msg = f"Error processing {game_name}: {e}" print(f" ✗ {error_msg}") errors.append(error_msg) return processed, skipped, errors def main(): # Parse command line arguments parser = argparse.ArgumentParser( description='Generate comprehensive game server documentation for GSP', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Generate docs for all incomplete games: python3 generate_game_docs.py # Generate docs for specific game(s): python3 generate_game_docs.py --games minecraft csgo rust # Regenerate docs (overwrite existing): python3 generate_game_docs.py --games minecraft --force # Process all "todo" category games: python3 generate_game_docs.py --todo-only """ ) parser.add_argument('--games', nargs='+', metavar='GAME', help='Generate docs for specific game folder names') parser.add_argument('--force', action='store_true', help='Overwrite existing documentation files') parser.add_argument('--todo-only', action='store_true', help='Only process games with category="todo"') args = parser.parse_args() docs_dir = "/home/runner/work/GSP/GSP/modules/billing/docs" config_dir = "/home/runner/work/GSP/GSP/modules/config_games/server_configs" knowledgepack = "/home/runner/work/GSP/GSP/modules/billing/docs/gameserver_knowledgepack_v2.yaml" generator = GameDocGenerator(docs_dir, config_dir, knowledgepack) print("="*70) print("COMPREHENSIVE GAME SERVER DOCUMENTATION GENERATOR") print("="*70) print() print("Loading data sources...") generator.load_knowledgepack() generator.load_xml_configs() processed = 0 skipped = 0 errors = [] # Determine which mode to run if args.games: # Process specific games print("\n" + "="*70) print(f"Processing {len(args.games)} specified game(s)...") if args.force: print("FORCE mode: Will overwrite existing files") print("="*70) processed, skipped, errors = generator.process_specific_games(args.games, args.force) elif args.todo_only: # Process only todo category games print("\n" + "="*70) print("Processing TODO category games...") print("="*70) processed, errors = generator.process_todo_folders() else: # Process incomplete games (default) print("\n" + "="*70) print("Processing INCOMPLETE game documentation...") print("="*70) processed, skipped, errors = generator.process_incomplete_games() print(f"\n{'='*70}") print(f"Documentation generation complete!") print(f" ✓ Processed: {processed}") if skipped > 0: print(f" → Skipped: {skipped}") print(f" ✗ Errors: {len(errors)}") if errors: print("\nErrors encountered:") for error in errors[:10]: # Show first 10 errors print(f" - {error}") if len(errors) > 10: print(f" ... and {len(errors) - 10} more") print(f"\nGenerated documentation includes:") print(f" • Actual port information from XML configs") print(f" • Steam App IDs and exact SteamCMD commands") print(f" • Configuration file details from XML configs") print(f" • DETAILED startup parameters extracted from XML") print(f" • Comprehensive troubleshooting sections") print("="*70) return 0 if not errors else 1 if __name__ == "__main__": sys.exit(main())