Data Persistence - andrewgari/starbunk-dm GitHub Wiki
Data Persistence System
This document outlines the data persistence design for the Discord RPG Campaign Bot.
Overview
The bot uses a JSON-based persistence system to store information about campaigns, characters, and relationships between Discord entities (channels, roles, users). This design ensures that data survives bot restarts and maintains the integrity of the roleplaying environment.
Data Structures
Campaigns Data
{
"guild_id": {
"channel_id": {
"name": "Campaign Name",
"gm_id": "user_id_of_gm",
"player_role_id": "role_id",
"gm_role_id": "role_id",
"webhook_id": "webhook_id",
"players": ["user_id1", "user_id2", ...]
}
}
}
This structure allows:
- Organization by guild (server) and channel
- Quick lookup of campaign-specific details
- Association with Discord roles for permission management
- Storage of the webhook ID for messaging features
Characters Data
{
"guild_id": {
"user_id": {
"channel_id": {
"name": "Character Name",
"avatar_url": "URL to avatar image",
"description": "Character description"
}
}
}
}
This structure enables:
- One character per player per campaign channel
- Quick lookup of a player's character in a specific campaign
- Storage of character-specific details for messaging
GM-Created NPCs
{
"guild_id": {
"channel_id": {
"npc_id": {
"name": "NPC Name",
"avatar_url": "URL to avatar image",
"creator_id": "gm_user_id"
}
}
}
}
This allows GMs to:
- Create multiple NPCs per campaign
- Quickly access their NPC list for storytelling
- Maintain separate NPCs for different campaigns
Implementation
DataManager Class
class DataManager:
"""Manages persistence of bot data"""
def __init__(self, data_dir="./data"):
self.data_dir = data_dir
self.campaigns_file = os.path.join(data_dir, "campaigns.json")
self.characters_file = os.path.join(data_dir, "characters.json")
self.npcs_file = os.path.join(data_dir, "npcs.json")
# Ensure data directory exists
os.makedirs(data_dir, exist_ok=True)
# Initialize data structures
self.campaigns = {}
self.characters = {}
self.npcs = {}
# Load data on initialization
self.load_data()
def load_data(self):
"""Load all data from files"""
self.campaigns = self._load_json_file(self.campaigns_file, {})
self.characters = self._load_json_file(self.characters_file, {})
self.npcs = self._load_json_file(self.npcs_file, {})
def save_data(self):
"""Save all data to files"""
self._save_json_file(self.campaigns_file, self.campaigns)
self._save_json_file(self.characters_file, self.characters)
self._save_json_file(self.npcs_file, self.npcs)
def _load_json_file(self, file_path, default_value):
"""Load JSON from file with error handling"""
try:
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
return default_value
except Exception as e:
print(f"Error loading {file_path}: {e}")
return default_value
def _save_json_file(self, file_path, data):
"""Save JSON to file with error handling"""
try:
# Create a backup first
self._create_backup(file_path)
# Save the new data
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
print(f"Error saving {file_path}: {e}")
return False
def _create_backup(self, file_path):
"""Create a backup of the given file"""
try:
if os.path.exists(file_path):
backup_dir = os.path.join(self.data_dir, "backups")
os.makedirs(backup_dir, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
filename = os.path.basename(file_path)
backup_path = os.path.join(backup_dir, f"{filename}.{timestamp}")
shutil.copy2(file_path, backup_path)
except Exception as e:
print(f"Error creating backup: {e}")
Integration with Bot Features
Campaign Creation
async def create_campaign(self, interaction):
guild_id = str(interaction.guild_id)
channel_id = str(interaction.channel_id)
# Create campaign entry
if guild_id not in self.bot.data.campaigns:
self.bot.data.campaigns[guild_id] = {}
self.bot.data.campaigns[guild_id][channel_id] = {
"name": interaction.channel.name,
"gm_id": str(interaction.user.id),
"player_role_id": str(player_role.id),
"gm_role_id": str(gm_role.id),
"players": []
}
# Save data
self.bot.data.save_data()
Character Creation
async def create_character(self, interaction, name, avatar_url):
guild_id = str(interaction.guild_id)
channel_id = str(interaction.channel_id)
user_id = str(interaction.user.id)
# Initialize data structure if needed
if guild_id not in self.bot.data.characters:
self.bot.data.characters[guild_id] = {}
if user_id not in self.bot.data.characters[guild_id]:
self.bot.data.characters[guild_id][user_id] = {}
# Create or update character
self.bot.data.characters[guild_id][user_id][channel_id] = {
"name": name,
"avatar_url": avatar_url,
"description": ""
}
# Save data
self.bot.data.save_data()
Data Validation and Recovery
The system includes several safeguards:
- Backups: Every save operation creates a backup with timestamp
- Error Handling: All file operations include try/except blocks
- Default Values: Missing data is handled gracefully with defaults
- Validation: Data is validated before saving and after loading
- Recovery: If data is corrupted, it attempts to load from backups
Security Considerations
- File Permissions: Data files have restricted permissions
- Sanitization: All user input is sanitized before storage
- No Sensitive Data: The system never stores tokens or credentials
- Guild Isolation: Data from different Discord servers is isolated
Limitations and Considerations
- Performance: JSON is not optimized for large datasets but works well for typical use cases
- Concurrency: The current design doesn't handle concurrent modifications optimally
- Scalability: For very large servers, a database solution might be preferable
- Backup Management: Old backups are not automatically pruned
Future Improvements
- Database Migration: Option to use SQLite or another database for larger deployments
- Transaction Support: Atomic operations for better data integrity
- Compression: Data compression for reduced storage needs
- Cloud Storage: Option for cloud-based backup and synchronization