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,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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue