Moved the Agents into their own repo. Kept the agent.pl just for reference
This commit is contained in:
parent
22381be29a
commit
8680a02b13
18132 changed files with 0 additions and 2569420 deletions
|
|
@ -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) ==="
|
||||
|
|
@ -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', '<br>')
|
||||
return escaped
|
||||
|
||||
def generate_config_files_content(self, game):
|
||||
"""Generate Config Files section content"""
|
||||
content = "<strong>Configuration Files</strong><br>"
|
||||
|
||||
for config in game['configs']:
|
||||
file_name = config['file']
|
||||
paths = config.get('paths', [])
|
||||
desc = config.get('desc', '')
|
||||
|
||||
content += "<br>- " + 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 += "<br>"
|
||||
|
||||
return content
|
||||
|
||||
def generate_startup_parameters_content(self, game):
|
||||
"""Generate Startup Parameters section content"""
|
||||
startup = game['startup']
|
||||
content = "<strong>Default Command Line</strong><br>"
|
||||
content += "<br>" + self.escape_html_content(startup['default_command']) + "<br>"
|
||||
|
||||
# Port scheme
|
||||
if 'ports' in startup and startup['ports']:
|
||||
content += "<br><strong>Port Scheme</strong><br>"
|
||||
for port in startup['ports']:
|
||||
label = port.get('label', '')
|
||||
port_num = port.get('port', '')
|
||||
proto = port.get('proto', '')
|
||||
relative = port.get('relative', '')
|
||||
content += "<br>- " + self.escape_html_content(f"{label} ({proto}) — {relative} (default {port_num})") + "<br>"
|
||||
|
||||
# Command line flags
|
||||
if 'flags' in startup and startup['flags']:
|
||||
content += "<br><strong>Command Line Flags</strong><br>"
|
||||
for flag in startup['flags']:
|
||||
flag_name = flag.get('flag', '')
|
||||
default = flag.get('default', '')
|
||||
flag_type = flag.get('type', '')
|
||||
desc = flag.get('desc', '')
|
||||
content += "<br>" + self.escape_html_content(f"{flag_name} — {desc}. Type: {flag_type}, Default: {default}") + "<br>"
|
||||
|
||||
return content
|
||||
|
||||
def generate_troubleshooting_content(self, game):
|
||||
"""Generate Troubleshooting section content"""
|
||||
content = "<strong>Common Issues and Solutions</strong><br>"
|
||||
|
||||
for issue in game['troubleshooting']:
|
||||
content += "<br>- " + self.escape_html_content(issue) + "<br>"
|
||||
|
||||
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 = "<strong>Steam Workshop Configuration</strong><br>"
|
||||
|
||||
notes = workshop.get('notes', [])
|
||||
for note in notes:
|
||||
content += "<br>- " + self.escape_html_content(note) + "<br>"
|
||||
|
||||
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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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('<') and not tag.startswith('>'):
|
||||
# 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 <strong>)
|
||||
if '<strong>' in content and '<strong>' not in content:
|
||||
self.warnings.append(f"Item {i}: Use <strong> 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue