Moved the Agents into their own repo. Kept the agent.pl just for reference

This commit is contained in:
Frank Harris 2025-09-11 13:27:32 -04:00
parent 22381be29a
commit 8680a02b13
18132 changed files with 0 additions and 2569420 deletions

View file

@ -1,49 +0,0 @@
#!/bin/bash
# Comprehensive Server Admin Guide Generation Workflow
# Automates the complete guide generation and validation process
set -e
echo "=== Comprehensive Server Admin Guide Generation ==="
echo "Starting workflow at $(date)"
echo
# Step 1: Generate all guides
echo "1. Generating Markdown guides and PDFs..."
python3 tools/generate_server_guides.py
echo
# Step 2: Validate output
echo "2. Validating generated guides..."
python3 tools/validate_guides.py
echo
# Step 3: Statistics
echo "3. Generation Statistics:"
echo " - Markdown guides: $(find docs/games -name 'index.md' | wc -l)"
echo " - PDF files: $(find dist/pdfs -name '*.pdf' | wc -l)"
echo " - Total file size: $(du -sh dist/pdfs/ | cut -f1)"
echo
# Step 4: Quality check
echo "4. Quality Check:"
WARNINGS=$(python3 tools/validate_guides.py 2>&1 | grep -c "⚠️" || true)
ERRORS=$(python3 tools/validate_guides.py 2>&1 | grep -c "❌" || true)
echo " - Validation warnings: $WARNINGS"
echo " - Validation errors: $ERRORS"
echo
if [ "$ERRORS" -eq 0 ]; then
echo "✅ All guides generated successfully!"
echo "📁 Guides available at: docs/games/"
echo "📄 PDFs available at: dist/pdfs/"
echo "📋 Index page: docs/games/_index.md"
echo "📊 Manifest: dist/pdfs/manifest.json"
else
echo "❌ Guide generation completed with errors. Please review validation output."
exit 1
fi
echo
echo "=== Workflow completed at $(date) ==="

View file

@ -1,292 +0,0 @@
#!/usr/bin/env python3
"""
FAQ RSS Generator
Generates FAQ.RSS from YAML game documentation files.
Produces clean HTML-escaped content without CDATA, using <br> for line breaks.
"""
import os
import sys
import yaml
import html
import xml.etree.ElementTree as ET
from datetime import datetime
import shutil
from pathlib import Path
class FAQRSSGenerator:
def __init__(self, data_dir="data/games", output_file="FAQ.RSS"):
self.data_dir = Path(data_dir)
self.output_file = Path(output_file)
self.games = []
def load_games(self):
"""Load all YAML game files from data directory"""
self.games = []
if not self.data_dir.exists():
print(f"Error: Data directory {self.data_dir} does not exist")
return False
yaml_files = list(self.data_dir.glob("*.yml")) + list(self.data_dir.glob("*.yaml"))
if not yaml_files:
print(f"Warning: No YAML files found in {self.data_dir}")
return True
for yaml_file in yaml_files:
try:
with open(yaml_file, 'r', encoding='utf-8') as f:
game_data = yaml.safe_load(f)
if self.validate_game_data(game_data, yaml_file):
self.games.append(game_data)
print(f"Loaded: {game_data['name']}")
except Exception as e:
print(f"Error loading {yaml_file}: {e}")
return True
def validate_game_data(self, data, filename):
"""Validate game YAML data structure"""
required_fields = ['name', 'supports_workshop', 'startup', 'configs', 'troubleshooting']
for field in required_fields:
if field not in data:
print(f"Error in {filename}: Missing required field '{field}'")
return False
# Validate startup section
startup = data['startup']
if 'default_command' not in startup:
print(f"Error in {filename}: Missing 'default_command' in startup section")
return False
# Validate workshop section if supported
if data['supports_workshop'] and 'workshop' not in data:
print(f"Error in {filename}: supports_workshop is true but 'workshop' section missing")
return False
return True
def escape_html_content(self, text):
"""Escape HTML content and convert newlines to <br> tags"""
if not text:
return ""
# Escape HTML entities
escaped = html.escape(str(text))
# Convert newlines to <br> tags
escaped = escaped.replace('\n', '&lt;br&gt;')
return escaped
def generate_config_files_content(self, game):
"""Generate Config Files section content"""
content = "&lt;strong&gt;Configuration Files&lt;/strong&gt;&lt;br&gt;"
for config in game['configs']:
file_name = config['file']
paths = config.get('paths', [])
desc = config.get('desc', '')
content += "&lt;br&gt;- " + self.escape_html_content(file_name)
if paths:
path_str = ", ".join(paths)
content += "" + self.escape_html_content(desc) + ". Paths: " + self.escape_html_content(path_str)
elif desc:
content += "" + self.escape_html_content(desc)
content += "&lt;br&gt;"
return content
def generate_startup_parameters_content(self, game):
"""Generate Startup Parameters section content"""
startup = game['startup']
content = "&lt;strong&gt;Default Command Line&lt;/strong&gt;&lt;br&gt;"
content += "&lt;br&gt;" + self.escape_html_content(startup['default_command']) + "&lt;br&gt;"
# Port scheme
if 'ports' in startup and startup['ports']:
content += "&lt;br&gt;&lt;strong&gt;Port Scheme&lt;/strong&gt;&lt;br&gt;"
for port in startup['ports']:
label = port.get('label', '')
port_num = port.get('port', '')
proto = port.get('proto', '')
relative = port.get('relative', '')
content += "&lt;br&gt;- " + self.escape_html_content(f"{label} ({proto}) — {relative} (default {port_num})") + "&lt;br&gt;"
# Command line flags
if 'flags' in startup and startup['flags']:
content += "&lt;br&gt;&lt;strong&gt;Command Line Flags&lt;/strong&gt;&lt;br&gt;"
for flag in startup['flags']:
flag_name = flag.get('flag', '')
default = flag.get('default', '')
flag_type = flag.get('type', '')
desc = flag.get('desc', '')
content += "&lt;br&gt;" + self.escape_html_content(f"{flag_name}{desc}. Type: {flag_type}, Default: {default}") + "&lt;br&gt;"
return content
def generate_troubleshooting_content(self, game):
"""Generate Troubleshooting section content"""
content = "&lt;strong&gt;Common Issues and Solutions&lt;/strong&gt;&lt;br&gt;"
for issue in game['troubleshooting']:
content += "&lt;br&gt;- " + self.escape_html_content(issue) + "&lt;br&gt;"
return content
def generate_workshop_content(self, game):
"""Generate Steam Workshop section content"""
if not game.get('supports_workshop', False):
return None
workshop = game.get('workshop', {})
content = "&lt;strong&gt;Steam Workshop Configuration&lt;/strong&gt;&lt;br&gt;"
notes = workshop.get('notes', [])
for note in notes:
content += "&lt;br&gt;- " + self.escape_html_content(note) + "&lt;br&gt;"
return content
def create_rss_item(self, title, category, content):
"""Create RSS item element"""
item = ET.Element('item')
title_elem = ET.SubElement(item, 'title')
title_elem.text = title
category_elem = ET.SubElement(item, 'category')
category_elem.text = category
# Handle namespaced element properly
content_elem = ET.SubElement(item, '{http://purl.org/rss/1.0/modules/content/}encoded')
content_elem.text = content
return item
def generate_rss(self):
"""Generate the complete RSS file"""
# Create backup if file exists
if self.output_file.exists():
backup_file = self.output_file.with_suffix('.bak')
shutil.copy2(self.output_file, backup_file)
print(f"Created backup: {backup_file}")
# Create RSS root
rss = ET.Element('rss', version='2.0')
rss.set('xmlns:content', 'http://purl.org/rss/1.0/modules/content/')
rss.set('xmlns:dc', 'http://purl.org/dc/elements/1.1/')
channel = ET.SubElement(rss, 'channel')
# Channel metadata
title = ET.SubElement(channel, 'title')
title.text = 'Game Server FAQ'
link = ET.SubElement(channel, 'link')
link.text = 'https://gameservers.world/faq'
description = ET.SubElement(channel, 'description')
description.text = 'Comprehensive game server configuration and troubleshooting guide'
language = ET.SubElement(channel, 'dc:language')
language.text = 'en'
pubdate = ET.SubElement(channel, 'pubDate')
# Fix datetime deprecation warning
pubdate.text = datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT')
# Sort games alphabetically by name, then items by section order
sorted_games = sorted(self.games, key=lambda x: x['name'])
# Define section order
section_order = ['Config Files', 'Startup Parameters', 'Troubleshooting', 'Steam Workshop']
# Generate items for each game
all_items = []
for game in sorted_games:
game_name = game['name']
# Required sections in order
sections = [
('Config Files', self.generate_config_files_content(game)),
('Startup Parameters', self.generate_startup_parameters_content(game)),
('Troubleshooting', self.generate_troubleshooting_content(game))
]
# Optional workshop section
workshop_content = self.generate_workshop_content(game)
if workshop_content:
sections.append(('Steam Workshop', workshop_content))
# Create RSS items
for title, content in sections:
item = self.create_rss_item(title, game_name, content)
all_items.append((game_name, title, item))
# Sort all items by category (game name) then by section order
all_items.sort(key=lambda x: (x[0], section_order.index(x[1]) if x[1] in section_order else 999))
# Add all items to channel (no longer needed since we're writing manually)
# for game_name, title, item in all_items:
# channel.append(item)
# Write RSS file manually to avoid namespace issues
with open(self.output_file, 'w', encoding='utf-8') as f:
f.write('<?xml version="1.0" encoding="utf-8" ?>\n')
f.write('<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">\n')
f.write(' <channel>\n')
f.write(' <title>Game Server FAQ</title>\n')
f.write(' <link>https://gameservers.world/faq</link>\n')
f.write(' <description>Comprehensive game server configuration and troubleshooting guide</description>\n')
f.write(' <dc:language>en</dc:language>\n')
f.write(f' <pubDate>{datetime.now().strftime("%a, %d %b %Y %H:%M:%S GMT")}</pubDate>\n')
# Write all items
for game_name, title, item in all_items:
title_text = item.find('title').text
category_text = item.find('category').text
content_elem = item.find('{http://purl.org/rss/1.0/modules/content/}encoded')
content = content_elem.text if content_elem is not None else ""
f.write(' <item>\n')
f.write(f' <title>{html.escape(title_text)}</title>\n')
f.write(f' <category>{html.escape(category_text)}</category>\n')
f.write(f' <content:encoded>{content}</content:encoded>\n')
f.write(' </item>\n')
f.write(' </channel>\n')
f.write('</rss>\n')
print(f"Generated RSS with {len(all_items)} items for {len(sorted_games)} games")
print(f"Output: {self.output_file}")
def run(self):
"""Main execution method"""
print("FAQ RSS Generator")
print("================")
if not self.load_games():
return False
if not self.games:
print("No valid games found. RSS file will be empty.")
self.generate_rss()
return True
def main():
if len(sys.argv) > 1:
data_dir = sys.argv[1]
else:
data_dir = "data/games"
if len(sys.argv) > 2:
output_file = sys.argv[2]
else:
output_file = "FAQ.RSS"
generator = FAQRSSGenerator(data_dir, output_file)
success = generator.run()
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()

