Separate player stats per channel
This commit is contained in:
203
src/db.py
Normal file → Executable file
203
src/db.py
Normal file → Executable file
@@ -23,7 +23,8 @@ class DuckDB:
|
|||||||
project_root = os.path.dirname(os.path.dirname(__file__))
|
project_root = os.path.dirname(os.path.dirname(__file__))
|
||||||
self.db_file = os.path.join(project_root, db_file)
|
self.db_file = os.path.join(project_root, db_file)
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.players = {}
|
# Channel-scoped data: {"#channel": {"players": {"nick": player_dict}}}
|
||||||
|
self.channels = {}
|
||||||
self.logger = logging.getLogger('DuckHuntBot.DB')
|
self.logger = logging.getLogger('DuckHuntBot.DB')
|
||||||
|
|
||||||
# Error recovery configuration
|
# Error recovery configuration
|
||||||
@@ -32,12 +33,37 @@ class DuckDB:
|
|||||||
|
|
||||||
data = self.load_database()
|
data = self.load_database()
|
||||||
# Hydrate in-memory state from disk.
|
# Hydrate in-memory state from disk.
|
||||||
# Previously, load_database() returned data but self.players stayed empty,
|
if isinstance(data, dict) and isinstance(data.get('channels'), dict):
|
||||||
# making it look like everything reset after restart.
|
self.channels = data['channels']
|
||||||
if isinstance(data, dict) and isinstance(data.get('players'), dict):
|
|
||||||
self.players = data['players']
|
|
||||||
else:
|
else:
|
||||||
self.players = {}
|
self.channels = {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_channel(channel: str) -> str:
|
||||||
|
"""Normalize channel keys (case-insensitive). Non-channel contexts go to a reserved bucket."""
|
||||||
|
if not isinstance(channel, str):
|
||||||
|
return '__unknown__'
|
||||||
|
channel = channel.strip()
|
||||||
|
if not channel:
|
||||||
|
return '__unknown__'
|
||||||
|
if channel.startswith('#') or channel.startswith('&'):
|
||||||
|
return channel.lower()
|
||||||
|
return '__pm__'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def players(self):
|
||||||
|
"""Backward-compatible flattened view of all players across channels."""
|
||||||
|
flattened = {}
|
||||||
|
try:
|
||||||
|
for _channel_key, channel_data in (self.channels or {}).items():
|
||||||
|
players = channel_data.get('players', {}) if isinstance(channel_data, dict) else {}
|
||||||
|
if isinstance(players, dict):
|
||||||
|
for nick, player in players.items():
|
||||||
|
# Last-write-wins if the same nick exists in multiple channels.
|
||||||
|
flattened[nick] = player
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
return flattened
|
||||||
|
|
||||||
def load_database(self) -> dict:
|
def load_database(self) -> dict:
|
||||||
"""Load the database, creating it if it doesn't exist"""
|
"""Load the database, creating it if it doesn't exist"""
|
||||||
@@ -66,15 +92,40 @@ class DuckDB:
|
|||||||
'created': datetime.now().isoformat(),
|
'created': datetime.now().isoformat(),
|
||||||
'last_modified': datetime.now().isoformat()
|
'last_modified': datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Initialize players section if missing
|
# Migrate legacy flat structure (players) -> channels
|
||||||
if 'players' not in data:
|
if 'channels' not in data or not isinstance(data.get('channels'), dict):
|
||||||
data['players'] = {}
|
legacy_players = data.get('players') if isinstance(data.get('players'), dict) else {}
|
||||||
|
channels = {}
|
||||||
|
if isinstance(legacy_players, dict):
|
||||||
|
for legacy_nick, legacy_player in legacy_players.items():
|
||||||
|
try:
|
||||||
|
last_channel = legacy_player.get('last_activity_channel') if isinstance(legacy_player, dict) else None
|
||||||
|
channel_key = self._normalize_channel(last_channel) if last_channel else '__global__'
|
||||||
|
channels.setdefault(channel_key, {'players': {}})
|
||||||
|
if isinstance(channels[channel_key].get('players'), dict):
|
||||||
|
channels[channel_key]['players'][str(legacy_nick).lower()] = legacy_player
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
data['channels'] = channels
|
||||||
|
data['metadata']['version'] = '2.0'
|
||||||
|
|
||||||
|
# Ensure channels structure exists
|
||||||
|
if 'channels' not in data:
|
||||||
|
data['channels'] = {}
|
||||||
|
|
||||||
# Update last_modified
|
# Update last_modified
|
||||||
data['metadata']['last_modified'] = datetime.now().isoformat()
|
data['metadata']['last_modified'] = datetime.now().isoformat()
|
||||||
|
|
||||||
self.logger.info(f"Successfully loaded database with {len(data.get('players', {}))} players")
|
total_players = 0
|
||||||
|
try:
|
||||||
|
for _c, cdata in data.get('channels', {}).items():
|
||||||
|
if isinstance(cdata, dict) and isinstance(cdata.get('players'), dict):
|
||||||
|
total_players += len(cdata['players'])
|
||||||
|
except Exception:
|
||||||
|
total_players = 0
|
||||||
|
self.logger.info(f"Successfully loaded database with {total_players} players across {len(data.get('channels', {}))} channels")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
except (json.JSONDecodeError, ValueError) as e:
|
except (json.JSONDecodeError, ValueError) as e:
|
||||||
@@ -88,9 +139,9 @@ class DuckDB:
|
|||||||
"""Create a new default database file with proper structure"""
|
"""Create a new default database file with proper structure"""
|
||||||
try:
|
try:
|
||||||
default_data = {
|
default_data = {
|
||||||
"players": {},
|
"channels": {},
|
||||||
"last_save": str(time.time()),
|
"last_save": str(time.time()),
|
||||||
"version": "1.0",
|
"version": "2.0",
|
||||||
"created": time.strftime("%Y-%m-%d %H:%M:%S"),
|
"created": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"description": "DuckHunt Bot Player Database"
|
"description": "DuckHunt Bot Player Database"
|
||||||
}
|
}
|
||||||
@@ -105,9 +156,9 @@ class DuckDB:
|
|||||||
self.logger.error(f"Failed to create default database: {e}")
|
self.logger.error(f"Failed to create default database: {e}")
|
||||||
# Return a minimal valid structure even if file creation fails
|
# Return a minimal valid structure even if file creation fails
|
||||||
return {
|
return {
|
||||||
"players": {},
|
"channels": {},
|
||||||
"last_save": str(time.time()),
|
"last_save": str(time.time()),
|
||||||
"version": "1.0",
|
"version": "2.0",
|
||||||
"created": time.strftime("%Y-%m-%d %H:%M:%S"),
|
"created": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"description": "DuckHunt Bot Player Database"
|
"description": "DuckHunt Bot Player Database"
|
||||||
}
|
}
|
||||||
@@ -135,6 +186,14 @@ class DuckDB:
|
|||||||
# Equipment and stats
|
# Equipment and stats
|
||||||
sanitized['accuracy'] = max(0, min(max_accuracy, int(float(player_data.get('accuracy', default_accuracy)))))
|
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))
|
sanitized['gun_confiscated'] = bool(player_data.get('gun_confiscated', False))
|
||||||
|
|
||||||
|
# Activity / admin flags
|
||||||
|
sanitized['last_activity_channel'] = str(player_data.get('last_activity_channel', ''))[:100]
|
||||||
|
try:
|
||||||
|
sanitized['last_activity_time'] = float(player_data.get('last_activity_time', 0.0))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
sanitized['last_activity_time'] = 0.0
|
||||||
|
sanitized['ignored'] = bool(player_data.get('ignored', False))
|
||||||
|
|
||||||
# Ammo system with validation
|
# Ammo system with validation
|
||||||
sanitized['current_ammo'] = max(0, min(50, int(float(player_data.get('current_ammo', default_bullets_per_mag)))))
|
sanitized['current_ammo'] = max(0, min(50, int(float(player_data.get('current_ammo', default_bullets_per_mag)))))
|
||||||
@@ -214,23 +273,30 @@ class DuckDB:
|
|||||||
try:
|
try:
|
||||||
# Prepare data with validation
|
# Prepare data with validation
|
||||||
data = {
|
data = {
|
||||||
'players': {},
|
'channels': {},
|
||||||
'last_save': str(time.time()),
|
'last_save': str(time.time()),
|
||||||
'version': '1.0'
|
'version': '2.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Validate and clean player data before saving
|
# Validate and clean player data before saving
|
||||||
valid_count = 0
|
valid_count = 0
|
||||||
for nick, player_data in self.players.items():
|
for channel_key, channel_data in (self.channels or {}).items():
|
||||||
if isinstance(nick, str) and isinstance(player_data, dict):
|
if not isinstance(channel_key, str) or not isinstance(channel_data, dict):
|
||||||
try:
|
continue
|
||||||
sanitized_nick = sanitize_user_input(nick, max_length=50)
|
players = channel_data.get('players', {})
|
||||||
data['players'][sanitized_nick] = self._sanitize_player_data(player_data)
|
if not isinstance(players, dict):
|
||||||
valid_count += 1
|
continue
|
||||||
except Exception as e:
|
|
||||||
self.logger.warning(f"Error processing player {nick} during save: {e}")
|
out_channel_key = str(channel_key)
|
||||||
else:
|
data['channels'].setdefault(out_channel_key, {'players': {}})
|
||||||
self.logger.warning(f"Skipping invalid player data during save: {nick}")
|
for nick, player_data in players.items():
|
||||||
|
if isinstance(nick, str) and isinstance(player_data, dict):
|
||||||
|
try:
|
||||||
|
sanitized_nick = sanitize_user_input(nick, max_length=50)
|
||||||
|
data['channels'][out_channel_key]['players'][sanitized_nick] = self._sanitize_player_data(player_data)
|
||||||
|
valid_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Error processing player {nick} in {out_channel_key} during save: {e}")
|
||||||
|
|
||||||
# Saving an empty database is valid (e.g., first run or after admin wipes).
|
# Saving an empty database is valid (e.g., first run or after admin wipes).
|
||||||
# Previously this raised and prevented the file from being written/updated.
|
# Previously this raised and prevented the file from being written/updated.
|
||||||
@@ -270,8 +336,52 @@ class DuckDB:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_player(self, nick):
|
def get_players_for_channel(self, channel: str) -> dict:
|
||||||
"""Get player data, creating if doesn't exist with comprehensive validation"""
|
"""Get the players dict for a channel, creating the channel bucket if needed."""
|
||||||
|
channel_key = self._normalize_channel(channel)
|
||||||
|
bucket = self.channels.setdefault(channel_key, {'players': {}})
|
||||||
|
if not isinstance(bucket, dict):
|
||||||
|
bucket = {'players': {}}
|
||||||
|
self.channels[channel_key] = bucket
|
||||||
|
if 'players' not in bucket or not isinstance(bucket.get('players'), dict):
|
||||||
|
bucket['players'] = {}
|
||||||
|
return bucket['players']
|
||||||
|
|
||||||
|
def iter_all_players(self):
|
||||||
|
"""Yield (channel_key, nick, player_dict) for all players in all channels."""
|
||||||
|
for channel_key, channel_data in (self.channels or {}).items():
|
||||||
|
if not isinstance(channel_data, dict):
|
||||||
|
continue
|
||||||
|
players = channel_data.get('players', {})
|
||||||
|
if not isinstance(players, dict):
|
||||||
|
continue
|
||||||
|
for nick, player in players.items():
|
||||||
|
yield channel_key, nick, player
|
||||||
|
|
||||||
|
def get_player_if_exists(self, nick, channel: str):
|
||||||
|
"""Return player dict for nick+channel if present; does not create records."""
|
||||||
|
try:
|
||||||
|
if not isinstance(nick, str) or not nick.strip():
|
||||||
|
return None
|
||||||
|
nick_clean = sanitize_user_input(nick, max_length=50,
|
||||||
|
allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-[]{}^`|\\')
|
||||||
|
nick_lower = nick_clean.lower().strip()
|
||||||
|
if not nick_lower:
|
||||||
|
return None
|
||||||
|
channel_key = self._normalize_channel(channel)
|
||||||
|
channel_data = self.channels.get(channel_key)
|
||||||
|
if not isinstance(channel_data, dict):
|
||||||
|
return None
|
||||||
|
players = channel_data.get('players')
|
||||||
|
if not isinstance(players, dict):
|
||||||
|
return None
|
||||||
|
player = players.get(nick_lower)
|
||||||
|
return player if isinstance(player, dict) else None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_player(self, nick, channel: str):
|
||||||
|
"""Get player data for a specific channel, creating if doesn't exist with comprehensive validation"""
|
||||||
try:
|
try:
|
||||||
# Validate and sanitize nick
|
# Validate and sanitize nick
|
||||||
if not isinstance(nick, str) or not nick.strip():
|
if not isinstance(nick, str) or not nick.strip():
|
||||||
@@ -290,15 +400,17 @@ class DuckDB:
|
|||||||
if not nick_lower:
|
if not nick_lower:
|
||||||
self.logger.warning(f"Empty nick after sanitization: {nick}")
|
self.logger.warning(f"Empty nick after sanitization: {nick}")
|
||||||
return self.create_player('Unknown')
|
return self.create_player('Unknown')
|
||||||
|
|
||||||
if nick_lower not in self.players:
|
players = self.get_players_for_channel(channel)
|
||||||
self.players[nick_lower] = self.create_player(nick_clean)
|
|
||||||
|
if nick_lower not in players:
|
||||||
|
players[nick_lower] = self.create_player(nick_clean)
|
||||||
else:
|
else:
|
||||||
# Ensure existing players have all required fields
|
# Ensure existing players have all required fields
|
||||||
player = self.players[nick_lower]
|
player = players[nick_lower]
|
||||||
if not isinstance(player, dict):
|
if not isinstance(player, dict):
|
||||||
self.logger.warning(f"Invalid player data for {nick_lower}, recreating")
|
self.logger.warning(f"Invalid player data for {nick_lower}, recreating")
|
||||||
self.players[nick_lower] = self.create_player(nick_clean)
|
players[nick_lower] = self.create_player(nick_clean)
|
||||||
else:
|
else:
|
||||||
# Migrate and validate existing player data with error recovery
|
# Migrate and validate existing player data with error recovery
|
||||||
validated = self.error_recovery.safe_execute(
|
validated = self.error_recovery.safe_execute(
|
||||||
@@ -306,9 +418,9 @@ class DuckDB:
|
|||||||
fallback=self.create_player(nick_clean),
|
fallback=self.create_player(nick_clean),
|
||||||
logger=self.logger
|
logger=self.logger
|
||||||
)
|
)
|
||||||
self.players[nick_lower] = validated
|
players[nick_lower] = validated
|
||||||
|
|
||||||
return self.players[nick_lower]
|
return players[nick_lower]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Critical error getting player {nick}: {e}")
|
self.logger.error(f"Critical error getting player {nick}: {e}")
|
||||||
@@ -376,6 +488,9 @@ class DuckDB:
|
|||||||
'confiscated_magazines': 0,
|
'confiscated_magazines': 0,
|
||||||
'inventory': {},
|
'inventory': {},
|
||||||
'temporary_effects': [],
|
'temporary_effects': [],
|
||||||
|
'last_activity_channel': '',
|
||||||
|
'last_activity_time': 0.0,
|
||||||
|
'ignored': False,
|
||||||
# Additional fields to prevent KeyErrors
|
# Additional fields to prevent KeyErrors
|
||||||
'best_time': 0.0,
|
'best_time': 0.0,
|
||||||
'worst_time': 0.0,
|
'worst_time': 0.0,
|
||||||
@@ -408,6 +523,9 @@ class DuckDB:
|
|||||||
'confiscated_magazines': 0,
|
'confiscated_magazines': 0,
|
||||||
'inventory': {},
|
'inventory': {},
|
||||||
'temporary_effects': [],
|
'temporary_effects': [],
|
||||||
|
'last_activity_channel': '',
|
||||||
|
'last_activity_time': 0.0,
|
||||||
|
'ignored': False,
|
||||||
'best_time': 0.0,
|
'best_time': 0.0,
|
||||||
'worst_time': 0.0,
|
'worst_time': 0.0,
|
||||||
'total_time_hunting': 0.0,
|
'total_time_hunting': 0.0,
|
||||||
@@ -421,12 +539,13 @@ class DuckDB:
|
|||||||
'chargers': 2
|
'chargers': 2
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_leaderboard(self, category='xp', limit=3):
|
def get_leaderboard(self, channel: str, category='xp', limit=3):
|
||||||
"""Get top players by specified category"""
|
"""Get top players by specified category for a given channel"""
|
||||||
try:
|
try:
|
||||||
leaderboard = []
|
leaderboard = []
|
||||||
|
|
||||||
for nick, player_data in self.players.items():
|
players = self.get_players_for_channel(channel)
|
||||||
|
for nick, player_data in players.items():
|
||||||
sanitized_data = self._sanitize_player_data(player_data)
|
sanitized_data = self._sanitize_player_data(player_data)
|
||||||
|
|
||||||
if category == 'xp':
|
if category == 'xp':
|
||||||
|
|||||||
69
src/duckhuntbot.py
Normal file → Executable file
69
src/duckhuntbot.py
Normal file → Executable file
@@ -62,7 +62,7 @@ class DuckHuntBot:
|
|||||||
# Database health check
|
# Database health check
|
||||||
self.health_checker.add_check(
|
self.health_checker.add_check(
|
||||||
'database',
|
'database',
|
||||||
lambda: self.db is not None and len(self.db.players) >= 0,
|
lambda: self.db is not None and hasattr(self.db, 'channels'),
|
||||||
critical=True
|
critical=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -142,11 +142,8 @@ class DuckHuntBot:
|
|||||||
self.send_message(channel, message)
|
self.send_message(channel, message)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
target = args[0].lower()
|
target = args[0]
|
||||||
player = self.db.get_player(target)
|
player = self.db.get_player(target, channel)
|
||||||
if player is None:
|
|
||||||
player = self.db.create_player(target)
|
|
||||||
self.db.players[target] = player
|
|
||||||
action_func(player)
|
action_func(player)
|
||||||
|
|
||||||
message = self.messages.get(success_message_key, target=target, admin=nick)
|
message = self.messages.get(success_message_key, target=target, admin=nick)
|
||||||
@@ -164,25 +161,16 @@ class DuckHuntBot:
|
|||||||
if not is_private_msg:
|
if not is_private_msg:
|
||||||
if target_nick.lower() == nick.lower():
|
if target_nick.lower() == nick.lower():
|
||||||
target_nick = target_nick.lower()
|
target_nick = target_nick.lower()
|
||||||
player = self.db.get_player(target_nick)
|
player = self.db.get_player(target_nick, channel)
|
||||||
if player is None:
|
|
||||||
player = self.db.create_player(target_nick)
|
|
||||||
self.db.players[target_nick] = player
|
|
||||||
return player, None
|
return player, None
|
||||||
else:
|
else:
|
||||||
is_valid, player, error_msg = self.validate_target_player(target_nick, channel)
|
is_valid, player, error_msg = self.validate_target_player(target_nick, channel)
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
return None, error_msg
|
return None, error_msg
|
||||||
target_nick = target_nick.lower()
|
|
||||||
if target_nick not in self.db.players:
|
|
||||||
self.db.players[target_nick] = player
|
|
||||||
return player, None
|
return player, None
|
||||||
else:
|
else:
|
||||||
target_nick = target_nick.lower()
|
target_nick = target_nick.lower()
|
||||||
player = self.db.get_player(target_nick)
|
player = self.db.get_player(target_nick, channel)
|
||||||
if player is None:
|
|
||||||
player = self.db.create_player(target_nick)
|
|
||||||
self.db.players[target_nick] = player
|
|
||||||
return player, None
|
return player, None
|
||||||
|
|
||||||
def _get_validated_target_player(self, nick, channel, target_nick):
|
def _get_validated_target_player(self, nick, channel, target_nick):
|
||||||
@@ -620,7 +608,7 @@ class DuckHuntBot:
|
|||||||
|
|
||||||
# Get player data with error recovery
|
# Get player data with error recovery
|
||||||
player = self.error_recovery.safe_execute(
|
player = self.error_recovery.safe_execute(
|
||||||
lambda: self.db.get_player(nick),
|
lambda: self.db.get_player(nick, safe_channel),
|
||||||
fallback={'nick': nick, 'xp': 0, 'ducks_shot': 0, 'gun_confiscated': False},
|
fallback={'nick': nick, 'xp': 0, 'ducks_shot': 0, 'gun_confiscated': False},
|
||||||
logger=self.logger
|
logger=self.logger
|
||||||
)
|
)
|
||||||
@@ -633,7 +621,6 @@ class DuckHuntBot:
|
|||||||
try:
|
try:
|
||||||
player['last_activity_channel'] = safe_channel
|
player['last_activity_channel'] = safe_channel
|
||||||
player['last_activity_time'] = time.time()
|
player['last_activity_time'] = time.time()
|
||||||
self.db.players[nick.lower()] = player
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Error updating player activity for {nick}: {e}")
|
self.logger.warning(f"Error updating player activity for {nick}: {e}")
|
||||||
|
|
||||||
@@ -811,9 +798,9 @@ class DuckHuntBot:
|
|||||||
if not target_nick:
|
if not target_nick:
|
||||||
return False, None, "Invalid target nickname"
|
return False, None, "Invalid target nickname"
|
||||||
|
|
||||||
player = self.db.get_player(target_nick)
|
player = self.db.get_player_if_exists(target_nick, channel)
|
||||||
if not player:
|
if not player:
|
||||||
return False, None, f"Player '{target_nick}' not found. They need to participate in the game first."
|
return False, None, f"Player '{target_nick}' not found in {channel}. They need to participate in this channel first."
|
||||||
|
|
||||||
has_activity = (
|
has_activity = (
|
||||||
player.get('xp', 0) > 0 or
|
player.get('xp', 0) > 0 or
|
||||||
@@ -835,7 +822,7 @@ class DuckHuntBot:
|
|||||||
We assume if someone has been active recently, they're still in the channel.
|
We assume if someone has been active recently, they're still in the channel.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
player = self.db.get_player(nick)
|
player = self.db.get_player_if_exists(nick, channel)
|
||||||
if not player:
|
if not player:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -954,9 +941,9 @@ class DuckHuntBot:
|
|||||||
"""Handle !duckstats command"""
|
"""Handle !duckstats command"""
|
||||||
if args and len(args) > 0:
|
if args and len(args) > 0:
|
||||||
target_nick = args[0]
|
target_nick = args[0]
|
||||||
target_player = self.db.get_player(target_nick)
|
target_player = self.db.get_player_if_exists(target_nick, channel)
|
||||||
if not target_player:
|
if not target_player:
|
||||||
message = f"{nick} > Player '{target_nick}' not found."
|
message = f"{nick} > Player '{target_nick}' not found in {channel}."
|
||||||
self.send_message(channel, message)
|
self.send_message(channel, message)
|
||||||
return
|
return
|
||||||
display_nick = target_nick
|
display_nick = target_nick
|
||||||
@@ -1048,10 +1035,10 @@ class DuckHuntBot:
|
|||||||
reset = self.messages.messages.get('colours', {}).get('reset', '')
|
reset = self.messages.messages.get('colours', {}).get('reset', '')
|
||||||
|
|
||||||
# Get top 3 by XP
|
# Get top 3 by XP
|
||||||
top_xp = self.db.get_leaderboard('xp', 3)
|
top_xp = self.db.get_leaderboard(channel, 'xp', 3)
|
||||||
|
|
||||||
# Get top 3 by ducks shot
|
# Get top 3 by ducks shot
|
||||||
top_ducks = self.db.get_leaderboard('ducks_shot', 3)
|
top_ducks = self.db.get_leaderboard(channel, 'ducks_shot', 3)
|
||||||
|
|
||||||
# Format XP leaderboard as single line
|
# Format XP leaderboard as single line
|
||||||
if top_xp:
|
if top_xp:
|
||||||
@@ -1329,12 +1316,20 @@ class DuckHuntBot:
|
|||||||
# Check if admin wants to rearm all players
|
# Check if admin wants to rearm all players
|
||||||
if target_nick.lower() == 'all':
|
if target_nick.lower() == 'all':
|
||||||
rearmed_count = 0
|
rearmed_count = 0
|
||||||
for player_nick, player in self.db.players.items():
|
if is_private_msg:
|
||||||
if player.get('gun_confiscated', False):
|
for _ch, _pn, p in self.db.iter_all_players():
|
||||||
player['gun_confiscated'] = False
|
if p.get('gun_confiscated', False):
|
||||||
self.levels.update_player_magazines(player)
|
p['gun_confiscated'] = False
|
||||||
player['current_ammo'] = player.get('bullets_per_magazine', 6)
|
self.levels.update_player_magazines(p)
|
||||||
rearmed_count += 1
|
p['current_ammo'] = p.get('bullets_per_magazine', 6)
|
||||||
|
rearmed_count += 1
|
||||||
|
else:
|
||||||
|
for _pn, p in self.db.get_players_for_channel(channel).items():
|
||||||
|
if p.get('gun_confiscated', False):
|
||||||
|
p['gun_confiscated'] = False
|
||||||
|
self.levels.update_player_magazines(p)
|
||||||
|
p['current_ammo'] = p.get('bullets_per_magazine', 6)
|
||||||
|
rearmed_count += 1
|
||||||
|
|
||||||
if is_private_msg:
|
if is_private_msg:
|
||||||
message = f"{nick} > Rearmed all players ({rearmed_count} players)"
|
message = f"{nick} > Rearmed all players ({rearmed_count} players)"
|
||||||
@@ -1368,10 +1363,7 @@ class DuckHuntBot:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Rearm the admin themselves (only in channels)
|
# Rearm the admin themselves (only in channels)
|
||||||
player = self.db.get_player(nick)
|
player = self.db.get_player(nick, channel)
|
||||||
if player is None:
|
|
||||||
player = self.db.create_player(nick)
|
|
||||||
self.db.players[nick.lower()] = player
|
|
||||||
|
|
||||||
player['gun_confiscated'] = False
|
player['gun_confiscated'] = False
|
||||||
|
|
||||||
@@ -1434,10 +1426,7 @@ class DuckHuntBot:
|
|||||||
return
|
return
|
||||||
|
|
||||||
target = args[0].lower()
|
target = args[0].lower()
|
||||||
player = self.db.get_player(target)
|
player = self.db.get_player(target, channel)
|
||||||
if player is None:
|
|
||||||
player = self.db.create_player(target)
|
|
||||||
self.db.players[target] = player
|
|
||||||
|
|
||||||
action_func(player)
|
action_func(player)
|
||||||
|
|
||||||
|
|||||||
18
src/game.py
Normal file → Executable file
18
src/game.py
Normal file → Executable file
@@ -284,7 +284,7 @@ class DuckGame:
|
|||||||
|
|
||||||
# If config option enabled, rearm all disarmed players when duck is shot
|
# If config option enabled, rearm all disarmed players when duck is shot
|
||||||
if self.bot.get_config('duck_spawning.rearm_on_duck_shot', False):
|
if self.bot.get_config('duck_spawning.rearm_on_duck_shot', False):
|
||||||
self._rearm_all_disarmed_players()
|
self._rearm_all_disarmed_players(channel)
|
||||||
|
|
||||||
# Check for item drops
|
# Check for item drops
|
||||||
dropped_item = self._check_item_drop(player, duck_type)
|
dropped_item = self._check_item_drop(player, duck_type)
|
||||||
@@ -324,8 +324,8 @@ class DuckGame:
|
|||||||
if random.random() < friendly_fire_chance:
|
if random.random() < friendly_fire_chance:
|
||||||
# Get other armed players in the same channel
|
# Get other armed players in the same channel
|
||||||
armed_players = []
|
armed_players = []
|
||||||
for other_nick, other_player in self.db.players.items():
|
for other_nick, other_player in self.db.get_players_for_channel(channel).items():
|
||||||
if (other_nick.lower() != nick.lower() and
|
if (str(other_nick).lower() != nick.lower() and
|
||||||
not other_player.get('gun_confiscated', False) and
|
not other_player.get('gun_confiscated', False) and
|
||||||
other_player.get('current_ammo', 0) > 0):
|
other_player.get('current_ammo', 0) > 0):
|
||||||
armed_players.append((other_nick, other_player))
|
armed_players.append((other_nick, other_player))
|
||||||
@@ -424,7 +424,7 @@ class DuckGame:
|
|||||||
|
|
||||||
# If config option enabled, rearm all disarmed players when duck is befriended
|
# If config option enabled, rearm all disarmed players when duck is befriended
|
||||||
if self.bot.get_config('rearm_on_duck_shot', False):
|
if self.bot.get_config('rearm_on_duck_shot', False):
|
||||||
self._rearm_all_disarmed_players()
|
self._rearm_all_disarmed_players(channel)
|
||||||
|
|
||||||
self.db.save_database()
|
self.db.save_database()
|
||||||
return {
|
return {
|
||||||
@@ -494,11 +494,11 @@ class DuckGame:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def _rearm_all_disarmed_players(self):
|
def _rearm_all_disarmed_players(self, channel):
|
||||||
"""Rearm all players who have been disarmed (gun confiscated)"""
|
"""Rearm all players who have been disarmed (gun confiscated) in the given channel"""
|
||||||
try:
|
try:
|
||||||
rearmed_count = 0
|
rearmed_count = 0
|
||||||
for player_name, player_data in self.db.players.items():
|
for _player_name, player_data in self.db.get_players_for_channel(channel).items():
|
||||||
if player_data.get('gun_confiscated', False):
|
if player_data.get('gun_confiscated', False):
|
||||||
player_data['gun_confiscated'] = False
|
player_data['gun_confiscated'] = False
|
||||||
# Update magazines based on player level
|
# Update magazines based on player level
|
||||||
@@ -518,7 +518,7 @@ class DuckGame:
|
|||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for player_name, player_data in self.db.players.items():
|
for _ch, _player_name, player_data in self.db.iter_all_players():
|
||||||
effects = player_data.get('temporary_effects', [])
|
effects = player_data.get('temporary_effects', [])
|
||||||
for effect in effects:
|
for effect in effects:
|
||||||
if (effect.get('type') == 'attract_ducks' and
|
if (effect.get('type') == 'attract_ducks' and
|
||||||
@@ -566,7 +566,7 @@ class DuckGame:
|
|||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for player_name, player_data in self.db.players.items():
|
for _ch, player_name, player_data in self.db.iter_all_players():
|
||||||
effects = player_data.get('temporary_effects', [])
|
effects = player_data.get('temporary_effects', [])
|
||||||
active_effects = []
|
active_effects = []
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user