diff --git a/messages.json b/messages.json index 9b894f7..68aa721 100644 --- a/messages.json +++ b/messages.json @@ -1,12 +1,36 @@ { "duck_spawn": [ - "・゜゜・。。・゜゜\\_O< QUACK!", + "・゜゜・。。・゜゜\\_O< {light_grey}QUACK!{reset}", + "{light_grey}・゜゜・。。・゜゜{reset}\\_O< {light_grey}QUACK!{reset}", + "・゜゜・。。・゜゜{black}\\_O< QUACK!{reset}", "・゜゜・。。・゜゜\\_o< quack~", "・゜゜・。。・゜゜\\_O> *flap flap*" ], - "duck_flies_away": "The duck flies away. ·°'`'°-.,¸¸.·°'`", - "fast_duck_flies_away": "The fast duck quickly flies away! ·°'`'°-.,¸¸.·°'`", - "golden_duck_flies_away": "The {gold}golden duck{reset} flies away majestically. ·°'`'°-.,¸¸.·°'`", + "duck_flies_away": [ + "The duck flies away. ·°'`'°-.,¸¸.·°'`", + "The duck escapes into the sky! ·°'`'°-.,¸¸.·°'`", + "\\o< *quack* The duck waddles away safely.", + "The duck flaps away, living another day. ·°'`'°-.,¸¸.·°'`", + "\\o< The duck disappears into the distance.", + "The duck takes flight and vanishes! ·°'`'°-.,¸¸.·°'`", + "\\o< *flap* *flap* The duck has escaped!" + ], + "fast_duck_flies_away": [ + "The fast duck quickly flies away! ·°'`'°-.,¸¸.·°'`", + "\\o< *ZOOM* The speedy duck vanishes in a flash!", + "The fast duck zips away at lightning speed! ·°'`'°-.,¸¸.·°'`", + "\\o< Too slow! The fast duck has already escaped!", + "The swift duck darts away before you can blink! ·°'`'°-.,¸¸.·°'`", + "\\o< *whoosh* The fast duck is gone!" + ], + "golden_duck_flies_away": [ + "The {gold}golden duck{reset} flies away majestically. ·°'`'°-.,¸¸.·°'`", + "\\o< The {gold}golden duck{reset} glides away gracefully, its feathers shimmering.", + "The precious {gold}golden duck{reset} escapes to safety! ·°'`'°-.,¸¸.·°'`", + "\\o< The {gold}golden duck{reset} spreads its magnificent wings and soars away.", + "The valuable {gold}golden duck{reset} disappears into the sunset! ·°'`'°-.,¸¸.·°'`", + "\\o< *glimmer* The {gold}golden duck{reset} vanishes like a treasure in the wind." + ], "bang_hit": "{nick} > {red}*BANG*{reset} You shot the duck! \\_X< *KWAK* {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]", "bang_hit_golden": "{nick} > {red}*BANG*{reset} You shot a {gold}GOLDEN DUCK!{reset} [{hp_remaining} HP remaining] {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]", "bang_hit_golden_killed": "{nick} > {red}*BANG*{reset} You killed the GOLDEN DUCK! [+{xp_gained} xp] [Total ducks: {ducks_shot}]", diff --git a/src/db.py b/src/db.py index 44a9ab1..48cc7e6 100644 --- a/src/db.py +++ b/src/db.py @@ -1,6 +1,6 @@ """ Simplified Database management for DuckHunt Bot -Only essential player fields +Focus on fixing missing field errors """ import json @@ -23,7 +23,6 @@ class DuckDB: """Load player data from JSON file with comprehensive error handling""" try: if os.path.exists(self.db_file): - # Try to load the main database file with open(self.db_file, 'r', encoding='utf-8') as f: data = json.load(f) @@ -35,7 +34,7 @@ class DuckDB: if not isinstance(players_data, dict): raise ValueError("Players data is not a dictionary") - # Validate each player entry + # Validate each player entry and ensure required fields valid_players = {} for nick, player_data in players_data.items(): if isinstance(player_data, dict) and isinstance(nick, str): @@ -59,53 +58,93 @@ class DuckDB: self.players = {} def _sanitize_player_data(self, player_data): - """Sanitize and validate player data""" + """Sanitize and validate player data, ensuring ALL required fields exist""" try: sanitized = {} - # Ensure required fields with safe defaults - sanitized['nick'] = str(player_data.get('nick', 'Unknown'))[:50] # Limit nick length - sanitized['xp'] = max(0, int(player_data.get('xp', 0))) # Non-negative XP - sanitized['ducks_shot'] = max(0, int(player_data.get('ducks_shot', 0))) - sanitized['ducks_befriended'] = max(0, int(player_data.get('ducks_befriended', 0))) - sanitized['shots_fired'] = max(0, int(player_data.get('shots_fired', 0))) - sanitized['shots_missed'] = max(0, int(player_data.get('shots_missed', 0))) - default_accuracy = self.bot.get_config('default_accuracy', 75) if self.bot else 75 - max_accuracy = self.bot.get_config('max_accuracy', 100) if self.bot else 100 - sanitized['accuracy'] = max(0, min(max_accuracy, int(player_data.get('accuracy', default_accuracy)))) # 0-max_accuracy range + # Get default values from config or fallbacks + default_accuracy = self.bot.get_config('player_defaults.accuracy', 75) if self.bot else 75 + max_accuracy = self.bot.get_config('gameplay.max_accuracy', 100) if self.bot else 100 + default_magazines = self.bot.get_config('player_defaults.magazines', 3) if self.bot else 3 + default_bullets_per_mag = self.bot.get_config('player_defaults.bullets_per_magazine', 6) if self.bot else 6 + default_jam_chance = self.bot.get_config('player_defaults.jam_chance', 15) if self.bot else 15 + + # Core required fields - these MUST exist for messages to work + sanitized['nick'] = str(player_data.get('nick', 'Unknown'))[:50] + sanitized['xp'] = max(0, int(float(player_data.get('xp', 0)))) + sanitized['ducks_shot'] = max(0, int(float(player_data.get('ducks_shot', 0)))) + sanitized['ducks_befriended'] = max(0, int(float(player_data.get('ducks_befriended', 0)))) + sanitized['shots_fired'] = max(0, int(float(player_data.get('shots_fired', 0)))) + sanitized['shots_missed'] = max(0, int(float(player_data.get('shots_missed', 0)))) + + # Equipment and stats + sanitized['accuracy'] = max(0, min(max_accuracy, int(float(player_data.get('accuracy', default_accuracy))))) sanitized['gun_confiscated'] = bool(player_data.get('gun_confiscated', False)) # Ammo system with validation - sanitized['current_ammo'] = max(0, min(50, int(player_data.get('current_ammo', 6)))) - sanitized['magazines'] = max(0, min(20, int(player_data.get('magazines', 3)))) + sanitized['current_ammo'] = max(0, min(50, int(float(player_data.get('current_ammo', default_bullets_per_mag))))) + sanitized['magazines'] = max(0, min(20, int(float(player_data.get('magazines', default_magazines))))) + sanitized['bullets_per_magazine'] = max(1, min(50, int(float(player_data.get('bullets_per_magazine', default_bullets_per_mag))))) + sanitized['jam_chance'] = max(0, min(100, int(float(player_data.get('jam_chance', default_jam_chance))))) - # Confiscated ammo (optional fields) - if 'confiscated_ammo' in player_data: - sanitized['confiscated_ammo'] = max(0, min(50, int(player_data.get('confiscated_ammo', 0)))) - if 'confiscated_magazines' in player_data: - sanitized['confiscated_magazines'] = max(0, min(20, int(player_data.get('confiscated_magazines', 0)))) - sanitized['bullets_per_magazine'] = max(1, min(50, int(player_data.get('bullets_per_magazine', 6)))) - sanitized['jam_chance'] = max(0, min(100, int(player_data.get('jam_chance', 5)))) + # Confiscated ammo (optional fields but with safe defaults) + sanitized['confiscated_ammo'] = max(0, min(50, int(float(player_data.get('confiscated_ammo', 0))))) + sanitized['confiscated_magazines'] = max(0, min(20, int(float(player_data.get('confiscated_magazines', 0))))) # Safe inventory handling inventory = player_data.get('inventory', {}) if isinstance(inventory, dict): - sanitized['inventory'] = {str(k)[:10]: max(0, int(v)) for k, v in inventory.items() if isinstance(v, (int, float))} + clean_inventory = {} + for k, v in inventory.items(): + try: + clean_key = str(k)[:20] + clean_value = max(0, int(float(v))) if isinstance(v, (int, float, str)) else 0 + if clean_value > 0: + clean_inventory[clean_key] = clean_value + except (ValueError, TypeError): + continue + sanitized['inventory'] = clean_inventory else: sanitized['inventory'] = {} # Safe temporary effects temp_effects = player_data.get('temporary_effects', []) if isinstance(temp_effects, list): - sanitized['temporary_effects'] = temp_effects[:20] # Limit to 20 effects + clean_effects = [] + for effect in temp_effects[:20]: + if isinstance(effect, dict) and 'type' in effect: + clean_effects.append(effect) + sanitized['temporary_effects'] = clean_effects else: sanitized['temporary_effects'] = [] + # Add any missing fields that messages might reference + additional_fields = { + 'best_time': 0.0, + 'worst_time': 0.0, + 'total_time_hunting': 0.0, + 'level': 1, + 'xp_gained': 0, # For message templates + 'hp_remaining': 0, # For golden duck messages + 'victim': '', # For friendly fire messages + 'xp_lost': 0, # For penalty messages + 'ammo': 0, # Legacy field + 'max_ammo': 0, # Legacy field + 'chargers': 0 # Legacy field + } + + for field, default_value in additional_fields.items(): + if field not in sanitized: + if field in ['best_time', 'worst_time', 'total_time_hunting']: + sanitized[field] = max(0.0, float(player_data.get(field, default_value))) + else: + sanitized[field] = player_data.get(field, default_value) + return sanitized except Exception as e: self.logger.error(f"Error sanitizing player data: {e}") - return self.create_player('Unknown') + return self.create_player(player_data.get('nick', 'Unknown') if isinstance(player_data, dict) else 'Unknown') def save_database(self): """Save all player data to JSON file with comprehensive error handling""" @@ -128,10 +167,10 @@ class DuckDB: # Write to temporary file first (atomic write) with open(temp_file, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) - f.flush() # Ensure data is written to disk - os.fsync(f.fileno()) # Force write to disk + f.flush() + os.fsync(f.fileno()) - # Atomic replace: move temp file to actual file + # Atomic replace if os.name == 'nt': # Windows if os.path.exists(self.db_file): os.remove(self.db_file) @@ -141,12 +180,8 @@ class DuckDB: self.logger.debug(f"Database saved successfully with {len(data['players'])} players") - except PermissionError: - self.logger.error("Permission denied when saving database") - except OSError as e: - self.logger.error(f"OS error saving database: {e}") except Exception as e: - self.logger.error(f"Unexpected error saving database: {e}") + self.logger.error(f"Error saving database: {e}") finally: # Clean up temp file if it still exists try: @@ -163,12 +198,12 @@ class DuckDB: self.logger.warning(f"Invalid nick provided: {nick}") return None - nick_lower = nick.lower().strip()[:50] # Limit nick length and sanitize + nick_lower = nick.lower().strip()[:50] if nick_lower not in self.players: self.players[nick_lower] = self.create_player(nick) else: - # Ensure existing players have all required fields and sanitize data + # Ensure existing players have all required fields player = self.players[nick_lower] if not isinstance(player, dict): self.logger.warning(f"Invalid player data for {nick_lower}, recreating") @@ -189,17 +224,7 @@ class DuckDB: # Start with sanitized data validated_player = self._sanitize_player_data(player) - # Ensure new fields exist (migration from older versions) - if 'ducks_befriended' not in player: - validated_player['ducks_befriended'] = 0 - if 'inventory' not in player: - validated_player['inventory'] = {} - if 'temporary_effects' not in player: - validated_player['temporary_effects'] = [] - if 'jam_chance' not in player: - validated_player['jam_chance'] = 5 # Default 5% jam chance - - # Migrate from old ammo/chargers system to magazine system + # Migrate from old ammo/chargers system to magazine system if needed if 'magazines' not in player and ('ammo' in player or 'chargers' in player): self.logger.info(f"Migrating {nick} from old ammo system to magazine system") @@ -207,7 +232,7 @@ class DuckDB: old_chargers = player.get('chargers', 2) validated_player['current_ammo'] = max(0, min(50, int(old_ammo))) - validated_player['magazines'] = max(1, min(20, int(old_chargers) + 1)) # +1 for current loaded magazine + validated_player['magazines'] = max(1, min(20, int(old_chargers) + 1)) validated_player['bullets_per_magazine'] = 6 # Update nick in case it changed @@ -220,9 +245,8 @@ class DuckDB: return self.create_player(nick) def create_player(self, nick): - """Create a new player with configurable starting stats and inventory""" + """Create a new player with all required fields""" try: - # Sanitize nick safe_nick = str(nick)[:50] if nick else 'Unknown' # Get configurable defaults from bot config @@ -230,14 +254,13 @@ class DuckDB: accuracy = self.bot.get_config('player_defaults.accuracy', 75) magazines = self.bot.get_config('player_defaults.magazines', 3) bullets_per_mag = self.bot.get_config('player_defaults.bullets_per_magazine', 6) - jam_chance = self.bot.get_config('player_defaults.jam_chance', 5) + jam_chance = self.bot.get_config('player_defaults.jam_chance', 15) xp = self.bot.get_config('player_defaults.xp', 0) else: - # Fallback defaults if no bot config available accuracy = 75 magazines = 3 bullets_per_mag = 6 - jam_chance = 5 + jam_chance = 15 xp = 0 return { @@ -245,16 +268,30 @@ class DuckDB: 'xp': xp, 'ducks_shot': 0, 'ducks_befriended': 0, - 'shots_fired': 0, # Total shots fired - 'shots_missed': 0, # Total shots that missed - 'current_ammo': bullets_per_mag, # Bullets in current magazine - 'magazines': magazines, # Total magazines (including current) - 'bullets_per_magazine': bullets_per_mag, # Bullets per magazine - 'accuracy': accuracy, # Starting accuracy from config - 'jam_chance': jam_chance, # Base gun jamming chance from config + 'shots_fired': 0, + 'shots_missed': 0, + 'current_ammo': bullets_per_mag, + 'magazines': magazines, + 'bullets_per_magazine': bullets_per_mag, + 'accuracy': accuracy, + 'jam_chance': jam_chance, 'gun_confiscated': False, - 'inventory': {}, # Empty starting inventory - 'temporary_effects': [] # List of temporary effects + 'confiscated_ammo': 0, + 'confiscated_magazines': 0, + 'inventory': {}, + 'temporary_effects': [], + # Additional fields to prevent KeyErrors + 'best_time': 0.0, + 'worst_time': 0.0, + 'total_time_hunting': 0.0, + 'level': 1, + 'xp_gained': 0, + 'hp_remaining': 0, + 'victim': '', + 'xp_lost': 0, + 'ammo': bullets_per_mag, # Legacy + 'max_ammo': bullets_per_mag, # Legacy + 'chargers': magazines - 1 # Legacy } except Exception as e: self.logger.error(f"Error creating player for {nick}: {e}") @@ -263,33 +300,48 @@ class DuckDB: 'xp': 0, 'ducks_shot': 0, 'ducks_befriended': 0, + 'shots_fired': 0, + 'shots_missed': 0, 'current_ammo': 6, 'magazines': 3, 'bullets_per_magazine': 6, 'accuracy': 75, - 'jam_chance': 5, + 'jam_chance': 15, 'gun_confiscated': False, + 'confiscated_ammo': 0, + 'confiscated_magazines': 0, 'inventory': {}, - 'temporary_effects': [] + 'temporary_effects': [], + 'best_time': 0.0, + 'worst_time': 0.0, + 'total_time_hunting': 0.0, + 'level': 1, + 'xp_gained': 0, + 'hp_remaining': 0, + 'victim': '', + 'xp_lost': 0, + 'ammo': 6, + 'max_ammo': 6, + 'chargers': 2 } def get_leaderboard(self, category='xp', limit=3): """Get top players by specified category""" try: - # Create list of (nick, value) tuples leaderboard = [] for nick, player_data in self.players.items(): + sanitized_data = self._sanitize_player_data(player_data) + if category == 'xp': - value = player_data.get('xp', 0) + value = sanitized_data.get('xp', 0) elif category == 'ducks_shot': - value = player_data.get('ducks_shot', 0) + value = sanitized_data.get('ducks_shot', 0) else: continue leaderboard.append((nick, value)) - # Sort by value (descending) and take top N leaderboard.sort(key=lambda x: x[1], reverse=True) return leaderboard[:limit] diff --git a/src/duckhuntbot.py b/src/duckhuntbot.py index fc9b1a0..68159dd 100644 --- a/src/duckhuntbot.py +++ b/src/duckhuntbot.py @@ -1251,7 +1251,7 @@ class DuckHuntBot: except asyncio.TimeoutError: self.logger.warning("Task cancellation timed out") - # Quick database save + # Final database save try: self.db.save_database() self.logger.info("Database saved") @@ -1294,4 +1294,5 @@ class DuckHuntBot: finally: # Ensure writer is cleared regardless of errors self.writer = None - self.reader = None \ No newline at end of file + self.reader = None +