View file

@ -1,581 +0,0 @@
#!/usr/bin/env python3
"""
Comprehensive Server Admin Guide Generator
Generates exhaustive server admin guides in Markdown and PDF format from YAML game data.
Each guide includes complete startup parameters, config files, port mapping, Steam Workshop
integration, management procedures, and deep troubleshooting.
Outputs:
- ./docs/games/<slug>/index.md - Markdown guide for each game
- ./dist/pdfs/<slug>__Server_Admin_Guide_v1.pdf - PDF version
- ./docs/games/_index.md - Index of all games
- ./dist/pdfs/manifest.json - Machine-readable manifest
"""
import os
import sys
import yaml
import json
import subprocess
from pathlib import Path
from datetime import datetime
import html
import re
class ServerGuideGenerator:
def __init__(self, data_dir="data/games", docs_dir="docs/games", pdfs_dir="dist/pdfs"):
self.data_dir = Path(data_dir)
self.docs_dir = Path(docs_dir)
self.pdfs_dir = Path(pdfs_dir)
self.games = []
self.manifest = []
# Ensure output directories exist
self.docs_dir.mkdir(parents=True, exist_ok=True)
self.pdfs_dir.mkdir(parents=True, exist_ok=True)
def load_games(self):
"""Load all YAML game files from data directory"""
self.games = []
if not self.data_dir.exists():
print(f"Error: Data directory {self.data_dir} does not exist")
return False
yaml_files = list(self.data_dir.glob("*.yml")) + list(self.data_dir.glob("*.yaml"))
if not yaml_files:
print(f"Warning: No YAML files found in {self.data_dir}")
return True
for yaml_file in yaml_files:
try:
with open(yaml_file, 'r', encoding='utf-8') as f:
game_data = yaml.safe_load(f)
if self.validate_game_data(game_data, yaml_file):
# Add metadata
game_data['_slug'] = self.create_slug(game_data['name'])
game_data['_source_file'] = yaml_file.name
self.games.append(game_data)
print(f"Loaded: {game_data['name']}")
except Exception as e:
print(f"Error loading {yaml_file}: {e}")
# Sort games alphabetically by name
self.games.sort(key=lambda x: x['name'])
return True
def validate_game_data(self, data, filename):
"""Validate game YAML data structure"""
required_fields = ['name', 'supports_workshop', 'startup', 'configs', 'troubleshooting']
for field in required_fields:
if field not in data:
print(f"Error in {filename}: Missing required field '{field}'")
return False
# Validate startup section
if 'default_command' not in data['startup']:
print(f"Error in {filename}: Missing 'default_command' in startup section")
return False
if 'ports' not in data['startup'] or not isinstance(data['startup']['ports'], list):
print(f"Error in {filename}: Missing or invalid 'ports' in startup section")
return False
if 'flags' not in data['startup'] or not isinstance(data['startup']['flags'], list):
print(f"Error in {filename}: Missing or invalid 'flags' in startup section")
return False
return True
def create_slug(self, name):
"""Create URL-friendly slug from game name"""
# Convert to lowercase, replace spaces and special chars with hyphens
slug = re.sub(r'[^\w\s-]', '', name).strip().lower()
slug = re.sub(r'[-\s]+', '-', slug)
return slug
def extract_app_id(self, game_data):
"""Extract Steam App ID from game data or external sources"""
# Check if appid is in the data
if 'appid' in game_data:
return str(game_data['appid'])
# Try to extract from common patterns in startup commands
startup_cmd = game_data.get('startup', {}).get('default_command', '')
# Common AppID mappings based on game names
appid_mapping = {
'7 Days to Die': '294420',
'ARK: Survival Evolved': '376030',
'ARMA 3': '107410',
'ARMA 2: Operation Arrowhead': '33930',
'Counter-Strike: Global Offensive': '730',
'DayZ': '221100',
"Garry's Mod": '4000',
'Rust': '258550',
'Squad': '393380',
'Team Fortress 2': '440',
'Terraria': '105600',
'Unturned': '304930',
'Valheim': '892970'
}
return appid_mapping.get(game_data['name'], 'N/A')
def detect_engine(self, game_data):
"""Detect game engine from game data"""
# Check if engine is in the data
if 'engine' in game_data:
return game_data['engine']
name = game_data['name']
# Engine mapping based on known games
engine_mapping = {
'7 Days to Die': 'Unity',
'ARK: Survival Evolved': 'Unreal Engine 4',
'ARMA 3': 'Real Virtuality 4',
'ARMA 2: Operation Arrowhead': 'Real Virtuality 3',
'Counter-Strike: Global Offensive': 'Source Engine',
'DayZ': 'Enfusion',
"Garry's Mod": 'Source Engine',
'Minecraft': 'Java',
'Rust': 'Unity',
'Squad': 'Unreal Engine 4',
'Team Fortress 2': 'Source Engine',
'Terraria': 'XNA/MonoGame',
'Unturned': 'Unity',
'Valheim': 'Unity'
}
return engine_mapping.get(name, 'Unknown')
def generate_port_table(self, game_data):
"""Generate comprehensive port mapping table"""
ports = game_data.get('startup', {}).get('ports', [])
table = "| Feature | Port | Protocol | Relation | Notes |\n"
table += "|---|---:|---|---|---|\n"
for port_info in ports:
label = port_info.get('label', 'Unknown')
port = port_info.get('port', 'N/A')
proto = port_info.get('proto', 'UDP').upper()
relative = port_info.get('relative', 'Unknown')
notes = port_info.get('notes', '')
table += f"| {label} | {port} | {proto} | {relative} | {notes} |\n"
return table
def generate_startup_flags_table(self, game_data):
"""Generate comprehensive startup parameters table"""
flags = game_data.get('startup', {}).get('flags', [])
if len(flags) < 10:
print(f"Warning: {game_data['name']} has only {len(flags)} startup flags (minimum 10 recommended)")
table = "| Flag/Param | Default | Type/Range | Description | Example |\n"
table += "|---|---|---|---|---|\n"
for flag_info in flags:
flag = flag_info.get('flag', '')
default = flag_info.get('default', '')
flag_type = flag_info.get('type', 'string')
desc = flag_info.get('desc', '')
example = flag_info.get('example', f"{flag} {default}")
table += f"| {flag} | {default} | {flag_type} | {desc} | {example} |\n"
return table
def generate_config_files_section(self, game_data):
"""Generate configuration files section"""
configs = game_data.get('configs', [])
if len(configs) < 8:
print(f"Warning: {game_data['name']} has only {len(configs)} config entries (minimum 8 recommended)")
content = ""
for config in configs:
file_name = config.get('file', 'Unknown')
paths = config.get('paths', [])
desc = config.get('desc', '')
content += f"### {file_name}\n"
content += f"**Purpose:** {desc}\n\n"
content += "**Paths:**\n"
for path in paths:
content += f"- Linux: `{path}`\n"
# Add Windows path variant if different
if '/' in path:
win_path = path.replace('/', '\\')
content += f"- Windows: `{win_path}`\n"
content += "\n"
return content
def generate_workshop_section(self, game_data):
"""Generate Steam Workshop section"""
if not game_data.get('supports_workshop', False):
return "## Steam Workshop: Not Supported\n\nThis game does not support Steam Workshop integration.\n\n"
workshop_data = game_data.get('workshop', {})
notes = workshop_data.get('notes', [])
content = "## Steam Workshop\n\n"
if notes:
for note in notes:
content += f"- {note}\n"
else:
content += "**Note:** This game supports Steam Workshop but specific configuration details need to be added.\n"
content += "\n"
return content
def generate_troubleshooting_section(self, game_data):
"""Generate troubleshooting section"""
issues = game_data.get('troubleshooting', [])
content = "## Troubleshooting (Deep)\n\n"
content += "### Common Issues and Solutions\n\n"
for issue in issues:
# Split issue and solution on em dash
if '' in issue:
problem, solution = issue.split('', 1)
content += f"**{problem}**\n\n{solution}\n\n"
else:
content += f"- {issue}\n\n"
return content
def generate_markdown_guide(self, game_data):
"""Generate complete Markdown guide for a game"""
name = game_data['name']
slug = game_data['_slug']
appid = self.extract_app_id(game_data)
engine = self.detect_engine(game_data)
workshop_support = "Yes" if game_data.get('supports_workshop', False) else "No"
today = datetime.now().strftime('%Y-%m-%d')
# Extract default game port for the quick start section
ports = game_data.get('startup', {}).get('ports', [])
game_port = "27015" # Default fallback
for port in ports:
if 'game' in port.get('label', '').lower():
game_port = str(port.get('port', game_port))
break
markdown = f"""# {name} — Dedicated Server Admin Guide
- **Engine:** {engine} **AppID:** {appid} **Workshop:** {workshop_support} **LinuxGSM support:** Yes **OGP module:** Yes
- **Last updated:** {today}
- **Supported OS:** Windows/Linux
## Quick Start (Host Assigned Game Port = {game_port})
### Default Command Line
```bash
{game_data.get('startup', {}).get('default_command', 'N/A')}
```
### OGP Panel Setup
1. Create new server instance in OGP control panel
2. Select "{name}" from game list
3. Configure startup parameters:
- Game Port: {game_port}
- Server Name: [Your Server Name]
- Password: [Optional]
4. Set resource limits (RAM, CPU, disk space)
5. Start server and monitor console output
### LinuxGSM Installation
```bash
# Install LinuxGSM for {name}
./gameserver install
./gameserver start
./gameserver details
```
### First-Run Checklist
- [ ] Accept EULA (if required)
- [ ] Configure server token/API key (if required)
- [ ] Open firewall ports (see port map below)
- [ ] Set admin credentials
- [ ] Test connectivity from external client
## Full Port Map (relative to Game Port where applicable)
{self.generate_port_table(game_data)}
## Startup Parameters (EXHAUSTIVE)
### Command Structure
```bash
{game_data.get('startup', {}).get('default_command', 'N/A')}
```
### All Supported Flags
{self.generate_startup_flags_table(game_data)}
### Example Configurations
**Minimal (Testing):**
```bash
# Basic startup for testing
{game_data.get('startup', {}).get('default_command', 'N/A')}
```
**Production (Recommended):**
```bash
# Production server with common optimizations
{game_data.get('startup', {}).get('default_command', 'N/A')} +sv_setsteamaccount YOUR_GSLT +rcon_password YOUR_RCON_PASS +hostname "Your Server Name"
```
**High-Performance (Competitive):**
```bash
# High-performance setup for competitive play
{game_data.get('startup', {}).get('default_command', 'N/A')} -tickrate 128 -threads 4 +fps_max 300 +sv_setsteamaccount YOUR_GSLT
```
## Configuration Files & Paths (ALL)
{self.generate_config_files_section(game_data)}
{self.generate_workshop_section(game_data)}
## Player & Server Management
### RCON/Console Commands
- `status` - Show server status and connected players
- `kick <player>` - Kick a player
- `ban <player>` - Ban a player
- `exec <config>` - Execute configuration file
- `restart` - Restart the server
- `quit` - Shutdown the server
### Admin Configuration
- **Admin files:** Check config files section above for admin definitions
- **Permissions:** Refer to framework documentation (SourceMod, Oxide, etc.)
- **Reserved slots:** Configure in server configuration files
### Backup Procedures
1. **Hot Backup:** Use server commands to save state before copying files
2. **Cold Backup:** Stop server, copy save/world directories, restart
3. **Automated:** Set up cron/scheduled tasks for regular backups
4. **Restore:** Stop server, restore files, verify integrity, restart
### Update Management
- **Manual:** Download updates via SteamCMD or game launcher
- **Automatic:** Enable auto-update flags in startup parameters
- **Validation:** Use `steamcmd +app_update {appid} validate` to verify files
- **Rollback:** Keep previous version backups for quick rollback
### Performance Tuning
- **CPU:** Adjust thread count and affinity settings
- **Memory:** Monitor RAM usage, set appropriate limits
- **Network:** Tune tick rate and bandwidth settings
- **Storage:** Use SSD for world/save files, regular HDD for logs
{self.generate_troubleshooting_section(game_data)}
### Performance Issues
- **High CPU:** Reduce player count, optimize world settings, check for infinite loops
- **Memory leaks:** Monitor process memory, restart periodically, check for mod issues
- **Network lag:** Verify bandwidth, check for packet loss, tune network settings
- **Disk I/O:** Move to faster storage, optimize save intervals
### Anti-Cheat Integration
- **VAC:** Ensure valid Steam token, avoid -insecure flag
- **EAC/BattlEye:** Keep anti-cheat files updated, check for conflicts
- **Custom:** Configure mod-based anti-cheat systems properly
## Appendices
### Complete Server Variable Reference
*Note: Refer to official documentation for complete cvar/setting lists specific to this game.*
### Change Log
- **v1.0** ({today}): Initial comprehensive guide creation
### Source References
- Official dedicated server documentation
- Steam Dedicated Server pages
- LinuxGSM game-specific guides
- OGP module documentation
- Community admin guides and forums
---
*This guide is part of the Gameservers.World comprehensive server administration documentation project.*
"""
return markdown
def generate_pdf(self, markdown_file, output_pdf):
"""Convert Markdown to PDF using Pandoc"""
try:
cmd = [
'pandoc',
str(markdown_file),
'-o', str(output_pdf),
'--from=gfm',
'--toc',
'--toc-depth=3',
f'--metadata=title:{markdown_file.parent.name.replace("-", " ").title()} Server Admin Guide',
'--pdf-engine=xelatex'
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print(f"Generated PDF: {output_pdf}")
return True
else:
print(f"Error generating PDF for {markdown_file}: {result.stderr}")
return False
except Exception as e:
print(f"Exception generating PDF for {markdown_file}: {e}")
return False
def generate_index_page(self):
"""Generate main index page listing all games"""
content = f"""# Game Server Admin Guides
Complete server administration documentation for all supported games.
**Last updated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
## Available Guides
| Game | Engine | Workshop | AppID | Documentation | PDF Guide |
|---|---|---|---|---|---|
"""
for game in self.games:
name = game['name']
slug = game['_slug']
engine = self.detect_engine(game)
workshop = "" if game.get('supports_workshop', False) else ""
appid = self.extract_app_id(game)
md_link = f"[Documentation](./{slug}/index.md)"
pdf_link = f"[PDF](../dist/pdfs/{slug}__Server_Admin_Guide_v1.pdf)"
content += f"| {name} | {engine} | {workshop} | {appid} | {md_link} | {pdf_link} |\n"
content += f"""
## Statistics
- **Total games:** {len(self.games)}
- **Workshop supported:** {sum(1 for g in self.games if g.get('supports_workshop', False))}
- **Engines covered:** {len(set(self.detect_engine(g) for g in self.games))}
## Usage
Each guide includes:
- Complete startup parameter reference
- All configuration files and paths
- Port mapping and networking
- Steam Workshop integration (where supported)
- Management and administration procedures
- Deep troubleshooting and diagnostics
## Contributing
To add or update game documentation:
1. Edit the corresponding YAML file in `data/games/`
2. Run the guide generator: `python3 tools/generate_server_guides.py`
3. Review generated Markdown and PDF outputs
4. Submit pull request with changes
---
*Generated by Gameservers.World comprehensive server admin guide system*
"""
index_file = self.docs_dir / '_index.md'
with open(index_file, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Generated index: {index_file}")
def generate_manifest(self):
"""Generate machine-readable manifest"""
manifest = {
"generated": datetime.now().isoformat(),
"total_games": len(self.games),
"games": []
}
for game in self.games:
game_info = {
"title": game['name'],
"slug": game['_slug'],
"appid": self.extract_app_id(game),
"engine": self.detect_engine(game),
"workshop_support": game.get('supports_workshop', False),
"ports": game.get('startup', {}).get('ports', []),
"config_files": [c.get('file', '') for c in game.get('configs', [])],
"last_updated": datetime.now().strftime('%Y-%m-%d'),
"markdown_path": f"docs/games/{game['_slug']}/index.md",
"pdf_path": f"dist/pdfs/{game['_slug']}__Server_Admin_Guide_v1.pdf"
}
manifest["games"].append(game_info)
manifest_file = self.pdfs_dir / 'manifest.json'
with open(manifest_file, 'w', encoding='utf-8') as f:
json.dump(manifest, f, indent=2)
print(f"Generated manifest: {manifest_file}")
def generate_all_guides(self):
"""Generate all guides, PDFs, index, and manifest"""
if not self.load_games():
return False
print(f"Generating guides for {len(self.games)} games...")
generated_pdfs = 0
failed_pdfs = []
for game in self.games:
slug = game['_slug']
print(f"\nProcessing: {game['name']} ({slug})")
# Create game directory
game_dir = self.docs_dir / slug
game_dir.mkdir(exist_ok=True)
# Generate Markdown guide
markdown_content = self.generate_markdown_guide(game)
markdown_file = game_dir / 'index.md'
with open(markdown_file, 'w', encoding='utf-8') as f:
f.write(markdown_content)
print(f"Generated: {markdown_file}")
# Generate PDF
pdf_file = self.pdfs_dir / f"{slug}__Server_Admin_Guide_v1.pdf"
if self.generate_pdf(markdown_file, pdf_file):
generated_pdfs += 1
else:
failed_pdfs.append(slug)
# Generate index and manifest
self.generate_index_page()
self.generate_manifest()
print(f"\n=== Generation Complete ===")
print(f"Games processed: {len(self.games)}")
print(f"PDFs generated: {generated_pdfs}")
print(f"PDF failures: {len(failed_pdfs)}")
if failed_pdfs:
print(f"Failed PDFs: {', '.join(failed_pdfs)}")
return True
if __name__ == "__main__":
generator = ServerGuideGenerator()
success = generator.generate_all_guides()
sys.exit(0 if success else 1)

View file

@ -1,259 +0,0 @@
#!/usr/bin/env python3
"""
Server Admin Guide Validator
Validates generated server admin guides against quality gates and requirements.
Ensures guides meet the "exhaustive" standard specified in the requirements.
"""
import os
import sys
import yaml
import json
from pathlib import Path
import re
class GuideValidator:
def __init__(self, docs_dir="docs/games", pdfs_dir="dist/pdfs", data_dir="data/games"):
self.docs_dir = Path(docs_dir)
self.pdfs_dir = Path(pdfs_dir)
self.data_dir = Path(data_dir)
self.errors = []
self.warnings = []
def validate_all(self):
"""Run all validation checks"""
print("=== Server Admin Guide Validation ===\n")
self.validate_directory_structure()
self.validate_yaml_data()
self.validate_markdown_guides()
self.validate_pdf_files()
self.validate_manifest()
self.validate_index_page()
return self.print_results()
def validate_directory_structure(self):
"""Validate required directories exist"""
print("Checking directory structure...")
required_dirs = [self.docs_dir, self.pdfs_dir, self.data_dir]
for directory in required_dirs:
if not directory.exists():
self.errors.append(f"Required directory missing: {directory}")
required_files = [
self.docs_dir / "_index.md",
self.pdfs_dir / "manifest.json"
]
for file_path in required_files:
if not file_path.exists():
self.errors.append(f"Required file missing: {file_path}")
print("✓ Directory structure validated\n")
def validate_yaml_data(self):
"""Validate YAML game data meets exhaustive requirements"""
print("Checking YAML game data...")
yaml_files = list(self.data_dir.glob("*.yml")) + list(self.data_dir.glob("*.yaml"))
for yaml_file in yaml_files:
try:
with open(yaml_file, 'r', encoding='utf-8') as f:
game_data = yaml.safe_load(f)
self.validate_single_game_yaml(game_data, yaml_file)
except Exception as e:
self.errors.append(f"Error reading {yaml_file}: {e}")
print("✓ YAML data validated\n")
def validate_single_game_yaml(self, game_data, filename):
"""Validate individual game YAML meets requirements"""
game_name = game_data.get('name', 'Unknown')
# Check required sections
required_sections = ['name', 'supports_workshop', 'startup', 'configs', 'troubleshooting']
for section in required_sections:
if section not in game_data:
self.errors.append(f"{filename}: Missing required section '{section}'")
# Validate startup parameters (minimum 10 flags)
flags = game_data.get('startup', {}).get('flags', [])
if len(flags) < 10:
self.warnings.append(f"{game_name}: Only {len(flags)} startup flags (minimum 10 recommended)")
# Validate config files (minimum 8 entries)
configs = game_data.get('configs', [])
if len(configs) < 8:
self.warnings.append(f"{game_name}: Only {len(configs)} config entries (minimum 8 recommended)")
# Validate port mapping
ports = game_data.get('startup', {}).get('ports', [])
if not ports:
self.errors.append(f"{game_name}: No port mapping defined")
else:
for port in ports:
required_port_fields = ['label', 'port', 'proto', 'relative']
for field in required_port_fields:
if field not in port:
self.errors.append(f"{game_name}: Port entry missing '{field}' field")
def validate_markdown_guides(self):
"""Validate generated Markdown guides"""
print("Checking Markdown guides...")
required_sections = [
"Quick Start",
"Full Port Map",
"Startup Parameters \\(EXHAUSTIVE\\)",
"Configuration Files & Paths \\(ALL\\)",
"Steam Workshop",
"Player & Server Management",
"Troubleshooting \\(Deep\\)",
"Appendices"
]
game_dirs = [d for d in self.docs_dir.iterdir() if d.is_dir()]
for game_dir in game_dirs:
md_file = game_dir / "index.md"
if not md_file.exists():
self.errors.append(f"Missing Markdown file: {md_file}")
continue
with open(md_file, 'r', encoding='utf-8') as f:
content = f.read()
# Check for all required sections
for section in required_sections:
pattern = f"## {section}"
if not re.search(pattern, content):
self.errors.append(f"{md_file}: Missing required section '{section}'")
# Check for startup parameters table
if "| Flag/Param | Default | Type/Range | Description | Example |" not in content:
self.errors.append(f"{md_file}: Missing startup parameters table")
# Check for port mapping table
if "| Feature | Port | Protocol | Relation | Notes |" not in content:
self.errors.append(f"{md_file}: Missing port mapping table")
# Check for no TBD or placeholder content
placeholders = ["TBD", "TODO", "coming soon", "placeholder"]
for placeholder in placeholders:
if placeholder.lower() in content.lower():
self.warnings.append(f"{md_file}: Contains placeholder text '{placeholder}'")
print("✓ Markdown guides validated\n")
def validate_pdf_files(self):
"""Validate PDF files exist and have reasonable size"""
print("Checking PDF files...")
game_dirs = [d for d in self.docs_dir.iterdir() if d.is_dir()]
for game_dir in game_dirs:
slug = game_dir.name
pdf_file = self.pdfs_dir / f"{slug}__Server_Admin_Guide_v1.pdf"
if not pdf_file.exists():
self.errors.append(f"Missing PDF file: {pdf_file}")
continue
# Check file size (should be at least 20KB for a comprehensive guide)
file_size = pdf_file.stat().st_size
if file_size < 20480: # 20KB
self.warnings.append(f"{pdf_file}: Small file size ({file_size} bytes) - may indicate incomplete content")
print("✓ PDF files validated\n")
def validate_manifest(self):
"""Validate manifest.json structure and content"""
print("Checking manifest...")
manifest_file = self.pdfs_dir / "manifest.json"
if not manifest_file.exists():
self.errors.append("Missing manifest.json file")
return
try:
with open(manifest_file, 'r', encoding='utf-8') as f:
manifest = json.load(f)
# Check required fields
required_fields = ["generated", "total_games", "games"]
for field in required_fields:
if field not in manifest:
self.errors.append(f"Manifest missing required field: {field}")
# Validate games entries
if "games" in manifest:
for game in manifest["games"]:
required_game_fields = ["title", "slug", "appid", "engine", "workshop_support", "ports", "config_files", "last_updated", "markdown_path", "pdf_path"]
for field in required_game_fields:
if field not in game:
self.errors.append(f"Manifest game entry missing field: {field}")
except json.JSONDecodeError as e:
self.errors.append(f"Invalid JSON in manifest: {e}")
print("✓ Manifest validated\n")
def validate_index_page(self):
"""Validate index page content"""
print("Checking index page...")
index_file = self.docs_dir / "_index.md"
if not index_file.exists():
self.errors.append("Missing index page")
return
with open(index_file, 'r', encoding='utf-8') as f:
content = f.read()
# Check for required sections
required_content = [
"# Game Server Admin Guides",
"## Available Guides",
"| Game | Engine | Workshop | AppID | Documentation | PDF Guide |",
"## Statistics"
]
for required in required_content:
if required not in content:
self.errors.append(f"Index page missing required content: {required}")
print("✓ Index page validated\n")
def print_results(self):
"""Print validation results"""
print("=== Validation Results ===\n")
if self.errors:
print(f"❌ ERRORS ({len(self.errors)}):")
for error in self.errors:
print(f"{error}")
print()
if self.warnings:
print(f"⚠️ WARNINGS ({len(self.warnings)}):")
for warning in self.warnings:
print(f"{warning}")
print()
if not self.errors and not self.warnings:
print("✅ All validation checks passed!")
elif not self.errors:
print("✅ No critical errors found (warnings can be addressed)")
else:
print(f"❌ Validation failed with {len(self.errors)} errors")
return len(self.errors) == 0
if __name__ == "__main__":
validator = GuideValidator()
success = validator.validate_all()
sys.exit(0 if success else 1)

View file

@ -1,260 +0,0 @@
#!/usr/bin/env python3
"""
FAQ RSS Validator
Validates FAQ.RSS for well-formed XML and site-specific constraints.
Checks for no CDATA, allowed title values, and proper HTML escaping.
"""
import sys
import xml.etree.ElementTree as ET
import re
from pathlib import Path
class FAQRSSValidator:
def __init__(self, rss_file="FAQ.RSS"):
self.rss_file = Path(rss_file)
self.errors = []
self.warnings = []
self.allowed_titles = {
"Config Files",
"Startup Parameters",
"Troubleshooting",
"Steam Workshop"
}
def validate_xml_structure(self):
"""Validate basic XML structure and parsing"""
try:
tree = ET.parse(self.rss_file)
self.root = tree.getroot()
if self.root.tag != 'rss':
self.errors.append("Root element must be 'rss'")
return False
channel = self.root.find('channel')
if channel is None:
self.errors.append("Missing 'channel' element")
return False
self.channel = channel
return True
except ET.ParseError as e:
self.errors.append(f"XML parsing error: {e}")
return False
except FileNotFoundError:
self.errors.append(f"RSS file not found: {self.rss_file}")
return False
except Exception as e:
self.errors.append(f"Unexpected error parsing XML: {e}")
return False
def check_no_cdata(self):
"""Check that there are no CDATA sections in the file"""
try:
with open(self.rss_file, 'r', encoding='utf-8') as f:
content = f.read()
if '<![CDATA[' in content:
self.errors.append("CDATA sections are not allowed")
# Find specific locations
lines = content.split('\n')
for i, line in enumerate(lines, 1):
if '<![CDATA[' in line:
self.errors.append(f" CDATA found at line {i}")
except Exception as e:
self.errors.append(f"Error reading file for CDATA check: {e}")
def check_allowed_titles(self):
"""Check that all item titles are from allowed set"""
items = self.channel.findall('item')
for i, item in enumerate(items, 1):
title_elem = item.find('title')
if title_elem is None:
self.errors.append(f"Item {i}: Missing title element")
continue
title = title_elem.text
if title not in self.allowed_titles:
self.errors.append(f"Item {i}: Invalid title '{title}'. Allowed: {', '.join(sorted(self.allowed_titles))}")
def check_required_elements(self):
"""Check that each item has required elements"""
items = self.channel.findall('item')
for i, item in enumerate(items, 1):
# Check for title
if item.find('title') is None:
self.errors.append(f"Item {i}: Missing title element")
# Check for category
category_elem = item.find('category')
if category_elem is None:
self.errors.append(f"Item {i}: Missing category element")
elif not category_elem.text or not category_elem.text.strip():
self.errors.append(f"Item {i}: Empty category")
# Check for content:encoded (with namespace)
content_elem = item.find('{http://purl.org/rss/1.0/modules/content/}encoded')
if content_elem is None:
self.errors.append(f"Item {i}: Missing content:encoded element")
elif not content_elem.text or not content_elem.text.strip():
self.warnings.append(f"Item {i}: Empty content")
def check_html_escaping(self):
"""Check that HTML content is properly escaped"""
items = self.channel.findall('item')
for i, item in enumerate(items, 1):
content_elem = item.find('{http://purl.org/rss/1.0/modules/content/}encoded')
if content_elem is None or not content_elem.text:
continue
content = content_elem.text
# Check for unescaped < and > (except for allowed tags)
allowed_tags = ['br', 'strong', '/strong']
# Find all < and > characters
for match in re.finditer(r'<([^>]*)>', content):
tag = match.group(1).strip()
if tag not in allowed_tags and not tag.startswith('&lt;') and not tag.startswith('&gt;'):
# Check if it's properly escaped
if not tag.startswith('&') or not tag.endswith(';'):
self.errors.append(f"Item {i}: Unescaped HTML tag '<{tag}>'")
# Check for unescaped & that aren't part of entities
unescaped_amp = re.findall(r'&(?![a-zA-Z0-9#]+;)', content)
if unescaped_amp:
self.errors.append(f"Item {i}: Unescaped ampersand(s) found")
def check_alphabetical_order(self):
"""Check that categories are in alphabetical order"""
items = self.channel.findall('item')
categories = []
for item in items:
category_elem = item.find('category')
title_elem = item.find('title')
if category_elem is not None and title_elem is not None:
categories.append((category_elem.text, title_elem.text))
# Check if categories are sorted
sorted_categories = sorted(categories, key=lambda x: (x[0], x[1]))
if categories != sorted_categories:
self.warnings.append("Items are not in alphabetical order by category then title")
def check_duplicate_items(self):
"""Check for duplicate category/title combinations"""
items = self.channel.findall('item')
seen = set()
for i, item in enumerate(items, 1):
category_elem = item.find('category')
title_elem = item.find('title')
if category_elem is not None and title_elem is not None:
key = (category_elem.text, title_elem.text)
if key in seen:
self.errors.append(f"Item {i}: Duplicate category/title combination: {key}")
seen.add(key)
def check_content_formatting(self):
"""Check content formatting requirements"""
items = self.channel.findall('item')
for i, item in enumerate(items, 1):
content_elem = item.find('{http://purl.org/rss/1.0/modules/content/}encoded')
if content_elem is None or not content_elem.text:
continue
content = content_elem.text
# Check for proper use of <br> instead of newlines
if '\n' in content and '<br>' not in content:
self.warnings.append(f"Item {i}: Consider using <br> tags instead of newlines")
# Check for unescaped strong tags (should be &lt;strong&gt;)
if '<strong>' in content and '&lt;strong&gt;' not in content:
self.warnings.append(f"Item {i}: Use &lt;strong&gt; instead of <strong> tags")
def generate_statistics(self):
"""Generate statistics about the RSS file"""
items = self.channel.findall('item')
categories = {}
for item in items:
category_elem = item.find('category')
if category_elem is not None and category_elem.text:
categories[category_elem.text] = categories.get(category_elem.text, 0) + 1
print("\nStatistics:")
print(f"Total items: {len(items)}")
print(f"Total categories: {len(categories)}")
if categories:
print("\nItems per category:")
for category in sorted(categories.keys()):
print(f" {category}: {categories[category]} items")
def validate(self):
"""Run all validation checks"""
print(f"Validating RSS file: {self.rss_file}")
print("=" * 50)
# Basic XML structure validation
if not self.validate_xml_structure():
return False
# Run all validation checks
self.check_no_cdata()
self.check_allowed_titles()
self.check_required_elements()
self.check_html_escaping()
self.check_alphabetical_order()
self.check_duplicate_items()
self.check_content_formatting()
# Report results
print(f"Found {len(self.errors)} errors and {len(self.warnings)} warnings")
if self.errors:
print(f"\n❌ ERRORS ({len(self.errors)}):")
for error in self.errors:
print(f"{error}")
if self.warnings:
print(f"\n⚠️ WARNINGS ({len(self.warnings)}):")
for warning in self.warnings:
print(f"{warning}")
if not self.errors and not self.warnings:
print("\n✅ All validation checks passed!")
# Generate statistics
self.generate_statistics()
return len(self.errors) == 0
def run(self):
"""Main execution method"""
return self.validate()
def main():
if len(sys.argv) > 1:
rss_file = sys.argv[1]
else:
rss_file = "FAQ.RSS"
validator = FAQRSSValidator(rss_file)
success = validator.run()
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()