├── data └── .gitkeep ├── logs └── .gitkeep ├── .gitattributes ├── .gitignore ├── requirements.txt ├── utils ├── constants.py ├── settings.py ├── rconutility.py ├── bans.py ├── errorhandling.py ├── whitelist.py ├── pagination.py ├── servermodal.py └── database.py ├── start.bat ├── main.py ├── cogs ├── rcon │ ├── rcon.py │ ├── kits.py │ └── pdefender.py ├── utility │ ├── null.py │ ├── help.py │ └── globalban.py ├── logging │ ├── tracking.py │ ├── events.py │ ├── logplayer.py │ ├── query.py │ ├── backup.py │ └── chat.py └── control │ ├── info.py │ ├── players.py │ ├── server.py │ ├── control.py │ ├── admin.py │ └── whitelist.py ├── README.md ├── cogs-dev ├── savecheck.py └── sftpchat.py └── gamedata └── pals.json /data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.env 3 | *.log 4 | *.db 5 | *.json 6 | /venv/* 7 | /map/* 8 | 9 | !/gamedata/* -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | discord.py==2.5.2 2 | python-dotenv==1.1.1 3 | palworld-api==2.0.0 4 | gamercon-async==1.0.7 5 | aiohttp==3.11.18 6 | aiosqlite==0.21.0 7 | aiocache==0.12.3 -------------------------------------------------------------------------------- /utils/constants.py: -------------------------------------------------------------------------------- 1 | SPHERE_NAME = "Project Sphere" 2 | SPHERE_VERSION = "1.5 Beta" 3 | SPHERE_MESSAGE = f"Running {SPHERE_NAME} v{SPHERE_VERSION}" 4 | SPHERE_THUMBNAIL = "https://www.palbot.gg/assets/images/rexavatar.png" -------------------------------------------------------------------------------- /utils/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | from utils.database import initialize_db 4 | 5 | load_dotenv() 6 | bot_token = os.getenv('BOT_TOKEN', "No token found") 7 | bot_prefix = os.getenv('BOT_PREFIX', "!") 8 | 9 | async def setup_hook(bot): 10 | await initialize_db() 11 | for root, _, files in os.walk("./cogs"): 12 | for filename in files: 13 | if filename.endswith(".py"): 14 | extension = os.path.join(root, filename).replace(os.sep, ".")[2:-3] 15 | await bot.load_extension(extension) 16 | await bot.tree.sync() -------------------------------------------------------------------------------- /utils/rconutility.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from gamercon_async import GameRCON, ClientError, TimeoutError, InvalidPassword 3 | 4 | class RconUtility: 5 | def __init__(self, timeout=30): 6 | self.timeout = timeout 7 | 8 | async def rcon_command(self, host: str, port: int, password: str, command: str): 9 | try: 10 | async with GameRCON(host, port, password, self.timeout) as rcon: 11 | return await rcon.send(command) 12 | except (ClientError, TimeoutError, InvalidPassword) as e: 13 | return f"RCON error: {e}" 14 | except asyncio.TimeoutError: 15 | return "Timed out." 16 | except ConnectionResetError as e: 17 | return f"Connection reset: {e}" 18 | -------------------------------------------------------------------------------- /utils/bans.py: -------------------------------------------------------------------------------- 1 | import aiosqlite 2 | import os 3 | 4 | DATABASE_PATH = os.path.join('data', 'palworld.db') 5 | 6 | async def log_ban(player_id: str, reason: str): 7 | async with aiosqlite.connect(DATABASE_PATH) as db: 8 | await db.execute(""" 9 | INSERT INTO bans (player_id, reason) 10 | VALUES (?, ?) 11 | """, (player_id, reason)) 12 | await db.commit() 13 | 14 | async def fetch_bans(): 15 | async with aiosqlite.connect(DATABASE_PATH) as db: 16 | cursor = await db.execute("SELECT player_id, reason, timestamp FROM bans") 17 | results = await cursor.fetchall() 18 | return results 19 | 20 | async def clear_bans(): 21 | async with aiosqlite.connect(DATABASE_PATH) as db: 22 | await db.execute("DELETE FROM bans") 23 | await db.commit() -------------------------------------------------------------------------------- /start.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | SETLOCAL 3 | 4 | SET PYTHON_CMD=python 5 | 6 | where python >nul 2>&1 7 | IF NOT %ERRORLEVEL%==0 ( 8 | where py >nul 2>&1 9 | IF %ERRORLEVEL%==0 ( 10 | SET PYTHON_CMD=py 11 | ) ELSE ( 12 | echo [Sphere Bot] Python is not installed or not added to PATH. 13 | echo Please install Python and ensure it's accessible via 'python' or 'py'. 14 | pause 15 | EXIT /B 1 16 | ) 17 | ) 18 | 19 | IF NOT EXIST "venv" ( 20 | echo [Sphere Bot] Creating virtual environment... 21 | %PYTHON_CMD% -m venv venv 22 | ) 23 | 24 | echo [Sphere Bot] Activating virtual environment... 25 | call venv\Scripts\activate.bat 26 | 27 | echo ------------------------------------------ 28 | echo [Sphere Bot] Virtual Environment Ready 29 | echo ------------------------------------------ 30 | 31 | title Sphere Bot 32 | 33 | echo [Sphere Bot] Installing/upgrading dependencies... 34 | %PYTHON_CMD% -m pip install --upgrade pip 35 | %PYTHON_CMD% -m pip install setuptools 36 | %PYTHON_CMD% -m pip install -r requirements.txt 37 | 38 | echo [Sphere Bot] Starting bot... 39 | %PYTHON_CMD% main.py 40 | 41 | ENDLOCAL 42 | pause 43 | -------------------------------------------------------------------------------- /utils/errorhandling.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from logging.handlers import RotatingFileHandler 4 | from datetime import datetime 5 | 6 | def setup_logging(): 7 | if not os.path.exists('logs'): 8 | os.makedirs('logs') 9 | 10 | log_filename = f"sphere_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.log" 11 | log_path = os.path.join('logs', log_filename) 12 | 13 | log_handler = RotatingFileHandler( 14 | filename=log_path, 15 | maxBytes=10**7, 16 | backupCount=6, 17 | encoding='utf-8' 18 | ) 19 | log_formatter = logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s') 20 | log_handler.setFormatter(log_formatter) 21 | 22 | root_logger = logging.getLogger() 23 | root_logger.setLevel(logging.INFO) 24 | root_logger.handlers = [] 25 | root_logger.addHandler(log_handler) 26 | 27 | clean_old_logs('logs', 10) 28 | 29 | def clean_old_logs(directory, max_logs): 30 | log_files = sorted( 31 | [os.path.join(directory, f) for f in os.listdir(directory) if f.startswith("sphere_") and f.endswith(".log")], 32 | key=os.path.getctime, 33 | reverse=True 34 | ) 35 | 36 | while len(log_files) > max_logs: 37 | os.remove(log_files.pop()) 38 | 39 | STARTUP_CHECK = "496620796F75207061696420666F72207468697320796F7520676F74207363616D6D65642E205265706F727420697420746F2075732061742068747470733A2F2F70616C626F742E67672F737570706F7274" -------------------------------------------------------------------------------- /utils/whitelist.py: -------------------------------------------------------------------------------- 1 | import aiosqlite 2 | import os 3 | 4 | DATABASE_PATH = os.path.join('data', 'palworld.db') 5 | 6 | async def add_whitelist(player_id: str, whitelisted: bool): 7 | async with aiosqlite.connect(DATABASE_PATH) as db: 8 | await db.execute(""" 9 | INSERT OR REPLACE INTO whitelist (player_id, whitelisted) 10 | VALUES (?, ?) 11 | """, (player_id, whitelisted)) 12 | await db.commit() 13 | 14 | async def remove_whitelist(player_id: str): 15 | async with aiosqlite.connect(DATABASE_PATH) as db: 16 | await db.execute("DELETE FROM whitelist WHERE player_id = ?", (player_id,)) 17 | await db.commit() 18 | 19 | async def is_whitelisted(player_id: str): 20 | async with aiosqlite.connect(DATABASE_PATH) as db: 21 | cursor = await db.execute("SELECT whitelisted FROM whitelist WHERE player_id = ?", (player_id,)) 22 | result = await cursor.fetchone() 23 | if result: 24 | return result[0] 25 | return False 26 | 27 | async def whitelist_set(guild_id: int, server_name: str, enabled: bool): 28 | async with aiosqlite.connect(DATABASE_PATH) as db: 29 | await db.execute(""" 30 | INSERT OR REPLACE INTO whitelist_status (guild_id, server_name, enabled) 31 | VALUES (?, ?, ?) 32 | """, (guild_id, server_name, enabled)) 33 | await db.commit() 34 | 35 | async def whitelist_get(guild_id: int, server_name: str): 36 | async with aiosqlite.connect(DATABASE_PATH) as db: 37 | cursor = await db.execute("SELECT enabled FROM whitelist_status WHERE guild_id = ? AND server_name = ?", (guild_id, server_name)) 38 | result = await cursor.fetchone() 39 | if result: 40 | return result[0] 41 | return False 42 | -------------------------------------------------------------------------------- /utils/pagination.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | # I really need to make a pagination pip package 4 | 5 | class Pagination: 6 | def __init__(self, items, page_size=10): 7 | self.items = items 8 | self.page_size = page_size 9 | self.total_pages = len(items) // page_size + (1 if len(items) % page_size > 0 else 0) 10 | 11 | def get_page(self, page_number): 12 | start = (page_number - 1) * self.page_size 13 | end = start + self.page_size 14 | return self.items[start:end] 15 | 16 | class PaginationView(discord.ui.View): 17 | def __init__(self, paginator, current_page, embed_creator): 18 | super().__init__() 19 | self.paginator = paginator 20 | self.current_page = current_page 21 | self.embed_creator = embed_creator 22 | self.add_pagination_buttons() 23 | 24 | def add_pagination_buttons(self): 25 | if self.current_page > 1: 26 | self.add_item(PaginationButton("Previous", -1, self)) 27 | 28 | if self.current_page < self.paginator.total_pages: 29 | self.add_item(PaginationButton("Next", 1, self)) 30 | 31 | async def update_page(self, interaction, page_delta): 32 | self.current_page += page_delta 33 | new_embed = self.embed_creator(self.paginator.get_page(self.current_page), self.current_page, self.paginator.total_pages) 34 | await interaction.response.edit_message(embed=new_embed, view=PaginationView(self.paginator, self.current_page, self.embed_creator)) 35 | 36 | class PaginationButton(discord.ui.Button): 37 | def __init__(self, label, page_delta, pagination_view): 38 | super().__init__(label=label, style=discord.ButtonStyle.primary) 39 | self.page_delta = page_delta 40 | self.pagination_view = pagination_view 41 | 42 | async def callback(self, interaction: discord.Interaction): 43 | await self.pagination_view.update_page(interaction, self.page_delta) 44 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | import utils.settings as settings 4 | from utils.errorhandling import setup_logging, STARTUP_CHECK 5 | import utils.constants as c 6 | import logging 7 | 8 | setup_logging() 9 | 10 | intents = discord.Intents.all() 11 | bot = commands.Bot(command_prefix=settings.bot_prefix, intents=intents) 12 | 13 | @bot.command() 14 | async def ping(ctx): 15 | await ctx.send(f'Pong! {round(bot.latency * 1000)}ms') 16 | 17 | bot.setup_hook = lambda: settings.setup_hook(bot) 18 | 19 | @bot.event 20 | async def on_ready(): 21 | print(f'{c.SPHERE_MESSAGE}') 22 | print(f"Your bot is in {len(bot.guilds)} servers and serving {len(bot.users)} users.") 23 | print(f"Invite link: {discord.utils.oauth_url(bot.user.id, permissions=discord.Permissions(permissions=8))}") 24 | print(f'Logged in as {bot.user} (ID: {bot.user.id})') 25 | await bot.change_presence(activity=discord.Game(name="Palworld")) 26 | 27 | @bot.command() 28 | @commands.is_owner() 29 | async def load(ctx, extension): 30 | try: 31 | await bot.load_extension(f"cogs.{extension}") 32 | await ctx.send(f"Loaded {extension} successfully.") 33 | except Exception as e: 34 | await ctx.send(f"Failed to load {extension}. {type(e).__name__}: {e}") 35 | 36 | @bot.command() 37 | @commands.is_owner() 38 | async def unload(ctx, extension): 39 | try: 40 | await bot.unload_extension(f"cogs.{extension}") 41 | await ctx.send(f"Unloaded {extension} successfully.") 42 | except Exception as e: 43 | await ctx.send(f"Failed to unload {extension}. {type(e).__name__}: {e}") 44 | 45 | @bot.command() 46 | @commands.is_owner() 47 | async def reload(ctx, extension): 48 | try: 49 | await bot.unload_extension(f"cogs.{extension}") 50 | await bot.load_extension(f"cogs.{extension}") 51 | await ctx.send(f"Reloaded {extension} successfully.") 52 | except Exception as e: 53 | await ctx.send(f"Failed to reload {extension}. {type(e).__name__}: {e}") 54 | 55 | if __name__ == '__main__': 56 | logging.info(bytes.fromhex(STARTUP_CHECK).decode()) 57 | bot.run(settings.bot_token) -------------------------------------------------------------------------------- /utils/servermodal.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | class AddServerModal(discord.ui.Modal): 4 | def __init__(self, *args, **kwargs): 5 | super().__init__(*args, **kwargs) 6 | 7 | self.add_item(discord.ui.TextInput(label="Server Name", placeholder="Enter your server name here")) 8 | self.add_item(discord.ui.TextInput(label="Host", placeholder="Enter your server's IP address here")) 9 | self.add_item(discord.ui.TextInput(label="Admin Password", placeholder="Enter your server's admin password here")) 10 | self.add_item(discord.ui.TextInput(label="REST API Port", placeholder="Enter your server's RESTAPI port here", style=discord.TextStyle.short)) 11 | self.add_item(discord.ui.TextInput(label="RCON Port", placeholder="Enter your server's RCON port here", style=discord.TextStyle.short)) 12 | 13 | async def on_submit(self, interaction: discord.Interaction): 14 | pass 15 | 16 | class ChatSetupModal(discord.ui.Modal): 17 | def __init__(self, *args, **kwargs): 18 | super().__init__(*args, **kwargs) 19 | 20 | self.add_item(discord.ui.TextInput(label="Chatlog Channel ID", placeholder="Channel ID for logs and relay")) 21 | self.add_item(discord.ui.TextInput(label="Chatlog Path", placeholder="Path to your server logs")) 22 | self.add_item(discord.ui.TextInput(label="Webhook URL", placeholder="Webhook to post chat messages")) 23 | 24 | async def on_submit(self, interaction: discord.Interaction): 25 | pass 26 | 27 | class BackupModal(discord.ui.Modal): 28 | def __init__(self, *args, on_submit_callback=None, **kwargs): 29 | super().__init__(*args, **kwargs) 30 | self.on_submit_callback = on_submit_callback 31 | self.add_item(discord.ui.TextInput(label="Channel ID", placeholder="Destination channel ID")) 32 | self.add_item(discord.ui.TextInput(label="Save Path", placeholder="Full path to save folder")) 33 | self.add_item(discord.ui.TextInput(label="Interval (minutes)", placeholder="Backup interval in minutes")) 34 | 35 | async def on_submit(self, interaction: discord.Interaction): 36 | await self.on_submit_callback(interaction, self) -------------------------------------------------------------------------------- /cogs/rcon/rcon.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | from discord import app_commands 4 | from utils.rconutility import RconUtility 5 | from utils.database import fetch_server_details, server_autocomplete 6 | 7 | class RconCog(commands.Cog): 8 | def __init__(self, bot: commands.Bot): 9 | self.bot = bot 10 | self.rcon = RconUtility() 11 | self.servers = [] 12 | bot.loop.create_task(self.load_servers()) 13 | 14 | async def load_servers(self): 15 | self.servers = [] 16 | 17 | async def get_server_info(self, guild_id: int, server_name: str): 18 | details = await fetch_server_details(guild_id, server_name) 19 | if details: 20 | return {"host": details[2], "password": details[3], "port": details[5]} 21 | 22 | async def autocomplete_server(self, interaction: discord.Interaction, current: str): 23 | guild_id = interaction.guild.id if interaction.guild else 0 24 | server_names = await server_autocomplete(guild_id, current) 25 | return [app_commands.Choice(name=name, value=name) for name in server_names[:25]] 26 | 27 | @app_commands.command(name="rcon", description="Send an RCON command to a server") 28 | @app_commands.describe(command="RCON Command", server="Server") 29 | @app_commands.autocomplete(server=autocomplete_server) 30 | @app_commands.default_permissions(administrator=True) 31 | @app_commands.guild_only() 32 | async def rconcommand(self, interaction: discord.Interaction, command: str, server: str): 33 | await interaction.response.defer(ephemeral=True) 34 | if not interaction.guild: 35 | await interaction.followup.send("No guild.", ephemeral=True) 36 | return 37 | info = await self.get_server_info(interaction.guild.id, server) 38 | if not info: 39 | await interaction.followup.send(f"Server not found: {server}", ephemeral=True) 40 | return 41 | response = await self.rcon.rcon_command(info["host"], info["port"], info["password"], f"{command}") 42 | await interaction.followup.send(response, ephemeral=True) 43 | 44 | async def setup(bot: commands.Bot): 45 | await bot.add_cog(RconCog(bot)) 46 | -------------------------------------------------------------------------------- /cogs/utility/null.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands, tasks 3 | from utils.database import fetch_all_servers, fetch_logchannel 4 | from palworld_api import PalworldAPI 5 | import logging 6 | 7 | class NullPlayerCheck(commands.Cog): 8 | def __init__(self, bot): 9 | self.bot = bot 10 | self.check_players.start() 11 | 12 | def cog_unload(self): 13 | self.check_players.cancel() 14 | 15 | # Temporary fix for null players joining without a valid ID. 16 | @tasks.loop(seconds=10) 17 | async def check_players(self): 18 | servers = await fetch_all_servers() 19 | for server in servers: 20 | guild_id, server_name, host, password, api_port, rcon_port = server 21 | log_channel_id = await fetch_logchannel(guild_id, server_name) 22 | log_channel = self.bot.get_channel(log_channel_id) if log_channel_id else None 23 | 24 | try: 25 | api = PalworldAPI(f"http://{host}:{api_port}", password) 26 | player_list = await api.get_player_list() 27 | for player in player_list['players']: 28 | playerid = player['userId'] 29 | if "null_" in playerid: 30 | await api.kick_player(playerid, "Invalid ID detected.") 31 | logging.info(f"Kicked player {playerid} from server '{server_name}' due to invalid ID.") 32 | 33 | if log_channel: 34 | embed = discord.Embed( 35 | title="Invalid ID Detected", 36 | description=f"Player `{playerid}` was kicked from server {server_name} due to an invalid ID.", 37 | color=discord.Color.red(), 38 | timestamp=discord.utils.utcnow() 39 | ) 40 | await log_channel.send(embed=embed) 41 | 42 | # logging.info(f"Checked null players for server '{server_name}'.") 43 | except Exception as e: 44 | logging.error(f"Error checking null players for server '{server_name}': {str(e)}") 45 | 46 | @check_players.before_loop 47 | async def before_check_players(self): 48 | await self.bot.wait_until_ready() 49 | 50 | async def setup(bot): 51 | await bot.add_cog(NullPlayerCheck(bot)) 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Sphere Palworld 2 | > [!WARNING] 3 | > This bot is still in development, but it's in a usable state. No support will be provided unless it's for bug reports or feature requests. 4 | 5 | This bot is designed to be a server management replacement for my current [Palbot](https://github.com/dkoz/palworld-palbot) project. Unlike Palbot, which only supports Steam servers, this new project is created to support all platforms (steam, xbox, mac, and ps5). 6 | 7 | ## Features: 8 | - **Server Management**: Ability to control your servers directly from the bot. 9 | - **Player Logging**: Log extensive information about players who are active on your servers. 10 | - **Connection Events**: Logs and reports players connecting to the server. 11 | - **Ban List Logger**: When players are banned through the bot, it will be logged in the SQL database with the reason. 12 | - **Whitelist Management**: Allows you to enable a whitelist for your server so only select users can play. 13 | - **Administration Control**: Allows you to kick, ban, and manage players on your server directly from the bot. 14 | - **Server Query**: Allows you query servers added to the bot. 15 | - **Global Banlist**: This will allow you to global ban across all your servers using the [Sphere Banlist API](https://github.com/projectsphere/banlist-api). 16 | - **PalDefender**: Gives basic functionality of PalDefender rcon commands. 17 | - **Null Check**: This will check for players joining without a valid user id and kick them. (Experimental) 18 | - **Cross Server Chat**: Send and receive chats from the server to discord and vice versa. 19 | - **Scheduled Backups**: Create backups of your server and send them to a discord channel at timed intervals. 20 | 21 | ## Environment Variables 22 | - `BOT_TOKEN`: Your discord bot token generated on the [Discord Developer Portal](https://discord.com/developers/applications). 23 | - `BOT_PREFIX`: The prefix used for non slash commands. Example `!` 24 | - `API_URL`: API URL if you setup the [Banlist API](https://github.com/projectsphere/banlist-api). 25 | - `API_KEY`: The API Key you set for your banlist. This key is used to access the endpoints securely. 26 | 27 | ## Installation 28 | 1. Create a `.env` file and fill out your `BOT_TOKEN` and `BOT_PREFIX` 29 | 2. Run the bot with `python main.py` 30 | 3. Use `/help` to see all available commands on the bot. 31 | 32 | ## This project runs my libaries. 33 | - **Palworld API Wrapper** - A python library that acts as a wrapper for the Palworld server REST API. 34 | - **GameRCON** - An asynchronous RCON library designed to handle multiple RCON tasks across numerous servers. -------------------------------------------------------------------------------- /cogs/utility/help.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | from discord import app_commands 3 | import discord 4 | import sys 5 | import utils.constants as c 6 | from utils.pagination import Pagination, PaginationView 7 | 8 | class HelpCog(commands.Cog): 9 | def __init__(self, bot): 10 | self.bot = bot 11 | 12 | def get_commands_list(self, app_commands, prefix=''): 13 | lines = [] 14 | for cmd in app_commands: 15 | if isinstance(cmd, discord.app_commands.Command): 16 | lines.append(f"`/{cmd.name}` - {cmd.description}") 17 | elif isinstance(cmd, discord.app_commands.Group): 18 | lines.append(f"`/{cmd.name}` - {cmd.description}") 19 | lines.extend(self.get_commands_list(list(cmd.walk_commands()), f"{prefix}{cmd.name} ")) 20 | return lines 21 | 22 | @app_commands.command(name="help", description="Shows help information for all commands.") 23 | async def help(self, interaction: discord.Interaction): 24 | await interaction.response.defer(ephemeral=True) 25 | commands = list(self.bot.tree.walk_commands()) 26 | commands_list = self.get_commands_list(commands) 27 | paginator = Pagination(commands_list, page_size=10) 28 | page = 1 29 | embed = self.help_embed(paginator.get_page(page), page, paginator.total_pages) 30 | view = PaginationView(paginator, page, self.help_embed) 31 | await interaction.followup.send(embed=embed, view=view) 32 | 33 | def help_embed(self, commands_list, page, total_pages): 34 | description = "\n".join(commands_list) 35 | embed = discord.Embed(title=f"Help Menu", description=description, color=discord.Color.blurple()) 36 | return embed 37 | 38 | @app_commands.command(name="about", description="Shows information about the bot.") 39 | async def about(self, interaction: discord.Interaction): 40 | await interaction.response.defer(ephemeral=True) 41 | bot_owner = await self.bot.application_info() 42 | 43 | embed = discord.Embed( 44 | title=c.SPHERE_NAME, 45 | description="This bot was created for managing palworld servers. Keep up to date on the latest changes and updates on the [GitHub](https://github.com/projectsphere/sphere).", 46 | color=discord.Color.blurple(), 47 | url="https://github.com/projectsphere/sphere" 48 | ) 49 | embed.add_field(name="Instance Owner", value=f"{bot_owner.owner.name}", inline=True) 50 | embed.add_field(name="Version", value=f"v{c.SPHERE_VERSION}", inline=True) 51 | embed.add_field(name="Python", value=f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", inline=True) 52 | embed.set_thumbnail(url=c.SPHERE_THUMBNAIL) 53 | await interaction.followup.send(embed=embed) 54 | 55 | async def setup(bot): 56 | await bot.add_cog(HelpCog(bot)) 57 | -------------------------------------------------------------------------------- /cogs/logging/tracking.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands, tasks 3 | from discord import app_commands 4 | from utils.database import fetch_all_servers, get_tracking, set_tracking 5 | from palworld_api import PalworldAPI 6 | import logging 7 | 8 | class PlayerTrackerCog(commands.Cog): 9 | def __init__(self, bot): 10 | self.bot = bot 11 | self.player_tracking.start() 12 | 13 | def cog_unload(self): 14 | self.player_tracking.cancel() 15 | 16 | @tasks.loop(minutes=2) 17 | async def player_tracking(self): 18 | try: 19 | guilds = await get_tracking() 20 | if not guilds: 21 | return 22 | 23 | servers = await fetch_all_servers() 24 | total_players = 0 25 | 26 | for server in servers: 27 | try: 28 | guild_id, _, host, password, api_port, _ = server 29 | if guild_id not in guilds: 30 | continue 31 | api = PalworldAPI(f"http://{host}:{api_port}", password) 32 | metrics = await api.get_server_metrics() 33 | total_players += metrics.get('currentplayernum', 0) 34 | except Exception as e: 35 | logging.error(f"Error fetching metrics from {server[1]}: {e}") 36 | continue 37 | 38 | try: 39 | activity = discord.Activity(type=discord.ActivityType.watching, name=f"{total_players} Players") 40 | await self.bot.change_presence(activity=activity) 41 | logging.info(f"Updated presence to: Watching {total_players} Players") 42 | except Exception as e: 43 | logging.error(f"Error updating bot presence: {e}") 44 | 45 | except Exception as e: 46 | logging.error(f"Error in player_tracking loop: {e}") 47 | 48 | async def bool_autocomplete(self, interaction: discord.Interaction, current: str): 49 | return [ 50 | app_commands.Choice(name="True", value="true"), 51 | app_commands.Choice(name="False", value="false") 52 | ] 53 | 54 | @app_commands.command(name="trackplayers", description="Enable or disable status-based player tracking.") 55 | @app_commands.describe(status="Enable or disable tracking") 56 | @app_commands.autocomplete(status=bool_autocomplete) 57 | @app_commands.default_permissions(administrator=True) 58 | @app_commands.guild_only() 59 | async def trackplayers(self, interaction: discord.Interaction, status: str): 60 | try: 61 | enabled = status.lower() == "true" 62 | await set_tracking(interaction.guild.id, enabled) 63 | await interaction.response.send_message(f"Player tracking set to `{enabled}` for this server.", ephemeral=True) 64 | except Exception as e: 65 | await interaction.response.send_message("Failed to update tracking status.", ephemeral=True) 66 | logging.error(f"Failed to set tracking: {e}") 67 | 68 | @player_tracking.before_loop 69 | async def before_loop(self): 70 | await self.bot.wait_until_ready() 71 | 72 | async def setup(bot): 73 | await bot.add_cog(PlayerTrackerCog(bot)) 74 | -------------------------------------------------------------------------------- /cogs/control/info.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | from discord import app_commands 4 | from utils.database import server_autocomplete, fetch_server_details 5 | from palworld_api import PalworldAPI 6 | import logging 7 | 8 | class ServerInfoCog(commands.Cog): 9 | def __init__(self, bot): 10 | self.bot = bot 11 | 12 | async def server_autocomplete(self, interaction: discord.Interaction, current: str): 13 | guild_id = interaction.guild.id 14 | server_names = await server_autocomplete(guild_id, current) 15 | choices = [app_commands.Choice(name=name, value=name) for name in server_names] 16 | return choices 17 | 18 | @app_commands.command(name="serverinfo", description="Get server info from the API.") 19 | @app_commands.describe(server="The name of the server to get info for.") 20 | @app_commands.default_permissions(administrator=True) 21 | @app_commands.autocomplete(server=server_autocomplete) 22 | @app_commands.guild_only() 23 | async def server_info(self, interaction: discord.Interaction, server: str): 24 | await interaction.response.defer(thinking=True, ephemeral=True) 25 | try: 26 | guild_id = interaction.guild.id 27 | server_config = await fetch_server_details(guild_id, server) 28 | if not server_config: 29 | await interaction.followup.send(f"Server '{server}' configuration not found.", ephemeral=True) 30 | return 31 | 32 | host = server_config[2] 33 | password = server_config[3] 34 | api_port = server_config[4] 35 | 36 | api = PalworldAPI(f"http://{host}:{api_port}", password) 37 | server_info = await api.get_server_info() 38 | server_metrics = await api.get_server_metrics() 39 | 40 | embed = discord.Embed(title=f"{server_info.get('servername', server)}", description=f"{server_info.get('description', 'N/A')}", color=discord.Color.blurple()) 41 | embed.add_field(name="Players", value=f"{server_metrics.get('currentplayernum', 'N/A')}/{server_metrics.get('maxplayernum', 'N/A')}", inline=True) 42 | embed.add_field(name="Version", value=server_info.get('version', 'N/A'), inline=True) 43 | embed.add_field(name="Days Passed", value=server_metrics.get('days', 'N/A'), inline=True) 44 | embed.add_field(name="Uptime", value=f"{int(server_metrics.get('uptime', 'N/A') / 60)} minutes", inline=True) 45 | embed.add_field(name="FPS", value=server_metrics.get('serverfps', 'N/A'), inline=True) 46 | embed.add_field(name="Latency", value=f"{server_metrics.get('serverframetime', 'N/A'):.2f} ms", inline=True) 47 | embed.add_field(name="WorldGUID", value=f"`{server_info.get('worldguid', 'N/A')}`", inline=False) 48 | embed.set_thumbnail(url="https://www.palbot.gg/images/rexavatar.png") 49 | 50 | await interaction.followup.send(embed=embed) 51 | except Exception as e: 52 | await interaction.followup.send(f"Error getting server info: {str(e)}", ephemeral=True) 53 | logging.error(f"Error getting server info: {str(e)}") 54 | 55 | async def setup(bot): 56 | await bot.add_cog(ServerInfoCog(bot)) 57 | -------------------------------------------------------------------------------- /cogs/control/players.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | from discord import app_commands 4 | from utils.database import fetch_server_details, server_autocomplete 5 | from palworld_api import PalworldAPI 6 | import logging 7 | 8 | class PlayersCog(commands.Cog): 9 | def __init__(self, bot): 10 | self.bot = bot 11 | 12 | async def get_api_instance(self, guild_id, server_name): 13 | server_config = await fetch_server_details(guild_id, server_name) 14 | if not server_config: 15 | return None, f"Server '{server_name}' configuration not found." 16 | 17 | host = server_config[2] 18 | password = server_config[3] 19 | api_port = server_config[4] 20 | 21 | api = PalworldAPI(f"http://{host}:{api_port}", password) 22 | return api, None 23 | 24 | async def server_autocomplete(self, interaction: discord.Interaction, current: str): 25 | guild_id = interaction.guild.id 26 | server_names = await server_autocomplete(guild_id, current) 27 | choices = [app_commands.Choice(name=name, value=name) for name in server_names] 28 | return choices 29 | 30 | @app_commands.command(name="players", description="Get the full player list of a selected server.") 31 | @app_commands.describe(server="The name of the server to retrieve the player list from") 32 | @app_commands.autocomplete(server=server_autocomplete) 33 | @app_commands.default_permissions(administrator=True) 34 | @app_commands.guild_only() 35 | async def player_list(self, interaction: discord.Interaction, server: str): 36 | await interaction.response.defer(thinking=True, ephemeral=True) 37 | try: 38 | api, error = await self.get_api_instance(interaction.guild.id, server) 39 | if error: 40 | await interaction.followup.send(error, ephemeral=True) 41 | return 42 | 43 | player_list = await api.get_player_list() 44 | if player_list and 'players' in player_list: 45 | embed = self.playerlist_embed(server, player_list['players']) 46 | await interaction.followup.send(embed=embed, ephemeral=True) 47 | else: 48 | await interaction.followup.send(f"No players found on server '{server}'.", ephemeral=True) 49 | logging.info(f"No players found on server '{server}'.") 50 | except Exception as e: 51 | await interaction.followup.send(f"An unexpected error occurred: {str(e)}", ephemeral=True) 52 | logging.error(f"An unexpected error occurred while fetching player list: {str(e)}") 53 | 54 | def playerlist_embed(self, server_name, players): 55 | embed = discord.Embed(title=f"Player List for {server_name}", color=discord.Color.green()) 56 | 57 | player_names = "\n".join([f"`{player['name']} ({str(player['level'])})`" for player in players]) 58 | player_ids = "\n".join([f"`{player['userId']}`" for player in players]) 59 | player_location = "\n".join([f"`{player['location_x']}`,`{player['location_y']}`" for player in players]) 60 | 61 | embed.add_field(name="Name", value=player_names if player_names else "No players online", inline=True) 62 | embed.add_field(name="PlayerID", value=player_ids if player_ids else "No players online", inline=True) 63 | embed.add_field(name="Location", value=player_location if player_location else "No players online", inline=True) 64 | 65 | return embed 66 | 67 | async def setup(bot): 68 | await bot.add_cog(PlayersCog(bot)) 69 | -------------------------------------------------------------------------------- /cogs-dev/savecheck.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands, tasks 3 | import os 4 | import datetime 5 | import logging 6 | from palworld_api import PalworldAPI 7 | from utils.database import fetch_all_servers 8 | 9 | class SaveMonitor(commands.Cog): 10 | def __init__(self, bot): 11 | self.bot = bot 12 | self.last_mod_time = None 13 | self.first_check_time = None 14 | self.failure_count = 0 15 | self.failure_threshold = 3 16 | self.monitor_loop.start() 17 | 18 | def cog_unload(self): 19 | self.monitor_loop.cancel() 20 | 21 | @tasks.loop(seconds=60) 22 | async def monitor_loop(self): 23 | try: 24 | save_path = os.getenv("SAVE_PATH") 25 | server_name = os.getenv("SERVER_NAME") 26 | 27 | if not save_path or not server_name: 28 | return 29 | 30 | servers = await fetch_all_servers() 31 | target = next((s for s in servers if s[1] == server_name), None) 32 | if not target: 33 | return 34 | 35 | _, _, host, password, api_port, _ = target 36 | level_sav = os.path.join(save_path, "Level.sav") 37 | 38 | if not os.path.exists(level_sav): 39 | return 40 | 41 | now = datetime.datetime.utcnow().timestamp() 42 | mod_time = os.path.getmtime(level_sav) 43 | 44 | if self.first_check_time is None: 45 | self.first_check_time = now 46 | self.last_mod_time = mod_time 47 | return 48 | 49 | if now - self.first_check_time < 300: 50 | return 51 | 52 | if now - mod_time > 300 and self.last_mod_time == mod_time: 53 | self.failure_count += 1 54 | logging.warning(f"Detected save stall attempt {self.failure_count}/{self.failure_threshold} for '{server_name}'") 55 | else: 56 | self.failure_count = 0 57 | 58 | self.last_mod_time = mod_time 59 | 60 | if self.failure_count >= self.failure_threshold: 61 | api = PalworldAPI(f"http://{host}:{api_port}", password) 62 | await api.shutdown_server(30, "Save stalled! Restarting in 30 seconds!") 63 | logging.info(f"Server '{server_name}' save file is stalled. Restarting server.") 64 | self.failure_count = 0 65 | self.first_check_time = now 66 | 67 | except Exception as e: 68 | logging.exception(f"Exception occurred in save monitor loop: {e}") 69 | 70 | @monitor_loop.before_loop 71 | async def before_monitor_loop(self): 72 | await self.bot.wait_until_ready() 73 | try: 74 | save_path = os.getenv("SAVE_PATH") 75 | server_name = os.getenv("SERVER_NAME") 76 | 77 | if not save_path or not server_name: 78 | self.monitor_loop.cancel() 79 | return 80 | 81 | servers = await fetch_all_servers() 82 | target = next((s for s in servers if s[1] == server_name), None) 83 | if not target: 84 | self.monitor_loop.cancel() 85 | return 86 | 87 | _, _, host, password, api_port, _ = target 88 | api = PalworldAPI(f"http://{host}:{api_port}", password) 89 | await api.get_server_info() 90 | except Exception as e: 91 | logging.error(f"Failed to contact API. Canceling monitor: {e}") 92 | self.monitor_loop.cancel() 93 | 94 | async def setup(bot): 95 | await bot.add_cog(SaveMonitor(bot)) 96 | -------------------------------------------------------------------------------- /cogs/control/server.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | from discord import app_commands 3 | import discord 4 | from utils.database import ( 5 | add_server, 6 | remove_server, 7 | server_autocomplete, 8 | remove_whitelist_status, 9 | delete_chat, 10 | del_backup, 11 | delete_query, 12 | remove_logchannel 13 | ) 14 | from utils.servermodal import AddServerModal 15 | import logging 16 | 17 | class ServerManagementCog(commands.Cog): 18 | def __init__(self, bot): 19 | self.bot = bot 20 | 21 | # The devs need a "health" api endpoint to check if RESTAPI is up... 22 | @app_commands.command(name="addserver", description="Add a server configuration") 23 | @app_commands.default_permissions(administrator=True) 24 | @app_commands.guild_only() 25 | async def add_server_command(self, interaction: discord.Interaction): 26 | modal = AddServerModal(title="Add Server") 27 | 28 | async def on_submit_override(modal_interaction: discord.Interaction): 29 | await modal_interaction.response.defer(ephemeral=True) 30 | 31 | server_name = modal.children[0].value 32 | host = modal.children[1].value 33 | password = modal.children[2].value 34 | api_port = int(modal.children[3].value) if modal.children[3].value else None 35 | rcon_port = int(modal.children[4].value) if modal.children[4].value else None 36 | 37 | try: 38 | await add_server( 39 | interaction.guild_id, 40 | server_name, 41 | host, 42 | password, 43 | api_port, 44 | rcon_port 45 | ) 46 | await modal_interaction.followup.send("Server added successfully.", ephemeral=True) 47 | except Exception as e: 48 | await modal_interaction.followup.send(f"Failed to add server: {e}", ephemeral=True) 49 | logging.error(f"Failed to add server: {e}") 50 | 51 | modal.on_submit = on_submit_override 52 | await interaction.response.send_modal(modal) 53 | 54 | async def server_names(self, interaction: discord.Interaction, current: str): 55 | guild_id = interaction.guild.id 56 | server_names = await server_autocomplete(guild_id, current) 57 | return [app_commands.Choice(name=name, value=name) for name in server_names] 58 | 59 | @app_commands.command(name="removeserver", description="Remove a server configuration") 60 | @app_commands.autocomplete(server=server_names) 61 | @app_commands.describe(server="Server to remove") 62 | @app_commands.default_permissions(administrator=True) 63 | @app_commands.guild_only() 64 | async def remove_server_command(self, interaction: discord.Interaction, server: str): 65 | await interaction.response.defer(ephemeral=True) 66 | try: 67 | await remove_server(interaction.guild_id, server) 68 | await remove_whitelist_status(interaction.guild_id, server) 69 | await delete_chat(interaction.guild_id, server) 70 | await del_backup(interaction.guild_id, server) 71 | await delete_query(interaction.guild_id, server) 72 | await remove_logchannel(interaction.guild_id, server) 73 | await interaction.followup.send("Server removed successfully.") 74 | except Exception as e: 75 | await interaction.followup.send(f"Failed to remove server: {e}", ephemeral=True) 76 | logging.error(f"Failed to remove server: {e}") 77 | 78 | async def setup(bot): 79 | await bot.add_cog(ServerManagementCog(bot)) 80 | -------------------------------------------------------------------------------- /cogs/logging/events.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands, tasks 3 | from discord import app_commands 4 | from utils.database import ( 5 | fetch_all_servers, 6 | add_logchannel, 7 | remove_logchannel, 8 | fetch_logchannel, 9 | server_autocomplete 10 | ) 11 | from palworld_api import PalworldAPI 12 | import logging 13 | 14 | class EventsCog(commands.Cog): 15 | def __init__(self, bot): 16 | self.bot = bot 17 | self.player_cache = {} 18 | self.log_players.start() 19 | 20 | def cog_unload(self): 21 | self.log_players.cancel() 22 | 23 | @tasks.loop(seconds=20) 24 | async def log_players(self): 25 | servers = await fetch_all_servers() 26 | for server in servers: 27 | guild_id, server_name, host, password, api_port, rcon_port = server 28 | log_channel_id = await fetch_logchannel(guild_id, server_name) 29 | if log_channel_id: 30 | channel = self.bot.get_channel(log_channel_id) 31 | if channel: 32 | try: 33 | api = PalworldAPI(f"http://{host}:{api_port}", password) 34 | player_list = await api.get_player_list() 35 | current_players = {(player['userId'], player['accountName']) for player in player_list['players']} 36 | 37 | if server_name not in self.player_cache: 38 | self.player_cache[server_name] = current_players 39 | continue 40 | 41 | old_players = self.player_cache[server_name] 42 | joined_players = current_players - old_players 43 | left_players = old_players - current_players 44 | 45 | for userId, accountName in joined_players: 46 | join_text = f"Player `{accountName} ({userId})` has joined {server_name}." 47 | join = discord.Embed(title="Player Joined", description=join_text , color=discord.Color.green(), timestamp=discord.utils.utcnow()) 48 | await channel.send(embed=join) 49 | for userId, accountName in left_players: 50 | left_text = f"Player `{accountName} ({userId})` has left {server_name}." 51 | left = discord.Embed(title="Player Left", description=left_text, color=discord.Color.red(), timestamp=discord.utils.utcnow()) 52 | await channel.send(embed=left) 53 | 54 | self.player_cache[server_name] = current_players 55 | except Exception as e: 56 | logging.error(f"Issues logging player on '{server_name}': {str(e)}") 57 | 58 | @log_players.before_loop 59 | async def before_log_players(self): 60 | await self.bot.wait_until_ready() 61 | 62 | async def server_names(self, interaction: discord.Interaction, current: str): 63 | guild_id = interaction.guild.id 64 | server_names = await server_autocomplete(guild_id, current) 65 | return [app_commands.Choice(name=name, value=name) for name in server_names] 66 | 67 | log_group = app_commands.Group(name="logs", description="Log player join/leave events", default_permissions=discord.Permissions(administrator=True), guild_only=True) 68 | 69 | @log_group.command(name="set", description="Set the logging channel for player join/leave events") 70 | @app_commands.describe(server="The name of the server", channel="The channel to log events in") 71 | @app_commands.autocomplete(server=server_names) 72 | async def set_logchannel(self, interaction: discord.Interaction, server: str, channel: discord.TextChannel): 73 | await add_logchannel(interaction.guild.id, channel.id, server) 74 | await interaction.response.send_message(f"Log channel for server '{server}' set to {channel.mention}.", ephemeral=True) 75 | 76 | @log_group.command(name="remove", description="Remove the logging channel for player join/leave events") 77 | @app_commands.describe(server="The name of the server") 78 | @app_commands.autocomplete(server=server_names) 79 | async def delete_logchannel(self, interaction: discord.Interaction, server: str): 80 | await remove_logchannel(interaction.guild.id, server) 81 | await interaction.response.send_message(f"Log channel for server '{server}' removed.", ephemeral=True) 82 | 83 | async def setup(bot): 84 | await bot.add_cog(EventsCog(bot)) 85 | -------------------------------------------------------------------------------- /cogs/logging/logplayer.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands, tasks 3 | from discord import app_commands 4 | import datetime 5 | from utils.database import ( 6 | add_player, 7 | fetch_all_servers, 8 | fetch_player, 9 | player_autocomplete, 10 | track_sessions, 11 | get_player_session 12 | ) 13 | from utils.whitelist import is_whitelisted 14 | from palworld_api import PalworldAPI 15 | import logging 16 | 17 | class PlayerLoggingCog(commands.Cog): 18 | def __init__(self, bot): 19 | self.bot = bot 20 | self.log_players.start() 21 | 22 | def cog_unload(self): 23 | self.log_players.cancel() 24 | 25 | @tasks.loop(seconds=30) 26 | async def log_players(self): 27 | servers = await fetch_all_servers() 28 | now = datetime.datetime.now(datetime.timezone.utc).isoformat() 29 | 30 | if not hasattr(self, 'server_online_cache'): 31 | self.server_online_cache = {} 32 | 33 | for server in servers: 34 | guild_id, server_name, host, password, api_port, rcon_port = server 35 | try: 36 | api = PalworldAPI(f"http://{host}:{api_port}", password) 37 | player_list = await api.get_player_list() 38 | current_online = set(player['userId'] for player in player_list['players']) 39 | previous_online = self.server_online_cache.get(server_name, set()) 40 | self.server_online_cache[server_name] = current_online 41 | 42 | for player in player_list['players']: 43 | await add_player(player) 44 | 45 | await track_sessions(current_online, previous_online, now) 46 | 47 | except Exception as e: 48 | if server_name in self.server_online_cache: 49 | await track_sessions(set(), self.server_online_cache[server_name], now) 50 | del self.server_online_cache[server_name] 51 | logging.error(f"API unreachable for '{server_name}', sessions ended for tracked users: {str(e)}") 52 | 53 | async def player_autocomplete(self, interaction: discord.Interaction, current: str): 54 | players = await player_autocomplete(current) 55 | choices = [ 56 | app_commands.Choice(name=f"{player[1]} (ID: {player[0]})", value=player[0]) 57 | for player in players[:25] 58 | ] 59 | return choices 60 | 61 | @app_commands.command(name="lookup", description="Fetch and display player information") 62 | @app_commands.autocomplete(user=player_autocomplete) 63 | @app_commands.default_permissions(administrator=True) 64 | @app_commands.guild_only() 65 | async def player_lookup(self, interaction: discord.Interaction, user: str): 66 | player = await fetch_player(user) 67 | if player: 68 | session = await get_player_session(user) 69 | whitelisted = await is_whitelisted(player[0]) 70 | now = datetime.datetime.now(datetime.timezone.utc) 71 | total = session[1] if session else 0 72 | if session and session[2]: 73 | dt_start = datetime.datetime.fromisoformat(session[2]) 74 | total += int((now - dt_start).total_seconds()) 75 | h = total // 3600 76 | m = (total % 3600) // 60 77 | s = total % 60 78 | time_str = f"`{h}h {m}m {s}s`" if h else f"`{m}m {s}s`" 79 | embed = self.player_embed(player, time_str, whitelisted) 80 | await interaction.response.send_message(embed=embed, ephemeral=True) 81 | else: 82 | await interaction.response.send_message("Player not found.", ephemeral=True) 83 | 84 | def player_embed(self, player, time_str, whitelisted): 85 | embed = discord.Embed(title=f"Player: {player[1]} ({player[2]})", color=discord.Color.blurple()) 86 | embed.add_field(name="Level", value=player[8]) 87 | embed.add_field(name="Ping", value=player[5]) 88 | embed.add_field(name="Location", value=f"({player[6]}, {player[7]})") 89 | embed.add_field(name="Whitelisted", value="Yes" if whitelisted else "No") 90 | embed.add_field(name="PlayerID", value=f"```{player[0]}```", inline=False) 91 | embed.add_field(name="PlayerUID", value=f"```{player[3]}```", inline=False) 92 | embed.add_field(name="PlayerIP", value=f"```{player[4]}```", inline=False) 93 | embed.add_field(name="Playtime", value=time_str) 94 | return embed 95 | 96 | @log_players.before_loop 97 | async def before_log_players(self): 98 | await self.bot.wait_until_ready() 99 | 100 | async def setup(bot): 101 | await bot.add_cog(PlayerLoggingCog(bot)) -------------------------------------------------------------------------------- /cogs/utility/globalban.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | from discord import app_commands 4 | import aiohttp 5 | import logging 6 | import os 7 | from utils.pagination import Pagination, PaginationView 8 | 9 | class GlobalBan(commands.Cog): 10 | def __init__(self, bot): 11 | self.bot = bot 12 | self.api_url = os.getenv("API_URL") 13 | self.bearer_token = os.getenv("API_KEY") 14 | 15 | async def api_request(self, method: str, endpoint: str, json: dict = None, params: dict = None): 16 | url = f"{self.api_url}{endpoint}" 17 | headers = {"Authorization": f"Bearer {self.bearer_token}"} 18 | async with aiohttp.ClientSession() as session: 19 | async with session.request(method, url, headers=headers, json=json, params=params) as response: 20 | if response.status == 200: 21 | return await response.json() 22 | raise Exception(f"API request failed: {response.status}, {await response.text()}") 23 | 24 | api_group = app_commands.Group( 25 | name="api", 26 | description="API related commands.", 27 | default_permissions=discord.Permissions(administrator=True), 28 | guild_only=True 29 | ) 30 | 31 | @api_group.command(name="ban", description="Ban a user.") 32 | @app_commands.describe(name="The username to ban", user_id="The user ID to ban", reason="Reason for banning") 33 | async def ban_user(self, interaction: discord.Interaction, name: str, user_id: str, reason: str): 34 | await interaction.response.defer(ephemeral=True) 35 | try: 36 | payload = {"name": name, "id": user_id, "reason": reason} 37 | await self.api_request("POST", "/api/banuser", json=payload) 38 | await interaction.followup.send(f"User `{name}` (ID: {user_id}) has been banned for: {reason}", ephemeral=True) 39 | except Exception as e: 40 | logging.error(f"Failed to ban user: {e}") 41 | await interaction.followup.send(f"An error occurred while banning the user: {str(e)}", ephemeral=True) 42 | 43 | @api_group.command(name="unban", description="Unban a user.") 44 | @app_commands.describe(user_id="The user ID to unban") 45 | async def unban_user(self, interaction: discord.Interaction, user_id: str): 46 | await interaction.response.defer(ephemeral=True) 47 | try: 48 | await self.api_request("POST", "/api/unbanuser", params={"userid": user_id}) 49 | await interaction.followup.send(f"User with ID `{user_id}` has been unbanned successfully.", ephemeral=True) 50 | except Exception as e: 51 | logging.error(f"Failed to unban user: {e}") 52 | await interaction.followup.send(f"An error occurred while unbanning the user: {str(e)}", ephemeral=True) 53 | 54 | @api_group.command(name="banlist", description="Get detailed banned users list.") 55 | @app_commands.describe(name="Filter the banlist by name") 56 | async def banned_users(self, interaction: discord.Interaction, name: str = None): 57 | await interaction.response.defer(ephemeral=True) 58 | try: 59 | params = {"name": name} if name else None 60 | bans = await self.api_request("GET", "/api/bannedusers", params=params) 61 | if not bans: 62 | await interaction.followup.send("No users are currently banned.", ephemeral=True) 63 | return 64 | 65 | def create_embed(ban_page, current_page, total_pages): 66 | embed = discord.Embed( 67 | title="Banned Users", 68 | color=discord.Color.red() 69 | ) 70 | for ban in ban_page: 71 | embed.add_field( 72 | name=f"**Name:** {ban['name']}", 73 | value=( 74 | f"**ID:** `{ban['id']}`\n" 75 | f"**Reason:** {ban['reason']}" 76 | ), 77 | inline=False 78 | ) 79 | embed.set_footer(text=f"Page {current_page} of {total_pages}") 80 | return embed 81 | 82 | paginator = Pagination(bans, page_size=5) 83 | embed = create_embed(paginator.get_page(1), 1, paginator.total_pages) 84 | view = PaginationView(paginator, 1, create_embed) 85 | 86 | await interaction.followup.send(embed=embed, view=view, ephemeral=True) 87 | except Exception as e: 88 | logging.error(f"Failed to fetch banned users: {e}") 89 | await interaction.followup.send(f"An error occurred while fetching banned users: {str(e)}", ephemeral=True) 90 | 91 | async def setup(bot): 92 | api_url = os.getenv("API_URL") 93 | api_key = os.getenv("API_KEY") 94 | 95 | if not api_url or not api_key: 96 | logging.error("GlobalBan not loaded due to missing api key and url.") 97 | return 98 | 99 | await bot.add_cog(GlobalBan(bot)) 100 | -------------------------------------------------------------------------------- /cogs/control/control.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | from discord import app_commands 4 | from utils.database import fetch_server_details, server_autocomplete 5 | from palworld_api import PalworldAPI 6 | import logging 7 | 8 | class ControlCog(commands.Cog): 9 | def __init__(self, bot): 10 | self.bot = bot 11 | 12 | async def get_api_instance(self, guild_id, server_name): 13 | server_config = await fetch_server_details(guild_id, server_name) 14 | if not server_config: 15 | return None, f"Server '{server_name}' configuration not found." 16 | 17 | host = server_config[2] 18 | password = server_config[3] 19 | api_port = server_config[4] 20 | 21 | api = PalworldAPI(f"http://{host}:{api_port}", password) 22 | return api, None 23 | 24 | async def server_autocomplete(self, interaction: discord.Interaction, current: str): 25 | guild_id = interaction.guild.id 26 | server_names = await server_autocomplete(guild_id, current) 27 | choices = [app_commands.Choice(name=name, value=name) for name in server_names] 28 | return choices 29 | 30 | @app_commands.command(name="announce", description="Make an announcement to the server.") 31 | @app_commands.describe(server="The name of the server", message="The message to announce") 32 | @app_commands.autocomplete(server=server_autocomplete) 33 | @app_commands.default_permissions(administrator=True) 34 | @app_commands.guild_only() 35 | async def announce(self, interaction: discord.Interaction, server: str, message: str): 36 | await interaction.response.defer(thinking=True, ephemeral=True) 37 | try: 38 | api, error = await self.get_api_instance(interaction.guild.id, server) 39 | if error: 40 | await interaction.followup.send(error, ephemeral=True) 41 | return 42 | 43 | await api.make_announcement(message) 44 | await interaction.followup.send(f"Announcement sent: {message}", ephemeral=True) 45 | except Exception as e: 46 | await interaction.followup.send(f"An unexpected error occurred: {str(e)}", ephemeral=True) 47 | logging.error(f"An unexpected error occurred: {str(e)}") 48 | 49 | @app_commands.command(name="shutdown", description="Shutdown the server.") 50 | @app_commands.describe(server="The name of the server", message="The message to display before shutdown", seconds="The number of seconds before shutdown") 51 | @app_commands.autocomplete(server=server_autocomplete) 52 | @app_commands.default_permissions(administrator=True) 53 | @app_commands.guild_only() 54 | async def shutdown(self, interaction: discord.Interaction, server: str, message: str, seconds: int): 55 | await interaction.response.defer(thinking=True, ephemeral=True) 56 | try: 57 | api, error = await self.get_api_instance(interaction.guild.id, server) 58 | if error: 59 | await interaction.followup.send(error, ephemeral=True) 60 | return 61 | 62 | await api.shutdown_server(seconds, message) 63 | await interaction.followup.send(f"Server will shutdown in {seconds} seconds: {message}", ephemeral=True) 64 | except Exception as e: 65 | await interaction.followup.send(f"An unexpected error occurred: {str(e)}", ephemeral=True) 66 | logging.error(f"An unexpected error occurred: {str(e)}") 67 | 68 | @app_commands.command(name="stop", description="Stop the server.") 69 | @app_commands.describe(server="The name of the server") 70 | @app_commands.autocomplete(server=server_autocomplete) 71 | @app_commands.default_permissions(administrator=True) 72 | @app_commands.guild_only() 73 | async def stop(self, interaction: discord.Interaction, server: str): 74 | await interaction.response.defer(thinking=True, ephemeral=True) 75 | try: 76 | api, error = await self.get_api_instance(interaction.guild.id, server) 77 | if error: 78 | await interaction.followup.send(error, ephemeral=True) 79 | return 80 | 81 | response = await api.stop_server() 82 | await interaction.followup.send(f"Server stopped: {response}", ephemeral=True) 83 | except Exception as e: 84 | await interaction.followup.send(f"An unexpected error occurred: {str(e)}", ephemeral=True) 85 | logging.error(f"An unexpected error occurred: {str(e)}") 86 | 87 | @app_commands.command(name="save", description="Save the server state.") 88 | @app_commands.describe(server="The name of the server") 89 | @app_commands.autocomplete(server=server_autocomplete) 90 | @app_commands.default_permissions(administrator=True) 91 | @app_commands.guild_only() 92 | async def save(self, interaction: discord.Interaction, server: str): 93 | await interaction.response.defer(thinking=True, ephemeral=True) 94 | try: 95 | api, error = await self.get_api_instance(interaction.guild.id, server) 96 | if error: 97 | await interaction.followup.send(error, ephemeral=True) 98 | return 99 | 100 | response = await api.save_server_state() 101 | await interaction.followup.send(f"Server state saved: {response}", ephemeral=True) 102 | except Exception as e: 103 | await interaction.followup.send(f"An unexpected error occurred: {str(e)}", ephemeral=True) 104 | logging.error(f"An unexpected error occurred: {str(e)}") 105 | 106 | async def setup(bot): 107 | await bot.add_cog(ControlCog(bot)) 108 | -------------------------------------------------------------------------------- /cogs-dev/sftpchat.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands, tasks 3 | import aiohttp 4 | import re 5 | from paramiko import SSHClient, AutoAddPolicy 6 | import logging 7 | import os 8 | import asyncio 9 | from utils.database import fetch_server_details 10 | from palworld_api import PalworldAPI 11 | 12 | # Cog for SFTP based chat feed. 13 | sftp_host = os.getenv("SFTP_HOST", "") 14 | sftp_username = os.getenv("SFTP_USERNAME", "") 15 | sftp_password = os.getenv("SFTP_PASSWORD", "") 16 | sftp_port = int(os.getenv("SFTP_PORT", 2022)) 17 | sftp_path = os.getenv("SFTP_PATH", "Pal/Binaries/Win64/PalDefender/Logs") 18 | sftp_webhook = os.getenv("SFTP_WEBHOOK", "") 19 | sftp_channel = os.getenv("SFTP_CHANNEL", "") 20 | sftp_servername = os.getenv("SFTP_SERVERNAME", "") 21 | 22 | class ChatLogCog(commands.Cog): 23 | def __init__(self, bot): 24 | self.bot = bot 25 | self.sftp_host = sftp_host 26 | self.sftp_username = sftp_username 27 | self.sftp_password = sftp_password 28 | self.sftp_port = sftp_port 29 | self.log_directory = sftp_path 30 | self.webhook_url = sftp_webhook 31 | self.first_check_done = False 32 | self.last_processed_line = None 33 | self.session = aiohttp.ClientSession() 34 | self.check_logs.start() 35 | self.blocked_phrases = ["/adminpassword", "/creativemenu", "/"] 36 | 37 | def cog_unload(self): 38 | self.check_logs.cancel() 39 | self.bot.loop.create_task(self.session.close()) 40 | 41 | def connect_sftp(self): 42 | try: 43 | ssh = SSHClient() 44 | ssh.set_missing_host_key_policy(AutoAddPolicy()) 45 | ssh.connect( 46 | hostname=self.sftp_host, 47 | username=self.sftp_username, 48 | password=self.sftp_password, 49 | port=self.sftp_port 50 | ) 51 | sftp = ssh.open_sftp() 52 | return sftp, ssh 53 | except Exception as e: 54 | logging.error(f"Failed to connect to SFTP: {e}") 55 | return None, None 56 | 57 | @tasks.loop(seconds=15) 58 | async def check_logs(self): 59 | sftp, ssh = self.connect_sftp() 60 | if sftp is None or ssh is None: 61 | logging.info("SFTP connection could not be established.") 62 | return 63 | 64 | try: 65 | sftp.chdir(self.log_directory) 66 | files = sorted(sftp.listdir(), key=lambda x: sftp.stat(x).st_mtime, reverse=True) 67 | log_file_path = next((f for f in files if f.endswith('.log')), None) 68 | 69 | if log_file_path is None: 70 | logging.error("No log files found in the directory.") 71 | return 72 | 73 | with sftp.file(log_file_path, 'r') as file: 74 | content = file.read().decode('utf-8') 75 | lines = content.splitlines() 76 | 77 | if not self.first_check_done: 78 | self.last_processed_line = lines[-1] if lines else None 79 | self.first_check_done = True 80 | logging.info(f"Initial setup completed. Monitoring new lines from {log_file_path}.") 81 | return 82 | 83 | new_lines_start = False 84 | for line in lines: 85 | if line == self.last_processed_line: 86 | new_lines_start = True 87 | continue 88 | if new_lines_start or self.last_processed_line is None: 89 | if "[Chat::" in line: 90 | await self.process_and_send(line) 91 | await asyncio.sleep(1) 92 | 93 | if lines: 94 | self.last_processed_line = lines[-1] 95 | except Exception as e: 96 | logging.error(f"Error during log check: {e}") 97 | finally: 98 | sftp.close() 99 | ssh.close() 100 | 101 | async def process_and_send(self, line): 102 | try: 103 | match = re.search(r"\[Chat::(?:Global|Local|Guild)\]\['([^']+)'.*\]: (.*)", line) 104 | if match: 105 | username, message = match.groups() 106 | if any(blocked_phrase in message for blocked_phrase in self.blocked_phrases): 107 | logging.info(f"Blocked message from {username} containing a blocked phrase.") 108 | return 109 | payload = {"username": username, "content": message} 110 | async with self.session.post(self.webhook_url, json=payload) as response: 111 | if response.status != 200: 112 | logging.info(f"Error sending message to webhook: {response.status} - {await response.text()}") 113 | except Exception as e: 114 | logging.error(f"Error processing and sending log line: {e}") 115 | 116 | @commands.Cog.listener() 117 | async def on_message(self, message: discord.Message): 118 | if message.author.bot or not message.guild or not message.content: 119 | return 120 | if sftp_channel and message.channel.id == int(sftp_channel) and sftp_servername: 121 | details = await fetch_server_details(message.guild.id, sftp_servername) 122 | if details: 123 | host = details[2] 124 | password = details[3] 125 | api_port = details[4] 126 | api = PalworldAPI(f"http://{host}:{api_port}", password) 127 | await api.make_announcement(f"[{message.author.name}]: {message.content}") 128 | 129 | @check_logs.before_loop 130 | async def before_check_logs(self): 131 | await self.bot.wait_until_ready() 132 | 133 | async def setup(bot): 134 | await bot.add_cog(ChatLogCog(bot)) 135 | -------------------------------------------------------------------------------- /cogs/control/admin.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | from discord import app_commands 4 | from utils.database import fetch_server_details, server_autocomplete 5 | from utils.bans import ( 6 | fetch_bans, 7 | log_ban, 8 | clear_bans 9 | ) 10 | from palworld_api import PalworldAPI 11 | import logging 12 | import io 13 | 14 | class AdminCog(commands.Cog): 15 | def __init__(self, bot): 16 | self.bot = bot 17 | 18 | async def get_api_instance(self, guild_id, server_name): 19 | server_config = await fetch_server_details(guild_id, server_name) 20 | if not server_config: 21 | return None, f"Server '{server_name}' configuration not found." 22 | 23 | host = server_config[2] 24 | password = server_config[3] 25 | api_port = server_config[4] 26 | 27 | api = PalworldAPI(f"http://{host}:{api_port}", password) 28 | return api, None 29 | 30 | async def server_autocomplete(self, interaction: discord.Interaction, current: str): 31 | guild_id = interaction.guild.id 32 | server_names = await server_autocomplete(guild_id, current) 33 | choices = [app_commands.Choice(name=name, value=name) for name in server_names] 34 | return choices 35 | 36 | @app_commands.command(name="kick", description="Kick a player from the server.") 37 | @app_commands.describe(server="The name of the server", player_id="The player ID to kick", reason="The reason for the kick") 38 | @app_commands.autocomplete(server=server_autocomplete) 39 | @app_commands.default_permissions(administrator=True) 40 | @app_commands.guild_only() 41 | async def kick_player(self, interaction: discord.Interaction, server: str, player_id: str, reason: str): 42 | await interaction.response.defer(thinking=True, ephemeral=True) 43 | try: 44 | api, error = await self.get_api_instance(interaction.guild.id, server) 45 | if error: 46 | await interaction.followup.send(error, ephemeral=True) 47 | return 48 | 49 | await api.kick_player(player_id, reason) 50 | await interaction.followup.send(f"Player {player_id} has been kicked for: {reason}", ephemeral=True) 51 | except Exception as e: 52 | await interaction.followup.send(f"An unexpected error occurred: {str(e)}", ephemeral=True) 53 | logging.error(f"An unexpected error occurred: {str(e)}") 54 | 55 | @app_commands.command(name="ban", description="Ban a player from the server.") 56 | @app_commands.describe(server="The name of the server", player_id="The player ID to ban", reason="The reason for the ban") 57 | @app_commands.autocomplete(server=server_autocomplete) 58 | @app_commands.default_permissions(administrator=True) 59 | @app_commands.guild_only() 60 | async def ban_player(self, interaction: discord.Interaction, server: str, player_id: str, reason: str): 61 | await interaction.response.defer(thinking=True, ephemeral=True) 62 | try: 63 | api, error = await self.get_api_instance(interaction.guild.id, server) 64 | if error: 65 | await interaction.followup.send(error, ephemeral=True) 66 | return 67 | 68 | await api.ban_player(player_id, reason) 69 | await log_ban(player_id, reason) 70 | await interaction.followup.send(f"Player {player_id} has been banned for: {reason}", ephemeral=True) 71 | except Exception as e: 72 | await interaction.followup.send(f"An unexpected error occurred: {str(e)}", ephemeral=True) 73 | logging.error(f"An unexpected error occurred: {str(e)}") 74 | 75 | @app_commands.command(name="unban", description="Unban a player from the server.") 76 | @app_commands.describe(server="The name of the server", player_id="The player ID to unban") 77 | @app_commands.autocomplete(server=server_autocomplete) 78 | @app_commands.default_permissions(administrator=True) 79 | @app_commands.guild_only() 80 | async def unban_player(self, interaction: discord.Interaction, server: str, player_id: str): 81 | await interaction.response.defer(thinking=True, ephemeral=True) 82 | try: 83 | api, error = await self.get_api_instance(interaction.guild.id, server) 84 | if error: 85 | await interaction.followup.send(error, ephemeral=True) 86 | return 87 | 88 | await api.unban_player(player_id) 89 | await interaction.followup.send(f"Player {player_id} has been unbanned.", ephemeral=True) 90 | except Exception as e: 91 | await interaction.followup.send(f"An unexpected error occurred: {str(e)}", ephemeral=True) 92 | logging.error(f"An unexpected error occurred: {str(e)}") 93 | 94 | # Uploads ban logs as a text file 95 | @app_commands.command(name="bans", description="List all banned players.") 96 | @app_commands.default_permissions(administrator=True) 97 | @app_commands.guild_only() 98 | async def list_bans(self, interaction: discord.Interaction): 99 | await interaction.response.defer(thinking=True, ephemeral=True) 100 | bans = await fetch_bans() 101 | if bans: 102 | ban_list = "\n".join([f"{ban[0]}: {ban[1]}" for ban in bans]) 103 | ban_file = io.StringIO(ban_list) 104 | discord_file = discord.File(ban_file, filename="bannedplayers.txt") 105 | await interaction.followup.send("Banned players:", file=discord_file, ephemeral=True) 106 | ban_file.close() 107 | else: 108 | await interaction.followup.send("No players are banned.", ephemeral=True) 109 | logging.info("No players are banned.") 110 | 111 | @app_commands.command(name="clearbans", description="Clear ban history from the database.") 112 | @app_commands.default_permissions(administrator=True) 113 | @app_commands.guild_only() 114 | async def clear_bans_command(self, interaction: discord.Interaction): 115 | await interaction.response.defer(thinking=True, ephemeral=True) 116 | await clear_bans() 117 | await interaction.followup.send("All bans have been cleared.", ephemeral=True) 118 | logging.info("All bans have been cleared.") 119 | 120 | async def setup(bot): 121 | await bot.add_cog(AdminCog(bot)) 122 | -------------------------------------------------------------------------------- /cogs/control/whitelist.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands, tasks 3 | from discord import app_commands 4 | from utils.whitelist import ( 5 | add_whitelist, 6 | remove_whitelist, 7 | is_whitelisted, 8 | whitelist_set, 9 | whitelist_get 10 | ) 11 | from utils.database import ( 12 | fetch_all_servers, 13 | server_autocomplete, 14 | fetch_logchannel 15 | ) 16 | from palworld_api import PalworldAPI 17 | import logging 18 | 19 | class WhitelistCog(commands.Cog): 20 | def __init__(self, bot): 21 | self.bot = bot 22 | self.check_whitelist.start() 23 | 24 | def cog_unload(self): 25 | self.check_whitelist.cancel() 26 | 27 | @tasks.loop(seconds=60) 28 | async def check_whitelist(self): 29 | servers = await fetch_all_servers() 30 | for server in servers: 31 | guild_id, server_name, host, password, api_port, rcon_port = server 32 | if not await whitelist_get(guild_id, server_name): 33 | continue 34 | 35 | log_channel_id = await fetch_logchannel(guild_id, server_name) 36 | log_channel = self.bot.get_channel(log_channel_id) if log_channel_id else None 37 | 38 | try: 39 | api = PalworldAPI(f"http://{host}:{api_port}", password) 40 | player_list = await api.get_player_list() 41 | for player in player_list['players']: 42 | playerid = player['userId'] 43 | if not await is_whitelisted(playerid): 44 | await api.kick_player(playerid, "You are not whitelisted.") 45 | logging.info(f"Player {playerid} kicked from server '{server_name}' for not being whitelisted.") 46 | 47 | if log_channel: 48 | kick_message = f"Player `{playerid}` was kicked from server {server_name} for not being whitelisted." 49 | embed = discord.Embed(title="Whitelist Check", description=kick_message, color=discord.Color.red(), timestamp=discord.utils.utcnow()) 50 | await log_channel.send(embed=embed) 51 | 52 | logging.info(f"Whitelist checked for server '{server_name}'.") 53 | except Exception as e: 54 | logging.error(f"An unexpected error occurred while checking whitelist for server '{server_name}': {str(e)}") 55 | 56 | @check_whitelist.before_loop 57 | async def before_check_whitelist(self): 58 | await self.bot.wait_until_ready() 59 | 60 | @app_commands.command(name="add", description="Add a player to the whitelist.") 61 | @app_commands.describe(playerid="The playerid of the player to whitelist.") 62 | @app_commands.default_permissions(administrator=True) 63 | @app_commands.guild_only() 64 | async def whitelist_add(self, interaction: discord.Interaction, playerid: str): 65 | await interaction.response.defer(thinking=True, ephemeral=True) 66 | try: 67 | await add_whitelist(playerid, True) 68 | await interaction.followup.send(f"Player {playerid} has been added to the whitelist.", ephemeral=True) 69 | except Exception as e: 70 | await interaction.followup.send(f"An unexpected error occurred: {str(e)}", ephemeral=True) 71 | logging.error(f"An unexpected error occurred: {str(e)}") 72 | 73 | @app_commands.command(name="remove", description="Remove a player from the whitelist.") 74 | @app_commands.describe(playerid="The playerid of the player to remove from the whitelist.") 75 | @app_commands.default_permissions(administrator=True) 76 | @app_commands.guild_only() 77 | async def whitelist_remove(self, interaction: discord.Interaction, playerid: str): 78 | await interaction.response.defer(thinking=True, ephemeral=True) 79 | try: 80 | await remove_whitelist(playerid) 81 | await interaction.followup.send(f"Player {playerid} has been removed from the whitelist.", ephemeral=True) 82 | except Exception as e: 83 | await interaction.followup.send(f"An unexpected error occurred: {str(e)}", ephemeral=True) 84 | logging.error(f"An unexpected error occurred: {str(e)}") 85 | 86 | async def server_names(self, interaction: discord.Interaction, current: str): 87 | guild_id = interaction.guild.id 88 | server_names = await server_autocomplete(guild_id, current) 89 | return [app_commands.Choice(name=name, value=name) for name in server_names] 90 | 91 | @app_commands.command(name="enable", description="Enable whitelist for a server.") 92 | @app_commands.describe(server_name="The name of the server to enable the whitelist for.") 93 | @app_commands.autocomplete(server_name=server_names) 94 | @app_commands.default_permissions(administrator=True) 95 | @app_commands.guild_only() 96 | async def enable_whitelist(self, interaction: discord.Interaction, server_name: str): 97 | await interaction.response.defer(thinking=True, ephemeral=True) 98 | try: 99 | await whitelist_set(interaction.guild_id, server_name, True) 100 | await interaction.followup.send(f"Whitelist has been enabled for server {server_name}.", ephemeral=True) 101 | except Exception as e: 102 | await interaction.followup.send(f"An unexpected error occurred: {str(e)}", ephemeral=True) 103 | logging.error(f"An unexpected error occurred: {str(e)}") 104 | 105 | @app_commands.command(name="disable", description="Disable whitelist for a server.") 106 | @app_commands.describe(server_name="The name of the server to disable the whitelist for.") 107 | @app_commands.autocomplete(server_name=server_names) 108 | @app_commands.default_permissions(administrator=True) 109 | @app_commands.guild_only() 110 | async def disable_whitelist(self, interaction: discord.Interaction, server_name: str): 111 | await interaction.response.defer(thinking=True, ephemeral=True) 112 | try: 113 | await whitelist_set(interaction.guild_id, server_name, False) 114 | await interaction.followup.send(f"Whitelist has been disabled for server {server_name}.", ephemeral=True) 115 | except Exception as e: 116 | await interaction.followup.send(f"An unexpected error occurred: {str(e)}", ephemeral=True) 117 | logging.error(f"An unexpected error occurred: {str(e)}") 118 | 119 | async def setup(bot): 120 | await bot.add_cog(WhitelistCog(bot)) 121 | -------------------------------------------------------------------------------- /cogs/logging/query.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands, tasks 3 | from discord import app_commands 4 | from utils.database import ( 5 | server_autocomplete, 6 | fetch_server_details, 7 | add_query, 8 | fetch_query, 9 | delete_query, 10 | fetch_all_servers 11 | ) 12 | from palworld_api import PalworldAPI 13 | import utils.constants as c 14 | import logging 15 | import asyncio 16 | 17 | class ServerQueryCog(commands.Cog): 18 | def __init__(self, bot): 19 | self.bot = bot 20 | self.update_messages.start() 21 | 22 | def cog_unload(self): 23 | self.update_messages.cancel() 24 | 25 | @tasks.loop(seconds=180) 26 | async def update_messages(self): 27 | servers = await fetch_all_servers() 28 | for server in servers: 29 | guild_id, server_name, host, password, api_port, rcon_port = server 30 | message_ids = await fetch_query(guild_id, server_name) 31 | if message_ids: 32 | channel_id, message_id, player_message_id = message_ids 33 | channel = self.bot.get_channel(channel_id) 34 | if channel: 35 | try: 36 | server_config = await fetch_server_details(guild_id, server_name) 37 | if not server_config: 38 | continue 39 | 40 | api = PalworldAPI(f"http://{host}:{api_port}", password) 41 | server_info = await api.get_server_info() 42 | server_metrics = await api.get_server_metrics() 43 | player_list = await api.get_player_list() 44 | 45 | server_embed = self.create_server_embed(server_name, server_info, server_metrics) 46 | player_embed = self.create_player_embed(player_list) 47 | 48 | try: 49 | message = await channel.fetch_message(message_id) 50 | await message.edit(embed=server_embed) 51 | except discord.NotFound: 52 | message = await channel.send(embed=server_embed) 53 | await add_query(guild_id, channel_id, server_name, message.id, player_message_id) 54 | 55 | await asyncio.sleep(5) 56 | 57 | try: 58 | player_message = await channel.fetch_message(player_message_id) 59 | await player_message.edit(embed=player_embed) 60 | except discord.NotFound: 61 | player_message = await channel.send(embed=player_embed) 62 | await add_query(guild_id, channel_id, server_name, message.id, player_message.id) 63 | 64 | await asyncio.sleep(5) 65 | 66 | except Exception as e: 67 | logging.error(f"Error updating query server: '{server_name}': {str(e)}") 68 | 69 | def create_server_embed(self, server_name, server_info, server_metrics): 70 | embed = discord.Embed( 71 | title=f"{server_info.get('servername', server_name)}", 72 | description=f"{server_info.get('description', 'N/A')}", 73 | color=discord.Color.blurple() 74 | ) 75 | embed.add_field(name="Players", value=f"{server_metrics.get('currentplayernum', 'N/A')}/{server_metrics.get('maxplayernum', 'N/A')}", inline=True) 76 | embed.add_field(name="Version", value=server_info.get('version', 'N/A'), inline=True) 77 | embed.add_field(name="Days Passed", value=server_metrics.get('days', 'N/A'), inline=True) 78 | embed.add_field(name="Uptime", value=f"{int(server_metrics.get('uptime', 'N/A') / 60)} minutes", inline=True) 79 | embed.add_field(name="FPS", value=server_metrics.get('serverfps', 'N/A'), inline=True) 80 | embed.add_field(name="Latency", value=f"{server_metrics.get('serverframetime', 'N/A'):.2f} ms", inline=True) 81 | embed.add_field(name="WorldGUID", value=f"`{server_info.get('worldguid', 'N/A')}`", inline=False) 82 | embed.set_thumbnail(url=c.SPHERE_THUMBNAIL) 83 | return embed 84 | 85 | def create_player_embed(self, player_list): 86 | player_names = "\n".join([f"{player['name']}({player['accountName']}) - {player['userId']}" for player in player_list['players']]) 87 | embed = discord.Embed( 88 | title="Players", 89 | color=discord.Color.green() 90 | ) 91 | embed.add_field(name="Online Players", value=player_names if player_names else "No players online", inline=False) 92 | return embed 93 | 94 | async def server_names(self, interaction: discord.Interaction, current: str): 95 | guild_id = interaction.guild.id 96 | server_names = await server_autocomplete(guild_id, current) 97 | return [app_commands.Choice(name=name, value=name) for name in server_names] 98 | 99 | query_group = app_commands.Group(name="query", description="Query the server for information", default_permissions=discord.Permissions(administrator=True), guild_only=True) 100 | 101 | @query_group.command(name="add", description="Set the channel to query and log server info") 102 | @app_commands.describe(server="The name of the server", channel="The channel to log events in") 103 | @app_commands.autocomplete(server=server_names) 104 | async def add_query(self, interaction: discord.Interaction, server: str, channel: discord.TextChannel): 105 | try: 106 | await interaction.response.defer(ephemeral=True) 107 | guild_id = interaction.guild.id 108 | 109 | server_config = await fetch_server_details(guild_id, server) 110 | if not server_config: 111 | await interaction.followup.send(f"Server '{server}' configuration not found.", ephemeral=True) 112 | return 113 | 114 | host = server_config[2] 115 | password = server_config[3] 116 | api_port = server_config[4] 117 | 118 | api = PalworldAPI(f"http://{host}:{api_port}", password) 119 | server_info = await api.get_server_info() 120 | server_metrics = await api.get_server_metrics() 121 | player_list = await api.get_player_list() 122 | 123 | server_embed = self.create_server_embed(server, server_info, server_metrics) 124 | player_embed = self.create_player_embed(player_list) 125 | 126 | message = await channel.send(embed=server_embed) 127 | player_message = await channel.send(embed=player_embed) 128 | 129 | await add_query(guild_id, channel.id, server, message.id, player_message.id) 130 | await interaction.followup.send(f"Query channel for server `{server}` set to {channel.mention}.", ephemeral=True) 131 | except Exception as e: 132 | await interaction.followup.send(f"Error in 'Add Query' command: {str(e)}", ephemeral=True) 133 | logging.error(f"Error in 'Add Query' command: {str(e)}") 134 | 135 | @query_group.command(name="remove", description="Remove the query channel for server info") 136 | @app_commands.describe(server="The name of the server") 137 | @app_commands.autocomplete(server=server_names) 138 | async def remove_query(self, interaction: discord.Interaction, server: str): 139 | try: 140 | await interaction.response.defer(ephemeral=True) 141 | guild_id = interaction.guild.id 142 | await delete_query(guild_id, server) 143 | await interaction.followup.send(f"Query channel for server `{server}` removed.", ephemeral=True) 144 | except Exception as e: 145 | await interaction.followup.send(f"Error in 'Remove Query' command: {str(e)}", ephemeral=True) 146 | logging.error(f"Error in 'Remove Query' command: {str(e)}") 147 | 148 | async def setup(bot): 149 | await bot.add_cog(ServerQueryCog(bot)) 150 | -------------------------------------------------------------------------------- /cogs/logging/backup.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands, tasks 3 | from discord import app_commands 4 | import os 5 | import zipfile 6 | import datetime 7 | import logging 8 | from utils.database import ( 9 | set_backup, 10 | del_backup, 11 | all_backups, 12 | server_autocomplete, 13 | fetch_server_details 14 | ) 15 | from utils.servermodal import BackupModal 16 | from palworld_api import PalworldAPI 17 | 18 | class BackupCog(commands.Cog): 19 | def __init__(self, bot): 20 | self.bot = bot 21 | self.last_run = {} 22 | self.runloop.start() 23 | 24 | def cog_unload(self): 25 | self.runloop.cancel() 26 | 27 | @tasks.loop(seconds=60) 28 | async def runloop(self): 29 | data = await all_backups() 30 | for row in data: 31 | gid, name, path, cid, interval = row 32 | key = f"{gid}-{name}" 33 | now = datetime.datetime.utcnow().timestamp() 34 | if key not in self.last_run: 35 | self.last_run[key] = 0 36 | if now - self.last_run[key] >= interval * 60: 37 | self.last_run[key] = now 38 | 39 | try: 40 | server_config = await fetch_server_details(gid, name) 41 | if not server_config: 42 | continue 43 | 44 | host = server_config[2] 45 | password = server_config[3] 46 | api_port = server_config[4] 47 | 48 | api = PalworldAPI(f"http://{host}:{api_port}", password) 49 | info = await api.get_server_info() 50 | 51 | if not info or "version" not in info: 52 | logging.error(f"Skipping backup for {name}: Invalid API response.") 53 | continue 54 | except Exception as e: 55 | logging.error(f"Skipping backup for {name} because API unreachable: {e}") 56 | continue 57 | 58 | channel = self.bot.get_channel(cid) 59 | if channel: 60 | zip_name = f"{name}_{datetime.datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.zip" 61 | zip_dir = path 62 | if not os.path.exists(zip_dir): 63 | os.makedirs(zip_dir) 64 | zip_path = os.path.join(zip_dir, zip_name) 65 | try: 66 | with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as z: 67 | players = os.path.join(path, "Players") 68 | level_sav = os.path.join(path, "Level.sav") 69 | meta_sav = os.path.join(path, "LevelMeta.sav") 70 | if os.path.isdir(players): 71 | for root, dirs, files in os.walk(players): 72 | for f in files: 73 | fp = os.path.join(root, f) 74 | rel = os.path.relpath(fp, path) 75 | z.write(fp, rel) 76 | if os.path.isfile(level_sav): 77 | z.write(level_sav, "Level.sav") 78 | if os.path.isfile(meta_sav): 79 | z.write(meta_sav, "LevelMeta.sav") 80 | 81 | file_size = os.path.getsize(zip_path) 82 | timestamp_dt = discord.utils.utcnow() 83 | discord_ts = f"" 84 | 85 | embed = discord.Embed( 86 | title=f"Backup Completed - {name}", 87 | color=discord.Color.blurple(), 88 | description=f"Backup created successfully for **{name}**.\n" 89 | ) 90 | embed.add_field(name="Filename", value=zip_name, inline=False) 91 | embed.add_field(name="Size", value=f"{file_size / 1024:.2f} KB", inline=False) 92 | embed.add_field(name="Time", value=discord_ts, inline=False) 93 | embed.set_footer(text=discord.utils.utcnow()) 94 | 95 | await channel.send(embed=embed) 96 | await channel.send(file=discord.File(zip_path)) 97 | os.remove(zip_path) 98 | logging.info(f"Backup created and uploaded: {zip_path}") 99 | except Exception as e: 100 | logging.error(f"Error creating or sending backup: {e}") 101 | 102 | async def server_names(self, interaction: discord.Interaction, current: str): 103 | guild_id = interaction.guild.id 104 | names = await server_autocomplete(guild_id, current) 105 | return [app_commands.Choice(name=n, value=n) for n in names] 106 | 107 | backup_group = app_commands.Group(name="backup", description="Manage backup settings", default_permissions=discord.Permissions(administrator=True), guild_only=True) 108 | 109 | @backup_group.command(name="setup", description="Setup backup for a server.") 110 | @app_commands.describe(server="Select the server name") 111 | @app_commands.autocomplete(server=server_names) 112 | async def setupbackup(self, interaction: discord.Interaction, server: str): 113 | async def handle_submit(modal_interaction: discord.Interaction, modal: BackupModal): 114 | await modal_interaction.response.defer(ephemeral=True) 115 | cid = int(modal.children[0].value) 116 | path = modal.children[1].value 117 | minutes = int(modal.children[2].value) 118 | try: 119 | await set_backup(interaction.guild_id, server, path, cid, minutes) 120 | await modal_interaction.followup.send("Backup config saved.", ephemeral=True) 121 | except Exception as e: 122 | logging.error(f"Failed to set backup: {e}") 123 | await modal_interaction.followup.send(f"Failed: {e}", ephemeral=True) 124 | 125 | modal = BackupModal(title=server, on_submit_callback=handle_submit) 126 | await interaction.response.send_modal(modal) 127 | 128 | @backup_group.command(name="remove", description="Remove backup config for a server.") 129 | @app_commands.describe(server="Select the server name to remove") 130 | @app_commands.autocomplete(server=server_names) 131 | async def removebackup(self, interaction: discord.Interaction, server: str): 132 | try: 133 | await del_backup(interaction.guild.id, server) 134 | await interaction.response.send_message("Backup config removed.", ephemeral=True) 135 | logging.info(f"Removed backup config for {server} in guild {interaction.guild.id}") 136 | except Exception as e: 137 | logging.error(f"Error removing backup: {e}") 138 | await interaction.response.send_message("Failed to remove backup config.", ephemeral=True) 139 | 140 | @backup_group.command(name="wipe", description="Remove all backup configs for this server") 141 | async def wipebackups(self, interaction: discord.Interaction): 142 | try: 143 | data = await all_backups() 144 | deleted = False 145 | for row in data: 146 | gid, name, *_ = row 147 | if gid == interaction.guild.id: 148 | await del_backup(gid, name) 149 | deleted = True 150 | if deleted: 151 | await interaction.response.send_message("All backup configs wiped for this server.", ephemeral=True) 152 | else: 153 | await interaction.response.send_message("No backup configs found to wipe.", ephemeral=True) 154 | except Exception as e: 155 | logging.error(f"Error wiping backups: {e}") 156 | await interaction.response.send_message("Failed to wipe backup configs.", ephemeral=True) 157 | 158 | @runloop.before_loop 159 | async def before_runloop(self): 160 | await self.bot.wait_until_ready() 161 | 162 | async def setup(bot): 163 | await bot.add_cog(BackupCog(bot)) 164 | -------------------------------------------------------------------------------- /cogs/rcon/kits.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import discord 4 | from discord.ext import commands 5 | from discord import app_commands 6 | import asyncio 7 | import aiosqlite 8 | from utils.rconutility import RconUtility 9 | from utils.database import fetch_server_details, server_autocomplete 10 | 11 | # This is all temporary till I separate the database stuff into its own utility file. 12 | DATABASE_PATH = os.path.join("data", "palworld.db") 13 | 14 | async def db_connection(): 15 | conn = None 16 | try: 17 | conn = await aiosqlite.connect(DATABASE_PATH) 18 | except aiosqlite.Error as e: 19 | print(e) 20 | return conn 21 | 22 | async def ensure_kits_table(): 23 | conn = await db_connection() 24 | if conn is not None: 25 | cursor = await conn.cursor() 26 | await cursor.execute(""" 27 | CREATE TABLE IF NOT EXISTS kits ( 28 | kit_name TEXT PRIMARY KEY, 29 | commands TEXT NOT NULL, 30 | description TEXT NOT NULL 31 | ) 32 | """) 33 | await conn.commit() 34 | await conn.close() 35 | 36 | async def get_kit(kit_name: str): 37 | conn = await db_connection() 38 | if conn is None: 39 | return None 40 | cursor = await conn.cursor() 41 | await cursor.execute("SELECT commands, description FROM kits WHERE kit_name = ?", (kit_name,)) 42 | kit = await cursor.fetchone() 43 | await conn.close() 44 | return kit 45 | 46 | async def save_kit(kit_name: str, commands_data: str, desc: str): 47 | conn = await db_connection() 48 | if conn is None: 49 | return 50 | cursor = await conn.cursor() 51 | await cursor.execute(""" 52 | INSERT INTO kits (kit_name, commands, description) 53 | VALUES (?, ?, ?) 54 | ON CONFLICT(kit_name) DO UPDATE 55 | SET commands=excluded.commands, 56 | description=excluded.description 57 | """,(kit_name, commands_data, desc)) 58 | await conn.commit() 59 | await conn.close() 60 | 61 | async def delete_kit(kit_name: str): 62 | conn = await db_connection() 63 | if conn is None: 64 | return 65 | cursor = await conn.cursor() 66 | await cursor.execute("DELETE FROM kits WHERE kit_name = ?", (kit_name,)) 67 | await conn.commit() 68 | await conn.close() 69 | 70 | async def autocomplete_kits(interaction: discord.Interaction, current: str): 71 | conn = await db_connection() 72 | if conn is None: 73 | return [] 74 | cursor = await conn.cursor() 75 | await cursor.execute("SELECT kit_name FROM kits WHERE kit_name LIKE ?", (f"%{current}%",)) 76 | rows = await cursor.fetchall() 77 | await conn.close() 78 | return [app_commands.Choice(name=r[0], value=r[0]) for r in rows] 79 | 80 | class KitModal(discord.ui.Modal): 81 | def __init__(self, title, default_name="", default_commands="[]", default_desc=""): 82 | super().__init__(title=title) 83 | self.kit_name = discord.ui.TextInput(label="Kit Name", default=default_name) 84 | self.commands = discord.ui.TextInput(label="Commands (JSON)", style=discord.TextStyle.paragraph, default=default_commands) 85 | self.description = discord.ui.TextInput(label="Description", default=default_desc) 86 | self.add_item(self.kit_name) 87 | self.add_item(self.commands) 88 | self.add_item(self.description) 89 | 90 | async def on_submit(self, interaction: discord.Interaction): 91 | kit_name = self.kit_name.value.strip() 92 | commands_data = self.commands.value.strip() 93 | desc = self.description.value.strip() 94 | if not kit_name: 95 | await interaction.response.send_message("Kit name is required.", ephemeral=True) 96 | return 97 | try: 98 | json.loads(commands_data) 99 | except: 100 | await interaction.response.send_message("Commands must be valid JSON.", ephemeral=True) 101 | return 102 | await save_kit(kit_name, commands_data, desc) 103 | await interaction.response.send_message(f"Kit '{kit_name}' has been saved.", ephemeral=True) 104 | 105 | class KitsCog(commands.Cog): 106 | def __init__(self, bot: commands.Bot): 107 | self.bot = bot 108 | self.rcon = RconUtility() 109 | self.servers = [] 110 | bot.loop.create_task(ensure_kits_table()) 111 | bot.loop.create_task(self.load_servers()) 112 | 113 | async def load_servers(self): 114 | self.servers = [] 115 | 116 | async def get_server_info(self, guild_id: int, server_name: str): 117 | details = await fetch_server_details(guild_id, server_name) 118 | if details: 119 | return {"host": details[2], "password": details[3], "port": details[5]} 120 | 121 | async def autocomplete_server(self, interaction: discord.Interaction, current: str): 122 | guild_id = interaction.guild.id if interaction.guild else 0 123 | results = await server_autocomplete(guild_id, current) 124 | return [app_commands.Choice(name=x, value=x) for x in results[:25]] 125 | 126 | @app_commands.command(name="givekit", description="Give a kit to a user") 127 | @app_commands.describe(userid="User ID", kit_name="Kit Name", server="Server Name") 128 | @app_commands.autocomplete(server=autocomplete_server, kit_name=autocomplete_kits) 129 | @app_commands.default_permissions(administrator=True) 130 | @app_commands.guild_only() 131 | async def givekit(self, interaction: discord.Interaction, userid: str, kit_name: str, server: str): 132 | await interaction.response.defer(ephemeral=True) 133 | if not interaction.guild: 134 | await interaction.followup.send("No guild.", ephemeral=True) 135 | return 136 | server_info = await self.get_server_info(interaction.guild.id, server) 137 | if not server_info: 138 | await interaction.followup.send(f"Server not found: {server}", ephemeral=True) 139 | return 140 | kit = await get_kit(kit_name) 141 | if not kit: 142 | await interaction.followup.send(f"Kit not found: {kit_name}", ephemeral=True) 143 | return 144 | commands_str, desc = kit 145 | try: 146 | commands_list = json.loads(commands_str) 147 | except: 148 | await interaction.followup.send("Commands data is not valid JSON.", ephemeral=True) 149 | return 150 | for cmd_template in commands_list: 151 | final_cmd = cmd_template.format(userid=userid) 152 | await self.rcon.rcon_command(server_info["host"], server_info["port"], server_info["password"], final_cmd) 153 | await asyncio.sleep(1) 154 | await interaction.followup.send(f"Kit '{kit_name}' given to {userid} on '{server}'.", ephemeral=True) 155 | 156 | @app_commands.command(name="managekit", description="Create or update a kit.") 157 | @app_commands.describe(kit_name="Kit name (optional). If it exists, it will be loaded.") 158 | @app_commands.autocomplete(kit_name=autocomplete_kits) 159 | @app_commands.default_permissions(administrator=True) 160 | @app_commands.guild_only() 161 | async def manage_kit(self, interaction: discord.Interaction, kit_name: str = ""): 162 | if kit_name: 163 | existing = await get_kit(kit_name) 164 | if existing: 165 | commands_str, desc = existing 166 | modal = KitModal("Manage Kit", default_name=kit_name, default_commands=commands_str, default_desc=desc) 167 | else: 168 | modal = KitModal("Manage Kit") 169 | else: 170 | modal = KitModal("Manage Kit") 171 | await interaction.response.send_modal(modal) 172 | 173 | @app_commands.command(name="deletekit", description="Remove a kit from the database.") 174 | @app_commands.describe(kit_name="Kit name to remove") 175 | @app_commands.autocomplete(kit_name=autocomplete_kits) 176 | @app_commands.default_permissions(administrator=True) 177 | @app_commands.guild_only() 178 | async def deletekit(self, interaction: discord.Interaction, kit_name: str): 179 | await interaction.response.defer(ephemeral=True) 180 | await delete_kit(kit_name) 181 | await interaction.followup.send(f"Kit '{kit_name}' has been deleted.", ephemeral=True) 182 | 183 | async def setup(bot): 184 | await bot.add_cog(KitsCog(bot)) 185 | -------------------------------------------------------------------------------- /cogs/logging/chat.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands, tasks 3 | from discord import app_commands 4 | import aiohttp 5 | import re 6 | import logging 7 | import os 8 | import asyncio 9 | from utils.database import ( 10 | get_chat, 11 | set_chat, 12 | delete_chat, 13 | fetch_server_details, 14 | server_autocomplete 15 | ) 16 | from palworld_api import PalworldAPI 17 | from utils.servermodal import ChatSetupModal 18 | 19 | class ChatCog(commands.Cog): 20 | def __init__(self, bot): 21 | self.bot = bot 22 | self.current_log_file = {} 23 | self.last_processed_line = {} 24 | self.first_check_done = {} 25 | self.blocked_phrases = ["/adminpassword", "/creativemenu", "/"] 26 | self.group_filter = ["local", "guild"] 27 | self.check_logs.start() 28 | 29 | def cog_unload(self): 30 | self.check_logs.cancel() 31 | 32 | @tasks.loop(seconds=8) 33 | async def check_logs(self): 34 | for guild in self.bot.guilds: 35 | configs = await get_chat(guild.id) 36 | if not configs: 37 | continue 38 | 39 | for config in configs: 40 | server_name, chat_channel_id, log_path, webhook_url = config 41 | 42 | try: 43 | files = sorted( 44 | [f for f in os.listdir(log_path) if f.endswith(".txt") or f.endswith(".log")], 45 | key=lambda x: os.stat(os.path.join(log_path, x)).st_mtime, 46 | reverse=True 47 | ) 48 | if not files: 49 | continue 50 | 51 | newest_file = os.path.join(log_path, files[0]) 52 | key = f"{guild.id}_{server_name}" 53 | if self.current_log_file.get(key) != newest_file: 54 | self.current_log_file[key] = newest_file 55 | self.last_processed_line[key] = None 56 | self.first_check_done[key] = False 57 | 58 | with open(newest_file, "r", encoding="utf-8", errors="ignore") as file: 59 | content = file.read() 60 | lines = content.splitlines() 61 | 62 | if not self.first_check_done.get(key, False): 63 | if lines: 64 | self.last_processed_line[key] = lines[-1] 65 | self.first_check_done[key] = True 66 | continue 67 | 68 | new_lines_start = False 69 | for line in lines: 70 | if line == self.last_processed_line.get(key): 71 | new_lines_start = True 72 | continue 73 | if new_lines_start or self.last_processed_line.get(key) is None: 74 | if "[Chat::" in line: 75 | await self.process_and_send(line, webhook_url, server_name) 76 | 77 | if lines: 78 | self.last_processed_line[key] = lines[-1] 79 | except Exception as e: 80 | logging.error(f"Log check failed for guild {guild.id} - {server_name}: {e}") 81 | 82 | async def process_and_send(self, line, webhook_url, server_name): 83 | try: 84 | match = re.search(r"\[Chat::(Global|Local|Guild)\]\['([^']+)'.*?\]: (.*)", line) 85 | if match: 86 | group, username, message = match.groups() 87 | if group.lower() in self.group_filter: 88 | return 89 | if any(bp in message for bp in self.blocked_phrases): 90 | return 91 | async with aiohttp.ClientSession() as session: 92 | await session.post(webhook_url, json={"username": f"{username} ({server_name})", "content": message}) 93 | await asyncio.sleep(1) 94 | except: 95 | pass 96 | 97 | @commands.Cog.listener() 98 | async def on_message(self, message: discord.Message): 99 | if message.author.bot or not message.guild or not message.content: 100 | return 101 | 102 | configs = await get_chat(message.guild.id) 103 | if not configs: 104 | return 105 | 106 | for config in configs: 107 | server_name, chat_channel_id, log_path, webhook_url = config 108 | 109 | if message.channel.id != int(chat_channel_id): 110 | continue 111 | 112 | details = await fetch_server_details(message.guild.id, server_name) 113 | if not details: 114 | continue 115 | 116 | host = details[2] 117 | password = details[3] 118 | api_port = details[4] 119 | 120 | api = PalworldAPI(f"http://{host}:{api_port}", password) 121 | await api.make_announcement(f"[{message.author.name}]: {message.content}") 122 | 123 | async def server_names(self, interaction: discord.Interaction, current: str): 124 | guild_id = interaction.guild.id 125 | server_names = await server_autocomplete(guild_id, current) 126 | return [app_commands.Choice(name=name, value=name) for name in server_names] 127 | 128 | chat_group = app_commands.Group(name="chat", description="Configure chat feed and relay", default_permissions=discord.Permissions(administrator=True), guild_only=True) 129 | 130 | @chat_group.command(name="setup", description="Configure chat feed and relay") 131 | @app_commands.describe(server="Select the server name") 132 | @app_commands.autocomplete(server=server_names) 133 | async def setupchat(self, interaction: discord.Interaction, server: str): 134 | modal = ChatSetupModal(title="Setup Chat Feed/Relay") 135 | 136 | async def on_submit_override(modal_interaction: discord.Interaction): 137 | await modal_interaction.response.defer(ephemeral=True) 138 | 139 | chat_channel_id = int(modal.children[0].value) 140 | log_path = modal.children[1].value 141 | webhook_url = modal.children[2].value 142 | 143 | try: 144 | await set_chat( 145 | interaction.guild_id, 146 | server, 147 | chat_channel_id, 148 | log_path, 149 | webhook_url 150 | ) 151 | await modal_interaction.followup.send("Chat feed and relay configured successfully.", ephemeral=True) 152 | except Exception as e: 153 | await modal_interaction.followup.send(f"Failed to save chat config: {e}", ephemeral=True) 154 | logging.error(f"Failed to save chat config: {e}") 155 | 156 | modal.on_submit = on_submit_override 157 | await interaction.response.send_modal(modal) 158 | 159 | @chat_group.command(name="remove", description="Remove chat config for a server") 160 | @app_commands.describe(server="Select the server name to remove") 161 | @app_commands.autocomplete(server=server_names) 162 | async def removechat(self, interaction: discord.Interaction, server: str): 163 | try: 164 | await delete_chat(interaction.guild.id, server) 165 | await interaction.response.send_message("Chat config removed.", ephemeral=True) 166 | except Exception as e: 167 | await interaction.response.send_message(f"Failed to remove chat config: {e}", ephemeral=True) 168 | logging.error(f"Failed to remove chat config: {e}") 169 | 170 | @chat_group.command(name="wipe", description="Remove all chat configs for this server") 171 | async def wipechat(self, interaction: discord.Interaction): 172 | try: 173 | configs = await get_chat(interaction.guild.id) 174 | if not configs: 175 | await interaction.response.send_message("No chat configs found to wipe.", ephemeral=True) 176 | return 177 | 178 | for config in configs: 179 | server_name = config[0] 180 | await delete_chat(interaction.guild.id, server_name) 181 | 182 | await interaction.response.send_message("All chat configs wiped for this server.", ephemeral=True) 183 | except Exception as e: 184 | await interaction.response.send_message(f"Failed to wipe chat configs: {e}", ephemeral=True) 185 | logging.error(f"Failed to wipe chat configs: {e}") 186 | 187 | @check_logs.before_loop 188 | async def before_check_logs(self): 189 | await self.bot.wait_until_ready() 190 | 191 | async def setup(bot): 192 | await bot.add_cog(ChatCog(bot)) 193 | -------------------------------------------------------------------------------- /cogs/rcon/pdefender.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import discord 4 | from discord.ext import commands 5 | from discord import app_commands 6 | from utils.rconutility import RconUtility 7 | from utils.database import fetch_server_details, server_autocomplete 8 | 9 | class PalDefenderCog(commands.Cog): 10 | def __init__(self, bot: commands.Bot): 11 | self.bot = bot 12 | self.rcon = RconUtility() 13 | self.servers = [] 14 | self.pals = [] 15 | self.items = [] 16 | self.load_pals() 17 | self.load_items() 18 | bot.loop.create_task(self.load_servers()) 19 | 20 | async def load_servers(self): 21 | self.servers = [] 22 | 23 | def load_pals(self): 24 | path = os.path.join("gamedata", "pals.json") 25 | with open(path, "r", encoding="utf-8") as f: 26 | self.pals = json.load(f).get("pals", []) 27 | 28 | def load_items(self): 29 | path = os.path.join("gamedata", "items.json") 30 | with open(path, "r", encoding="utf-8") as f: 31 | self.items = json.load(f).get("items", []) 32 | 33 | async def get_server_info(self, guild_id: int, server_name: str): 34 | details = await fetch_server_details(guild_id, server_name) 35 | if details: 36 | return {"host": details[2], "password": details[3], "port": details[5]} 37 | 38 | async def autocomplete_server(self, interaction: discord.Interaction, current: str): 39 | guild_id = interaction.guild.id if interaction.guild else 0 40 | server_names = await server_autocomplete(guild_id, current) 41 | return [app_commands.Choice(name=name, value=name) for name in server_names[:25]] 42 | 43 | async def autocomplete_pal(self, interaction: discord.Interaction, current: str): 44 | results = [] 45 | for pal in self.pals: 46 | name = pal.get("name", "") 47 | dev_name = pal.get("id", "") 48 | if current.lower() in name.lower() or current.lower() in dev_name.lower(): 49 | display = f"{name} ({dev_name})" 50 | results.append(app_commands.Choice(name=display, value=dev_name)) 51 | return results[:15] 52 | 53 | async def autocomplete_item(self, interaction: discord.Interaction, current: str): 54 | results = [] 55 | for item in self.items: 56 | name = item.get("name", "") 57 | item_id = item.get("id", "") 58 | if current.lower() in name.lower() or current.lower() in item_id.lower(): 59 | display = f"{name} ({item_id})" 60 | results.append(app_commands.Choice(name=display, value=item_id)) 61 | return results[:15] 62 | 63 | @app_commands.command(name="reloadcfg", description="Reload server config") 64 | @app_commands.describe(server="Server name") 65 | @app_commands.autocomplete(server=autocomplete_server) 66 | @app_commands.default_permissions(administrator=True) 67 | @app_commands.guild_only() 68 | async def reloadcfg(self, interaction: discord.Interaction, server: str): 69 | await interaction.response.defer(ephemeral=True) 70 | if not interaction.guild: 71 | await interaction.followup.send("No guild.", ephemeral=True) 72 | return 73 | info = await self.get_server_info(interaction.guild.id, server) 74 | if not info: 75 | await interaction.followup.send(f"Server not found: {server}", ephemeral=True) 76 | return 77 | response = await self.rcon.rcon_command(info["host"], info["port"], info["password"], "reloadcfg") 78 | await interaction.followup.send(response, ephemeral=True) 79 | 80 | @app_commands.command(name="destroybase", description="Kill nearest base") 81 | @app_commands.describe(radius="Radius", server="Server") 82 | @app_commands.autocomplete(server=autocomplete_server) 83 | @app_commands.default_permissions(administrator=True) 84 | @app_commands.guild_only() 85 | async def killnearestbase(self, interaction: discord.Interaction, radius: str, server: str): 86 | await interaction.response.defer(ephemeral=True) 87 | if not interaction.guild: 88 | await interaction.followup.send("No guild.", ephemeral=True) 89 | return 90 | info = await self.get_server_info(interaction.guild.id, server) 91 | if not info: 92 | await interaction.followup.send(f"Server not found: {server}", ephemeral=True) 93 | return 94 | response = await self.rcon.rcon_command(info["host"], info["port"], info["password"], f"killnearestbase {radius}") 95 | await interaction.followup.send(response, ephemeral=True) 96 | 97 | @app_commands.command(name="getbase", description="Get nearest base") 98 | @app_commands.describe(radius="Radius", server="Server") 99 | @app_commands.autocomplete(server=autocomplete_server) 100 | @app_commands.default_permissions(administrator=True) 101 | @app_commands.guild_only() 102 | async def getnearestbase(self, interaction: discord.Interaction, radius: str, server: str): 103 | await interaction.response.defer(ephemeral=True) 104 | if not interaction.guild: 105 | await interaction.followup.send("No guild.", ephemeral=True) 106 | return 107 | info = await self.get_server_info(interaction.guild.id, server) 108 | if not info: 109 | await interaction.followup.send(f"Server not found: {server}", ephemeral=True) 110 | return 111 | response = await self.rcon.rcon_command(info["host"], info["port"], info["password"], f"getnearestbase {radius}") 112 | await interaction.followup.send(response, ephemeral=True) 113 | 114 | @app_commands.command(name="givepal", description="Give a Pal") 115 | @app_commands.describe(userid="User ID", palid="Pal name", level="Level", server="Server") 116 | @app_commands.autocomplete(server=autocomplete_server, palid=autocomplete_pal) 117 | @app_commands.default_permissions(administrator=True) 118 | @app_commands.guild_only() 119 | async def givepal(self, interaction: discord.Interaction, userid: str, palid: str, level: str, server: str): 120 | await interaction.response.defer(ephemeral=True) 121 | if not interaction.guild: 122 | await interaction.followup.send("No guild.", ephemeral=True) 123 | return 124 | info = await self.get_server_info(interaction.guild.id, server) 125 | if not info: 126 | await interaction.followup.send(f"Server not found: {server}", ephemeral=True) 127 | return 128 | pal_data = next((x for x in self.pals if x["id"] == palid or x["name"] == palid), None) 129 | if not pal_data: 130 | await interaction.followup.send(f"Pal not found: {palid}", ephemeral=True) 131 | return 132 | cmd = f"givepal {userid} {pal_data['id']} {level}" 133 | response = await self.rcon.rcon_command(info["host"], info["port"], info["password"], cmd) 134 | embed = discord.Embed(title=f"GivePal on {server}") 135 | embed.description = response 136 | await interaction.followup.send(embed=embed, ephemeral=True) 137 | 138 | @app_commands.command(name="giveitem", description="Give an item") 139 | @app_commands.describe(userid="User ID", itemid="Item name", amount="Amount", server="Server") 140 | @app_commands.autocomplete(server=autocomplete_server, itemid=autocomplete_item) 141 | @app_commands.default_permissions(administrator=True) 142 | @app_commands.guild_only() 143 | async def giveitem(self, interaction: discord.Interaction, userid: str, itemid: str, amount: str, server: str): 144 | await interaction.response.defer(ephemeral=True) 145 | if not interaction.guild: 146 | await interaction.followup.send("No guild.", ephemeral=True) 147 | return 148 | info = await self.get_server_info(interaction.guild.id, server) 149 | if not info: 150 | await interaction.followup.send(f"Server not found: {server}", ephemeral=True) 151 | return 152 | item_data = next((x for x in self.items if x["id"] == itemid or x["name"] == itemid), None) 153 | if not item_data: 154 | await interaction.followup.send(f"Item not found: {itemid}", ephemeral=True) 155 | return 156 | cmd = f"give {userid} {item_data['id']} {amount}" 157 | response = await self.rcon.rcon_command(info["host"], info["port"], info["password"], cmd) 158 | embed = discord.Embed(title=f"GiveItem on {server}") 159 | embed.description = response 160 | await interaction.followup.send(embed=embed, ephemeral=True) 161 | 162 | @app_commands.command(name="deleteitem", description="Delete an item") 163 | @app_commands.describe(userid="User ID", itemid="Item name", amount="Amount.", server="Server") 164 | @app_commands.autocomplete(server=autocomplete_server, itemid=autocomplete_item) 165 | @app_commands.default_permissions(administrator=True) 166 | @app_commands.guild_only() 167 | async def deleteitem(self, interaction: discord.Interaction, userid: str, itemid: str, amount: str, server: str): 168 | await interaction.response.defer(ephemeral=True) 169 | if not interaction.guild: 170 | await interaction.followup.send("No guild.", ephemeral=True) 171 | return 172 | info = await self.get_server_info(interaction.guild.id, server) 173 | if not info: 174 | await interaction.followup.send(f"Server not found: {server}", ephemeral=True) 175 | return 176 | item_data = next((x for x in self.items if x["id"] == itemid or x["name"] == itemid), None) 177 | if not item_data: 178 | await interaction.followup.send(f"Item not found: {itemid}", ephemeral=True) 179 | return 180 | cmd = f"delitem {userid} {item_data['id']} {amount}" 181 | response = await self.rcon.rcon_command(info["host"], info["port"], info["password"], cmd) 182 | embed = discord.Embed(title=f"DeleteItem on {server}") 183 | embed.description = response 184 | await interaction.followup.send(embed=embed, ephemeral=True) 185 | 186 | @app_commands.command(name="givexp", description="Give experience") 187 | @app_commands.describe(userid="User ID", amount="Amount", server="Server") 188 | @app_commands.autocomplete(server=autocomplete_server) 189 | @app_commands.default_permissions(administrator=True) 190 | @app_commands.guild_only() 191 | async def givexp(self, interaction: discord.Interaction, userid: str, amount: str, server: str): 192 | await interaction.response.defer(ephemeral=True) 193 | if not interaction.guild: 194 | await interaction.followup.send("No guild.", ephemeral=True) 195 | return 196 | info = await self.get_server_info(interaction.guild.id, server) 197 | if not info: 198 | await interaction.followup.send(f"Server not found: {server}", ephemeral=True) 199 | return 200 | cmd = f"give_exp {userid} {amount}" 201 | response = await self.rcon.rcon_command(info["host"], info["port"], info["password"], cmd) 202 | embed = discord.Embed(title=f"GiveEXP on {server}") 203 | embed.description = response 204 | await interaction.followup.send(embed=embed, ephemeral=True) 205 | 206 | # export pals player userid 207 | @app_commands.command(name="exportpals", description="Export pals") 208 | @app_commands.describe(userid="User ID", server="Server") 209 | @app_commands.autocomplete(server=autocomplete_server) 210 | @app_commands.default_permissions(administrator=True) 211 | @app_commands.guild_only() 212 | async def exportpals(self, interaction: discord.Interaction, userid: str, server: str): 213 | await interaction.response.defer(ephemeral=True) 214 | if not interaction.guild: 215 | await interaction.followup.send("No guild.", ephemeral=True) 216 | return 217 | info = await self.get_server_info(interaction.guild.id, server) 218 | if not info: 219 | await interaction.followup.send(f"Server not found: {server}", ephemeral=True) 220 | return 221 | response = await self.rcon.rcon_command(info["host"], info["port"], info["password"], f"exportpals {userid}") 222 | embed = discord.Embed(title=f"ExportPals on {server}") 223 | embed.description = response 224 | await interaction.followup.send(embed=embed, ephemeral=True) 225 | 226 | @app_commands.command(name="exportguilds", description="Export guilds") 227 | @app_commands.describe(server="Server") 228 | @app_commands.autocomplete(server=autocomplete_server) 229 | @app_commands.default_permissions(administrator=True) 230 | @app_commands.guild_only() 231 | async def exportguilds(self, interaction: discord.Interaction, server: str): 232 | await interaction.response.defer(ephemeral=True) 233 | if not interaction.guild: 234 | await interaction.followup.send("No guild.", ephemeral=True) 235 | return 236 | info = await self.get_server_info(interaction.guild.id, server) 237 | if not info: 238 | await interaction.followup.send(f"Server not found: {server}", ephemeral=True) 239 | return 240 | response = await self.rcon.rcon_command(info["host"], info["port"], info["password"], "exportguilds") 241 | embed = discord.Embed(title=f"ExportGuilds on {server}") 242 | embed.description = response 243 | await interaction.followup.send(embed=embed, ephemeral=True) 244 | 245 | async def setup(bot: commands.Bot): 246 | await bot.add_cog(PalDefenderCog(bot)) 247 | -------------------------------------------------------------------------------- /utils/database.py: -------------------------------------------------------------------------------- 1 | import aiosqlite 2 | import os 3 | import datetime 4 | 5 | DATABASE_PATH = os.path.join('data', 'palworld.db') 6 | 7 | async def db_connection(): 8 | conn = None 9 | try: 10 | conn = await aiosqlite.connect(DATABASE_PATH) 11 | except aiosqlite.Error as e: 12 | print(e) 13 | return conn 14 | 15 | async def initialize_db(): 16 | commands = [ 17 | """CREATE TABLE IF NOT EXISTS servers ( 18 | guild_id INTEGER NOT NULL, 19 | server_name TEXT NOT NULL, 20 | host TEXT NOT NULL, 21 | password TEXT NOT NULL, 22 | api_port INTEGER, 23 | rcon_port INTEGER, 24 | PRIMARY KEY (guild_id, server_name) 25 | )""", 26 | """CREATE TABLE IF NOT EXISTS players ( 27 | user_id TEXT PRIMARY KEY, 28 | name TEXT NOT NULL, 29 | account_name TEXT NOT NULL, 30 | player_id TEXT NOT NULL, 31 | ip TEXT NOT NULL, 32 | ping REAL NOT NULL, 33 | location_x REAL NOT NULL, 34 | location_y REAL NOT NULL, 35 | level INTEGER NOT NULL 36 | )""", 37 | """CREATE TABLE IF NOT EXISTS whitelist ( 38 | player_id TEXT PRIMARY KEY, 39 | whitelisted BOOLEAN NOT NULL 40 | )""", 41 | """CREATE TABLE IF NOT EXISTS whitelist_status ( 42 | guild_id INTEGER NOT NULL, 43 | server_name TEXT NOT NULL, 44 | enabled BOOLEAN NOT NULL, 45 | PRIMARY KEY (guild_id, server_name) 46 | )""", 47 | """CREATE TABLE IF NOT EXISTS bans ( 48 | player_id TEXT NOT NULL, 49 | reason TEXT NOT NULL, 50 | timestamp DEFAULT CURRENT_TIMESTAMP 51 | )""", 52 | """CREATE TABLE IF NOT EXISTS server_logs ( 53 | guild_id INTEGER NOT NULL, 54 | channel_id INTEGER NOT NULL, 55 | server_name TEXT NOT NULL, 56 | PRIMARY KEY (guild_id, server_name) 57 | )""", 58 | """CREATE TABLE IF NOT EXISTS query_logs ( 59 | guild_id INTEGER NOT NULL, 60 | channel_id INTEGER NOT NULL, 61 | server_name TEXT NOT NULL, 62 | message_id INTEGER NOT NULL, 63 | player_message_id INTEGER NOT NULL, 64 | PRIMARY KEY (guild_id, server_name) 65 | )""", 66 | """CREATE TABLE IF NOT EXISTS player_tracking ( 67 | guild_id INTEGER PRIMARY KEY, 68 | enabled BOOLEAN NOT NULL 69 | )""", 70 | """CREATE TABLE IF NOT EXISTS chat_settings ( 71 | guild_id INTEGER NOT NULL, 72 | server_name TEXT NOT NULL, 73 | log_channel_id INTEGER NOT NULL, 74 | log_path TEXT NOT NULL, 75 | webhook_url TEXT NOT NULL, 76 | PRIMARY KEY (guild_id, server_name) 77 | )""", 78 | """CREATE TABLE IF NOT EXISTS backups ( 79 | guild_id INTEGER NOT NULL, 80 | server_name TEXT NOT NULL, 81 | path TEXT NOT NULL, 82 | channel_id INTEGER NOT NULL, 83 | interval_minutes INTEGER NOT NULL, 84 | PRIMARY KEY (guild_id, server_name) 85 | )""", 86 | """CREATE TABLE IF NOT EXISTS player_sessions ( 87 | user_id TEXT PRIMARY KEY, 88 | total_time INTEGER NOT NULL DEFAULT 0, 89 | session_start TIMESTAMP, 90 | last_session INTEGER NOT NULL DEFAULT 0 91 | )""" 92 | ] 93 | conn = await db_connection() 94 | if conn is not None: 95 | cursor = await conn.cursor() 96 | for command in commands: 97 | await cursor.execute(command) 98 | try: 99 | await cursor.execute("ALTER TABLE servers ADD COLUMN rcon_port INTEGER") 100 | except: 101 | pass 102 | await conn.commit() 103 | await conn.close() 104 | 105 | async def add_player(player): 106 | conn = await db_connection() 107 | if conn is not None: 108 | cursor = await conn.cursor() 109 | await cursor.execute(""" 110 | INSERT OR REPLACE INTO players (user_id, name, account_name, player_id, ip, ping, location_x, location_y, level) 111 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) 112 | """, ( 113 | player['userId'], 114 | player['name'], 115 | player['accountName'], 116 | player['playerId'], 117 | player['ip'], 118 | player['ping'], 119 | player['location_x'], 120 | player['location_y'], 121 | player['level'] 122 | )) 123 | await conn.commit() 124 | await conn.close() 125 | 126 | async def fetch_player(user_id): 127 | conn = await db_connection() 128 | if conn is not None: 129 | cursor = await conn.cursor() 130 | await cursor.execute("SELECT * FROM players WHERE user_id = ?", (user_id,)) 131 | player = await cursor.fetchone() 132 | await conn.close() 133 | return player 134 | 135 | async def player_autocomplete(current): 136 | conn = await db_connection() 137 | if conn is not None: 138 | cursor = await conn.cursor() 139 | await cursor.execute("SELECT user_id, name FROM players WHERE name LIKE ?", (f'%{current}%',)) 140 | players = await cursor.fetchall() 141 | await conn.close() 142 | return [(player[0], player[1]) for player in players] 143 | 144 | async def fetch_all_servers(): 145 | conn = await db_connection() 146 | if conn is not None: 147 | cursor = await conn.cursor() 148 | await cursor.execute("SELECT * FROM servers") 149 | servers = await cursor.fetchall() 150 | await conn.close() 151 | return servers 152 | 153 | async def add_server(guild_id, server_name, host, password, api_port, rcon_port): 154 | conn = await db_connection() 155 | if conn is not None: 156 | cursor = await conn.cursor() 157 | await cursor.execute("INSERT INTO servers (guild_id, server_name, host, password, api_port, rcon_port) VALUES (?, ?, ?, ?, ?, ?)", 158 | (guild_id, server_name, host, password, api_port, rcon_port)) 159 | await conn.commit() 160 | await conn.close() 161 | 162 | async def fetch_server_details(guild_id, server_name): 163 | conn = await db_connection() 164 | if conn is not None: 165 | cursor = await conn.cursor() 166 | await cursor.execute("SELECT guild_id, server_name, host, password, api_port, rcon_port FROM servers WHERE guild_id = ? AND server_name = ?", (guild_id, server_name)) 167 | server_details = await cursor.fetchone() 168 | await conn.close() 169 | return server_details 170 | 171 | async def remove_server(guild_id, server_name): 172 | conn = await db_connection() 173 | if conn is not None: 174 | cursor = await conn.cursor() 175 | await cursor.execute("DELETE FROM servers WHERE guild_id = ? AND server_name = ?", (guild_id, server_name)) 176 | await conn.commit() 177 | await conn.close() 178 | 179 | async def remove_whitelist_status(guild_id, server_name): 180 | conn = await db_connection() 181 | if conn is not None: 182 | cursor = await conn.cursor() 183 | await cursor.execute("DELETE FROM whitelist_status WHERE guild_id = ? AND server_name = ?", (guild_id, server_name)) 184 | await conn.commit() 185 | await conn.close() 186 | 187 | async def server_autocomplete(guild_id, current): 188 | conn = await db_connection() 189 | if conn is not None: 190 | cursor = await conn.cursor() 191 | await cursor.execute("SELECT server_name FROM servers WHERE guild_id = ? AND server_name LIKE ?", (guild_id, f'%{current}%')) 192 | servers = await cursor.fetchall() 193 | await conn.close() 194 | return [server[0] for server in servers] 195 | 196 | # Server Logs 197 | async def add_logchannel(guild_id, channel_id, server_name): 198 | conn = await db_connection() 199 | if conn is not None: 200 | cursor = await conn.cursor() 201 | await cursor.execute(""" 202 | INSERT OR REPLACE INTO server_logs (guild_id, channel_id, server_name) 203 | VALUES (?, ?, ?) 204 | """, (guild_id, channel_id, server_name)) 205 | await conn.commit() 206 | await conn.close() 207 | 208 | async def remove_logchannel(guild_id, server_name): 209 | conn = await db_connection() 210 | if conn is not None: 211 | cursor = await conn.cursor() 212 | await cursor.execute("DELETE FROM server_logs WHERE guild_id = ? AND server_name = ?", (guild_id, server_name)) 213 | await conn.commit() 214 | await conn.close() 215 | 216 | async def fetch_logchannel(guild_id, server_name): 217 | conn = await db_connection() 218 | if conn is not None: 219 | cursor = await conn.cursor() 220 | await cursor.execute("SELECT channel_id FROM server_logs WHERE guild_id = ? AND server_name = ?", (guild_id, server_name)) 221 | result = await cursor.fetchone() 222 | await conn.close() 223 | return result[0] if result else None 224 | 225 | # Query Server 226 | async def add_query(guild_id, channel_id, server_name, message_id, player_message_id): 227 | conn = await db_connection() 228 | if conn is not None: 229 | cursor = await conn.cursor() 230 | await cursor.execute(""" 231 | INSERT OR REPLACE INTO query_logs (guild_id, channel_id, server_name, message_id, player_message_id) 232 | VALUES (?, ?, ?, ?, ?) 233 | """, (guild_id, channel_id, server_name, message_id, player_message_id)) 234 | await conn.commit() 235 | await conn.close() 236 | 237 | async def fetch_query(guild_id, server_name): 238 | conn = await db_connection() 239 | if conn is not None: 240 | cursor = await conn.cursor() 241 | await cursor.execute(""" 242 | SELECT channel_id, message_id, player_message_id 243 | FROM query_logs 244 | WHERE guild_id = ? AND server_name = ? 245 | """, (guild_id, server_name)) 246 | result = await cursor.fetchone() 247 | await conn.close() 248 | return result if result else None 249 | 250 | async def delete_query(guild_id, server_name): 251 | conn = await db_connection() 252 | if conn is not None: 253 | cursor = await conn.cursor() 254 | await cursor.execute("DELETE FROM query_logs WHERE guild_id = ? AND server_name = ?", (guild_id, server_name)) 255 | await conn.commit() 256 | await conn.close() 257 | 258 | # Status Tracking 259 | async def set_tracking(guild_id, enabled: bool): 260 | conn = await db_connection() 261 | if conn: 262 | cursor = await conn.cursor() 263 | await cursor.execute(""" 264 | INSERT OR REPLACE INTO player_tracking (guild_id, enabled) VALUES (?, ?) 265 | """, (guild_id, enabled)) 266 | await conn.commit() 267 | await conn.close() 268 | 269 | async def get_tracking(): 270 | conn = await db_connection() 271 | if conn: 272 | cursor = await conn.cursor() 273 | await cursor.execute("SELECT guild_id FROM player_tracking WHERE enabled = 1") 274 | rows = await cursor.fetchall() 275 | await conn.close() 276 | return [row[0] for row in rows] 277 | 278 | # Chat Relay/Feed 279 | async def set_chat(guild_id, server_name, chat_channel_id, log_path, webhook_url): 280 | conn = await db_connection() 281 | if conn: 282 | cursor = await conn.cursor() 283 | await cursor.execute(""" 284 | INSERT OR REPLACE INTO chat_settings ( 285 | guild_id, server_name, log_channel_id, log_path, webhook_url 286 | ) VALUES (?, ?, ?, ?, ?) 287 | """, (guild_id, server_name, chat_channel_id, log_path, webhook_url)) 288 | await conn.commit() 289 | await conn.close() 290 | 291 | async def get_chat(guild_id): 292 | conn = await db_connection() 293 | if conn: 294 | cursor = await conn.cursor() 295 | await cursor.execute(""" 296 | SELECT server_name, log_channel_id, log_path, webhook_url 297 | FROM chat_settings WHERE guild_id = ? 298 | """, (guild_id,)) 299 | result = await cursor.fetchall() 300 | await conn.close() 301 | return result 302 | 303 | async def delete_chat(guild_id, server_name): 304 | conn = await db_connection() 305 | if conn: 306 | cursor = await conn.cursor() 307 | await cursor.execute("DELETE FROM chat_settings WHERE guild_id = ? AND server_name = ?", (guild_id, server_name)) 308 | await conn.commit() 309 | await conn.close() 310 | 311 | # Backups 312 | async def set_backup(guild_id, server_name, path, channel_id, interval_minutes): 313 | conn = await db_connection() 314 | if conn: 315 | cursor = await conn.cursor() 316 | await cursor.execute(""" 317 | INSERT OR REPLACE INTO backups (guild_id, server_name, path, channel_id, interval_minutes) 318 | VALUES (?, ?, ?, ?, ?) 319 | """, (guild_id, server_name, path, channel_id, interval_minutes)) 320 | await conn.commit() 321 | await conn.close() 322 | 323 | async def all_backups(): 324 | conn = await db_connection() 325 | if conn: 326 | cursor = await conn.cursor() 327 | await cursor.execute(""" 328 | SELECT guild_id, server_name, path, channel_id, interval_minutes 329 | FROM backups 330 | """) 331 | rows = await cursor.fetchall() 332 | await conn.close() 333 | return rows 334 | 335 | async def del_backup(guild_id, server_name): 336 | conn = await db_connection() 337 | if conn: 338 | cursor = await conn.cursor() 339 | await cursor.execute(""" 340 | DELETE FROM backups 341 | WHERE guild_id = ? AND server_name = ? 342 | """, (guild_id, server_name)) 343 | await conn.commit() 344 | await conn.close() 345 | 346 | # Player Time Tracking 347 | async def track_sessions(current_online: set, previous_online: set, timestamp: str): 348 | conn = await db_connection() 349 | if not conn: 350 | return 351 | 352 | cursor = await conn.cursor() 353 | now = datetime.datetime.fromisoformat(timestamp) 354 | 355 | for uid in current_online: 356 | await cursor.execute("SELECT session_start, total_time FROM player_sessions WHERE user_id = ?", (uid,)) 357 | row = await cursor.fetchone() 358 | if row is None: 359 | await cursor.execute( 360 | "INSERT INTO player_sessions (user_id, total_time, session_start, last_session) VALUES (?, 0, ?, 0)", 361 | (uid, timestamp) 362 | ) 363 | elif row[0]: 364 | dt_start = datetime.datetime.fromisoformat(row[0]) 365 | delta = int((now - dt_start).total_seconds()) 366 | new_total = row[1] + delta 367 | await cursor.execute( 368 | "UPDATE player_sessions SET total_time = ?, session_start = ?, last_session = ? WHERE user_id = ?", 369 | (new_total, timestamp, delta, uid) 370 | ) 371 | 372 | for uid in previous_online - current_online: 373 | await cursor.execute("SELECT session_start, total_time FROM player_sessions WHERE user_id = ?", (uid,)) 374 | row = await cursor.fetchone() 375 | if row and row[0]: 376 | dt_start = datetime.datetime.fromisoformat(row[0]) 377 | delta = int((now - dt_start).total_seconds()) 378 | new_total = row[1] + delta 379 | await cursor.execute( 380 | "UPDATE player_sessions SET total_time = ?, session_start = NULL, last_session = ? WHERE user_id = ?", 381 | (new_total, delta, uid) 382 | ) 383 | 384 | await conn.commit() 385 | await conn.close() 386 | 387 | async def get_player_session(user_id: str): 388 | conn = await db_connection() 389 | if conn is not None: 390 | cursor = await conn.cursor() 391 | await cursor.execute("SELECT user_id, total_time, session_start FROM player_sessions WHERE user_id = ?", (user_id,)) 392 | row = await cursor.fetchone() 393 | await conn.close() 394 | return row 395 | 396 | if __name__ == "__main__": 397 | import asyncio 398 | asyncio.run(initialize_db()) 399 | -------------------------------------------------------------------------------- /gamedata/pals.json: -------------------------------------------------------------------------------- 1 | { 2 | "pals": [ 3 | { 4 | "name": "Anubis", 5 | "id": "Anubis" 6 | }, 7 | { 8 | "name": "Anubis (Boss)", 9 | "id": "Boss_Anubis" 10 | }, 11 | { 12 | "name": "Arsox", 13 | "id": "FlameBuffalo" 14 | }, 15 | { 16 | "name": "Arsox (Boss)", 17 | "id": "BOSS_FlameBuffalo" 18 | }, 19 | { 20 | "name": "Astegon", 21 | "id": "BlackMetalDragon" 22 | }, 23 | { 24 | "name": "Astegon (Boss)", 25 | "id": "BOSS_BlackMetalDragon" 26 | }, 27 | { 28 | "name": "Azurmane", 29 | "id": "BlueThunderHorse" 30 | }, 31 | { 32 | "name": "Azurmane (Boss)", 33 | "id": "BOSS_BlueThunderHorse" 34 | }, 35 | { 36 | "name": "Azurobe", 37 | "id": "BlueDragon" 38 | }, 39 | { 40 | "name": "Azurobe (Boss)", 41 | "id": "BOSS_BlueDragon" 42 | }, 43 | { 44 | "name": "Azurobe Cryst", 45 | "id": "BlueDragon_Ice" 46 | }, 47 | { 48 | "name": "Bastigor", 49 | "id": "SnowTigerBeastman" 50 | }, 51 | { 52 | "name": "Bastigor (Boss)", 53 | "id": "BOSS_SnowTigerBeastman" 54 | }, 55 | { 56 | "name": "Bastigor (Gym)", 57 | "id": "GYM_SnowTigerBeastman" 58 | }, 59 | { 60 | "name": "Bastigor (Gym)", 61 | "id": "GYM_SnowTigerBeastman_2" 62 | }, 63 | { 64 | "name": "Beakon", 65 | "id": "ThunderBird" 66 | }, 67 | { 68 | "name": "Beakon (Boss)", 69 | "id": "BOSS_ThunderBird" 70 | }, 71 | { 72 | "name": "Beegarde", 73 | "id": "SoldierBee" 74 | }, 75 | { 76 | "name": "Beegarde (Boss)", 77 | "id": "BOSS_SoldierBee" 78 | }, 79 | { 80 | "name": "Bellanoir", 81 | "id": "NightLady" 82 | }, 83 | { 84 | "name": "Bellanoir (Boss)", 85 | "id": "BOSS_NightLady" 86 | }, 87 | { 88 | "name": "Bellanoir (Raid)", 89 | "id": "RAID_NightLady" 90 | }, 91 | { 92 | "name": "Bellanoir Libero", 93 | "id": "NightLady_Dark" 94 | }, 95 | { 96 | "name": "Bellanoir Libero (Boss)", 97 | "id": "BOSS_NightLady_Dark" 98 | }, 99 | { 100 | "name": "Bellanoir Libero (Raid)", 101 | "id": "RAID_NightLady_Dark" 102 | }, 103 | { 104 | "name": "Bellanoir Libero (Raid)", 105 | "id": "RAID_NightLady_Dark_2" 106 | }, 107 | { 108 | "name": "Blazamut", 109 | "id": "KingBahamut" 110 | }, 111 | { 112 | "name": "Blazamut (Boss)", 113 | "id": "BOSS_KingBahamut" 114 | }, 115 | { 116 | "name": "Blazamut Ryu", 117 | "id": "KingBahamut_Dragon" 118 | }, 119 | { 120 | "name": "Blazamut Ryu (Boss)", 121 | "id": "BOSS_KingBahamut_Dragon" 122 | }, 123 | { 124 | "name": "Blazamut Ryu (Raid)", 125 | "id": "RAID_KingBahamut_Dragon" 126 | }, 127 | { 128 | "name": "Blazamut Ryu (Raid)", 129 | "id": "RAID_KingBahamut_Dragon_2" 130 | }, 131 | { 132 | "name": "Blazehowl", 133 | "id": "Manticore" 134 | }, 135 | { 136 | "name": "Blazehowl (Boss)", 137 | "id": "BOSS_Manticore" 138 | }, 139 | { 140 | "name": "Blazehowl Noct", 141 | "id": "Manticore_Dark" 142 | }, 143 | { 144 | "name": "Blazehowl Noct (Boss)", 145 | "id": "BOSS_Manticore_Dark" 146 | }, 147 | { 148 | "name": "Blazehowl Noct (Predator)", 149 | "id": "PREDATOR_Manticore_Dark" 150 | }, 151 | { 152 | "name": "Blue Slime", 153 | "id": "YakushimaMonster001_Blue" 154 | }, 155 | { 156 | "name": "Boltmane", 157 | "id": "ElecLion" 158 | }, 159 | { 160 | "name": "Boltmane (Boss)", 161 | "id": "BOSS_ElecLion" 162 | }, 163 | { 164 | "name": "Braloha", 165 | "id": "Plesiosaur" 166 | }, 167 | { 168 | "name": "Braloha (Boss)", 169 | "id": "BOSS_Plesiosaur" 170 | }, 171 | { 172 | "name": "Bristla", 173 | "id": "LittleBriarRose" 174 | }, 175 | { 176 | "name": "Bristla (Boss)", 177 | "id": "BOSS_LittleBriarRose" 178 | }, 179 | { 180 | "name": "Broncherry", 181 | "id": "SakuraSaurus" 182 | }, 183 | { 184 | "name": "Broncherry (Boss)", 185 | "id": "BOSS_SakuraSaurus" 186 | }, 187 | { 188 | "name": "Broncherry Aqua", 189 | "id": "SakuraSaurus_Water" 190 | }, 191 | { 192 | "name": "Broncherry Aqua (Boss)", 193 | "id": "BOSS_SakuraSaurus_Water" 194 | }, 195 | { 196 | "name": "Bushi", 197 | "id": "Ronin" 198 | }, 199 | { 200 | "name": "Bushi (Boss)", 201 | "id": "BOSS_Ronin" 202 | }, 203 | { 204 | "name": "Bushi Noct", 205 | "id": "Ronin_Dark" 206 | }, 207 | { 208 | "name": "Bushi Noct (Boss)", 209 | "id": "BOSS_Ronin_Dark" 210 | }, 211 | { 212 | "name": "Bushi Noct (Predator)", 213 | "id": "PREDATOR_Ronin_Dark" 214 | }, 215 | { 216 | "name": "Caprity", 217 | "id": "BerryGoat" 218 | }, 219 | { 220 | "name": "Caprity (Boss)", 221 | "id": "BOSS_BerryGoat" 222 | }, 223 | { 224 | "name": "Caprity Noct", 225 | "id": "BerryGoat_Dark" 226 | }, 227 | { 228 | "name": "Caprity Noct (Boss)", 229 | "id": "BOSS_BerryGoat_Dark" 230 | }, 231 | { 232 | "name": "Cattiva", 233 | "id": "PinkCat" 234 | }, 235 | { 236 | "name": "Cattiva (Boss)", 237 | "id": "BOSS_PinkCat" 238 | }, 239 | { 240 | "name": "Cattiva (Quest)", 241 | "id": "Quest_Farmer03_PinkCat" 242 | }, 243 | { 244 | "name": "Cave Bat", 245 | "id": "YakushimaMonster003" 246 | }, 247 | { 248 | "name": "Cawgnito", 249 | "id": "DarkCrow" 250 | }, 251 | { 252 | "name": "Cawgnito (Boss)", 253 | "id": "BOSS_DarkCrow" 254 | }, 255 | { 256 | "name": "Celaray", 257 | "id": "FlyingManta" 258 | }, 259 | { 260 | "name": "Celaray (Boss)", 261 | "id": "BOSS_FlyingManta" 262 | }, 263 | { 264 | "name": "Celaray Lux", 265 | "id": "FlyingManta_Thunder" 266 | }, 267 | { 268 | "name": "Celesdir", 269 | "id": "WhiteDeer" 270 | }, 271 | { 272 | "name": "Celesdir (Boss)", 273 | "id": "BOSS_WhiteDeer" 274 | }, 275 | { 276 | "name": "Chikipi", 277 | "id": "ChickenPal" 278 | }, 279 | { 280 | "name": "Chikipi (Boss)", 281 | "id": "BOSS_ChickenPal" 282 | }, 283 | { 284 | "name": "Chillet", 285 | "id": "WeaselDragon" 286 | }, 287 | { 288 | "name": "Chillet (Boss)", 289 | "id": "BOSS_WeaselDragon" 290 | }, 291 | { 292 | "name": "Chillet Ignis", 293 | "id": "WeaselDragon_Fire" 294 | }, 295 | { 296 | "name": "Chillet Ignis (Boss)", 297 | "id": "BOSS_WeaselDragon_Fire" 298 | }, 299 | { 300 | "name": "Cinnamoth", 301 | "id": "CuteButterfly" 302 | }, 303 | { 304 | "name": "Cinnamoth (Boss)", 305 | "id": "BOSS_CuteButterfly" 306 | }, 307 | { 308 | "name": "Cremis", 309 | "id": "WoolFox" 310 | }, 311 | { 312 | "name": "Cremis (Boss)", 313 | "id": "BOSS_WoolFox" 314 | }, 315 | { 316 | "name": "Croajiro", 317 | "id": "KendoFrog" 318 | }, 319 | { 320 | "name": "Croajiro (Boss)", 321 | "id": "BOSS_KendoFrog" 322 | }, 323 | { 324 | "name": "Croajiro Noct", 325 | "id": "KendoFrog_Dark" 326 | }, 327 | { 328 | "name": "Croajiro Noct (Boss)", 329 | "id": "BOSS_KendoFrog_Dark" 330 | }, 331 | { 332 | "name": "Cryolinx", 333 | "id": "WhiteTiger" 334 | }, 335 | { 336 | "name": "Cryolinx (Boss)", 337 | "id": "BOSS_WhiteTiger" 338 | }, 339 | { 340 | "name": "Cryolinx Terra", 341 | "id": "WhiteTiger_Ground" 342 | }, 343 | { 344 | "name": "Cryolinx Terra (Boss)", 345 | "id": "BOSS_WhiteTiger_Ground" 346 | }, 347 | { 348 | "name": "Cryolinx Terra (Predator)", 349 | "id": "PREDATOR_WhiteTiger_Ground" 350 | }, 351 | { 352 | "name": "Daedream", 353 | "id": "DreamDemon" 354 | }, 355 | { 356 | "name": "Daedream (Boss)", 357 | "id": "BOSS_DreamDemon" 358 | }, 359 | { 360 | "name": "Dazemu", 361 | "id": "FeatherOstrich" 362 | }, 363 | { 364 | "name": "Dazemu (Boss)", 365 | "id": "BOSS_FeatherOstrich" 366 | }, 367 | { 368 | "name": "Dazemu (Predator)", 369 | "id": "PREDATOR_FeatherOstrich" 370 | }, 371 | { 372 | "name": "Dazzi", 373 | "id": "RaijinDaughter" 374 | }, 375 | { 376 | "name": "Dazzi (Boss)", 377 | "id": "BOSS_RaijinDaughter" 378 | }, 379 | { 380 | "name": "Dazzi Noct", 381 | "id": "RaijinDaughter_Water" 382 | }, 383 | { 384 | "name": "Dazzi Noct (Boss)", 385 | "id": "BOSS_RaijinDaughter_Water" 386 | }, 387 | { 388 | "name": "Demon Eye", 389 | "id": "YakushimaBoss001_Small" 390 | }, 391 | { 392 | "name": "Depresso", 393 | "id": "NegativeKoala" 394 | }, 395 | { 396 | "name": "Depresso (Boss)", 397 | "id": "BOSS_NegativeKoala" 398 | }, 399 | { 400 | "name": "Digtoise", 401 | "id": "DrillGame" 402 | }, 403 | { 404 | "name": "Digtoise (Boss)", 405 | "id": "BOSS_DrillGame" 406 | }, 407 | { 408 | "name": "Digtoise (Predator)", 409 | "id": "PREDATOR_DrillGame" 410 | }, 411 | { 412 | "name": "Dinossom", 413 | "id": "FlowerDinosaur" 414 | }, 415 | { 416 | "name": "Dinossom (Boss)", 417 | "id": "BOSS_FlowerDinosaur" 418 | }, 419 | { 420 | "name": "Dinossom (Predator)", 421 | "id": "PREDATOR_FlowerDinosaur" 422 | }, 423 | { 424 | "name": "Dinossom Lux", 425 | "id": "FlowerDinosaur_Electric" 426 | }, 427 | { 428 | "name": "Dinossom Lux (Boss)", 429 | "id": "BOSS_FlowerDinosaur_Electric" 430 | }, 431 | { 432 | "name": "Direhowl", 433 | "id": "Garm" 434 | }, 435 | { 436 | "name": "Direhowl (Boss)", 437 | "id": "BOSS_Garm" 438 | }, 439 | { 440 | "name": "Direhowl (Predator)", 441 | "id": "PREDATOR_Garm" 442 | }, 443 | { 444 | "name": "Dogen", 445 | "id": "SifuDog" 446 | }, 447 | { 448 | "name": "Dogen (Boss)", 449 | "id": "BOSS_SifuDog" 450 | }, 451 | { 452 | "name": "Dogen (Predator)", 453 | "id": "PREDATOR_SifuDog" 454 | }, 455 | { 456 | "name": "Dragostrophe", 457 | "id": "BlackFurDragon" 458 | }, 459 | { 460 | "name": "Dragostrophe (Boss)", 461 | "id": "BOSS_BlackFurDragon" 462 | }, 463 | { 464 | "name": "Dumud", 465 | "id": "LazyCatfish" 466 | }, 467 | { 468 | "name": "Dumud (Boss)", 469 | "id": "BOSS_LazyCatfish" 470 | }, 471 | { 472 | "name": "Dumud Gild", 473 | "id": "LazyCatfish_Gold" 474 | }, 475 | { 476 | "name": "Eikthyrdeer", 477 | "id": "Deer" 478 | }, 479 | { 480 | "name": "Eikthyrdeer (Boss)", 481 | "id": "BOSS_Deer" 482 | }, 483 | { 484 | "name": "Eikthyrdeer Terra", 485 | "id": "Deer_Ground" 486 | }, 487 | { 488 | "name": "Eikthyrdeer Terra (Boss)", 489 | "id": "BOSS_Deer_Ground" 490 | }, 491 | { 492 | "name": "Elizabee", 493 | "id": "QueenBee" 494 | }, 495 | { 496 | "name": "Elizabee (Boss)", 497 | "id": "BOSS_QueenBee" 498 | }, 499 | { 500 | "name": "Elphidran", 501 | "id": "FairyDragon" 502 | }, 503 | { 504 | "name": "Elphidran (Boss)", 505 | "id": "BOSS_FairyDragon" 506 | }, 507 | { 508 | "name": "Elphidran (Predator)", 509 | "id": "PREDATOR_FairyDragon" 510 | }, 511 | { 512 | "name": "Elphidran Aqua", 513 | "id": "FairyDragon_Water" 514 | }, 515 | { 516 | "name": "Elphidran Aqua (Boss)", 517 | "id": "BOSS_FairyDragon_Water" 518 | }, 519 | { 520 | "name": "Enchanted Sword", 521 | "id": "YakushimaMonster002" 522 | }, 523 | { 524 | "name": "Eye of Cthulhu", 525 | "id": "YakushimaBoss001" 526 | }, 527 | { 528 | "name": "Eye of Cthulhu (Boss)", 529 | "id": "BOSS_YakushimaBoss001" 530 | }, 531 | { 532 | "name": "Faleris", 533 | "id": "Horus" 534 | }, 535 | { 536 | "name": "Faleris (Boss)", 537 | "id": "BOSS_Horus" 538 | }, 539 | { 540 | "name": "Faleris (Gym)", 541 | "id": "GYM_Horus" 542 | }, 543 | { 544 | "name": "Faleris (Gym)", 545 | "id": "GYM_Horus_2" 546 | }, 547 | { 548 | "name": "Faleris (Oilrig)", 549 | "id": "Horus_Oilrig" 550 | }, 551 | { 552 | "name": "Faleris Aqua", 553 | "id": "Horus_Water" 554 | }, 555 | { 556 | "name": "Faleris Aqua (Boss)", 557 | "id": "BOSS_Horus_Water" 558 | }, 559 | { 560 | "name": "Faleris Aqua (Predator)", 561 | "id": "PREDATOR_Horus_Water" 562 | }, 563 | { 564 | "name": "Felbat", 565 | "id": "CatVampire" 566 | }, 567 | { 568 | "name": "Felbat (Boss)", 569 | "id": "BOSS_CatVampire" 570 | }, 571 | { 572 | "name": "Fenglope", 573 | "id": "FengyunDeeper" 574 | }, 575 | { 576 | "name": "Fenglope (Boss)", 577 | "id": "BOSS_FengyunDeeper" 578 | }, 579 | { 580 | "name": "Fenglope Lux", 581 | "id": "FengyunDeeper_Electric" 582 | }, 583 | { 584 | "name": "Fenglope Lux (Boss)", 585 | "id": "BOSS_FengyunDeeper_Electric" 586 | }, 587 | { 588 | "name": "Finsider", 589 | "id": "StuffedShark" 590 | }, 591 | { 592 | "name": "Finsider (Boss)", 593 | "id": "BOSS_StuffedShark" 594 | }, 595 | { 596 | "name": "Finsider Ignis", 597 | "id": "StuffedShark_Fire" 598 | }, 599 | { 600 | "name": "Flambelle", 601 | "id": "LavaGirl" 602 | }, 603 | { 604 | "name": "Flambelle (Boss)", 605 | "id": "BOSS_LavaGirl" 606 | }, 607 | { 608 | "name": "Flopie", 609 | "id": "FlowerRabbit" 610 | }, 611 | { 612 | "name": "Flopie (Boss)", 613 | "id": "BOSS_FlowerRabbit" 614 | }, 615 | { 616 | "name": "Foxcicle", 617 | "id": "IceFox" 618 | }, 619 | { 620 | "name": "Foxcicle (Boss)", 621 | "id": "BOSS_IceFox" 622 | }, 623 | { 624 | "name": "Foxparks", 625 | "id": "Kitsunebi" 626 | }, 627 | { 628 | "name": "Foxparks (Boss)", 629 | "id": "BOSS_Kitsunebi" 630 | }, 631 | { 632 | "name": "Foxparks Cryst", 633 | "id": "Kitsunebi_Ice" 634 | }, 635 | { 636 | "name": "Foxparks Cryst (Boss)", 637 | "id": "BOSS_Kitsunebi_Ice" 638 | }, 639 | { 640 | "name": "Frostallion", 641 | "id": "IceHorse" 642 | }, 643 | { 644 | "name": "Frostallion (Boss)", 645 | "id": "BOSS_IceHorse" 646 | }, 647 | { 648 | "name": "Frostallion Noct", 649 | "id": "IceHorse_Dark" 650 | }, 651 | { 652 | "name": "Frostallion Noct (Boss)", 653 | "id": "BOSS_IceHorse_Dark" 654 | }, 655 | { 656 | "name": "Frostplume", 657 | "id": "SnowPeafowl" 658 | }, 659 | { 660 | "name": "Frostplume (Boss)", 661 | "id": "BOSS_SnowPeafowl" 662 | }, 663 | { 664 | "name": "Fuack", 665 | "id": "BluePlatypus" 666 | }, 667 | { 668 | "name": "Fuack (Boss)", 669 | "id": "BOSS_BluePlatypus" 670 | }, 671 | { 672 | "name": "Fuack Ignis", 673 | "id": "BluePlatypus_Fire" 674 | }, 675 | { 676 | "name": "Fuddler", 677 | "id": "CuteMole" 678 | }, 679 | { 680 | "name": "Fuddler (Boss)", 681 | "id": "BOSS_CuteMole" 682 | }, 683 | { 684 | "name": "Galeclaw", 685 | "id": "Eagle" 686 | }, 687 | { 688 | "name": "Galeclaw (Boss)", 689 | "id": "BOSS_Eagle" 690 | }, 691 | { 692 | "name": "Ghangler", 693 | "id": "GhostAnglerfish" 694 | }, 695 | { 696 | "name": "Ghangler (Boss)", 697 | "id": "BOSS_GhostAnglerfish" 698 | }, 699 | { 700 | "name": "Ghangler Ignis", 701 | "id": "GhostAnglerfish_Fire" 702 | }, 703 | { 704 | "name": "Gildane", 705 | "id": "GoldenHorse" 706 | }, 707 | { 708 | "name": "Gildane (Boss)", 709 | "id": "BOSS_GoldenHorse" 710 | }, 711 | { 712 | "name": "Gildane (Predator)", 713 | "id": "PREDATOR_GoldenHorse" 714 | }, 715 | { 716 | "name": "Gloopie", 717 | "id": "OctopusGirl" 718 | }, 719 | { 720 | "name": "Gloopie (Boss)", 721 | "id": "BOSS_OctopusGirl" 722 | }, 723 | { 724 | "name": "Gobfin", 725 | "id": "SharkKid" 726 | }, 727 | { 728 | "name": "Gobfin (Boss)", 729 | "id": "BOSS_SharkKid" 730 | }, 731 | { 732 | "name": "Gobfin Ignis", 733 | "id": "SharkKid_Fire" 734 | }, 735 | { 736 | "name": "Gobfin Ignis (Boss)", 737 | "id": "BOSS_SharkKid_Fire" 738 | }, 739 | { 740 | "name": "Gorirat", 741 | "id": "Gorilla" 742 | }, 743 | { 744 | "name": "Gorirat (Boss)", 745 | "id": "BOSS_Gorilla" 746 | }, 747 | { 748 | "name": "Gorirat (Predator)", 749 | "id": "PREDATOR_Gorilla" 750 | }, 751 | { 752 | "name": "Gorirat Terra", 753 | "id": "Gorilla_Ground" 754 | }, 755 | { 756 | "name": "Gorirat Terra (Boss)", 757 | "id": "BOSS_Gorilla_Ground" 758 | }, 759 | { 760 | "name": "Green Slime", 761 | "id": "YakushimaMonster001" 762 | }, 763 | { 764 | "name": "Grintale", 765 | "id": "NaughtyCat" 766 | }, 767 | { 768 | "name": "Grintale (Boss)", 769 | "id": "BOSS_NaughtyCat" 770 | }, 771 | { 772 | "name": "Grizzbolt", 773 | "id": "ElecPanda" 774 | }, 775 | { 776 | "name": "Grizzbolt (Boss)", 777 | "id": "BOSS_ElecPanda" 778 | }, 779 | { 780 | "name": "Grizzbolt (Gym)", 781 | "id": "GYM_ElecPanda" 782 | }, 783 | { 784 | "name": "Grizzbolt (Gym)", 785 | "id": "GYM_ElecPanda_2" 786 | }, 787 | { 788 | "name": "Grizzbolt (Gym)", 789 | "id": "GYM_ElecPanda_Otomo" 790 | }, 791 | { 792 | "name": "Gumoss", 793 | "id": "PlantSlime" 794 | }, 795 | { 796 | "name": "Gumoss", 797 | "id": "PlantSlime_Flower" 798 | }, 799 | { 800 | "name": "Gumoss (Boss)", 801 | "id": "BOSS_PlantSlime" 802 | }, 803 | { 804 | "name": "Gumoss (Boss)", 805 | "id": "BOSS_PlantSlime_Flower" 806 | }, 807 | { 808 | "name": "Hangyu", 809 | "id": "WindChimes" 810 | }, 811 | { 812 | "name": "Hangyu (Boss)", 813 | "id": "BOSS_WindChimes" 814 | }, 815 | { 816 | "name": "Hangyu Cryst", 817 | "id": "WindChimes_Ice" 818 | }, 819 | { 820 | "name": "Hangyu Cryst (Boss)", 821 | "id": "BOSS_WindChimes_Ice" 822 | }, 823 | { 824 | "name": "Helzephyr", 825 | "id": "HadesBird" 826 | }, 827 | { 828 | "name": "Helzephyr (Boss)", 829 | "id": "BOSS_HadesBird" 830 | }, 831 | { 832 | "name": "Helzephyr (Oilrig)", 833 | "id": "HadesBird_Oilrig" 834 | }, 835 | { 836 | "name": "Helzephyr Lux", 837 | "id": "HadesBird_Electric" 838 | }, 839 | { 840 | "name": "Helzephyr Lux (Boss)", 841 | "id": "BOSS_HadesBird_Electric" 842 | }, 843 | { 844 | "name": "Helzephyr Lux (Predator)", 845 | "id": "PREDATOR_HadesBird_Electric" 846 | }, 847 | { 848 | "name": "Herbil", 849 | "id": "LeafMomonga" 850 | }, 851 | { 852 | "name": "Herbil (Boss)", 853 | "id": "BOSS_LeafMomonga" 854 | }, 855 | { 856 | "name": "Hoocrates", 857 | "id": "WizardOwl" 858 | }, 859 | { 860 | "name": "Hoocrates (Boss)", 861 | "id": "BOSS_WizardOwl" 862 | }, 863 | { 864 | "name": "Icelyn", 865 | "id": "IceWitch" 866 | }, 867 | { 868 | "name": "Icelyn (Boss)", 869 | "id": "BOSS_IceWitch" 870 | }, 871 | { 872 | "name": "Illuminant Bat", 873 | "id": "YakushimaMonster003_Purple" 874 | }, 875 | { 876 | "name": "Illuminant Slime", 877 | "id": "YakushimaMonster001_Pink" 878 | }, 879 | { 880 | "name": "Incineram", 881 | "id": "Baphomet" 882 | }, 883 | { 884 | "name": "Incineram (Boss)", 885 | "id": "BOSS_Baphomet" 886 | }, 887 | { 888 | "name": "Incineram Noct", 889 | "id": "Baphomet_Dark" 890 | }, 891 | { 892 | "name": "Incineram Noct (Boss)", 893 | "id": "BOSS_Baphomet_Dark" 894 | }, 895 | { 896 | "name": "Incineram Noct (Oilrig)", 897 | "id": "Baphomet_Dark_Oilrig" 898 | }, 899 | { 900 | "name": "Incineram Noct (Predator)", 901 | "id": "PREDATOR_Baphomet_Dark" 902 | }, 903 | { 904 | "name": "Jelliette", 905 | "id": "JellyfishFairy" 906 | }, 907 | { 908 | "name": "Jelliette (Boss)", 909 | "id": "BOSS_JellyfishFairy" 910 | }, 911 | { 912 | "name": "Jellroy", 913 | "id": "JellyfishGhost" 914 | }, 915 | { 916 | "name": "Jellroy (Boss)", 917 | "id": "BOSS_JellyfishGhost" 918 | }, 919 | { 920 | "name": "Jetragon", 921 | "id": "JetDragon" 922 | }, 923 | { 924 | "name": "Jetragon (Boss)", 925 | "id": "BOSS_JetDragon" 926 | }, 927 | { 928 | "name": "Jolthog", 929 | "id": "Hedgehog" 930 | }, 931 | { 932 | "name": "Jolthog (Boss)", 933 | "id": "BOSS_Hedgehog" 934 | }, 935 | { 936 | "name": "Jolthog Cryst", 937 | "id": "Hedgehog_Ice" 938 | }, 939 | { 940 | "name": "Jolthog Cryst (Boss)", 941 | "id": "BOSS_Hedgehog_Ice" 942 | }, 943 | { 944 | "name": "Jormuntide", 945 | "id": "Umihebi" 946 | }, 947 | { 948 | "name": "Jormuntide (Boss)", 949 | "id": "BOSS_Umihebi" 950 | }, 951 | { 952 | "name": "Jormuntide Ignis", 953 | "id": "Umihebi_Fire" 954 | }, 955 | { 956 | "name": "Jormuntide Ignis (Boss)", 957 | "id": "BOSS_Umihebi_Fire" 958 | }, 959 | { 960 | "name": "Jormuntide Ignis (Predator)", 961 | "id": "PREDATOR_Umihebi_Fire" 962 | }, 963 | { 964 | "name": "Katress", 965 | "id": "CatMage" 966 | }, 967 | { 968 | "name": "Katress (Boss)", 969 | "id": "BOSS_CatMage" 970 | }, 971 | { 972 | "name": "Katress Ignis", 973 | "id": "CatMage_Fire" 974 | }, 975 | { 976 | "name": "Katress Ignis (Boss)", 977 | "id": "BOSS_CatMage_Fire" 978 | }, 979 | { 980 | "name": "Kelpsea", 981 | "id": "Kelpie" 982 | }, 983 | { 984 | "name": "Kelpsea (Boss)", 985 | "id": "BOSS_Kelpie" 986 | }, 987 | { 988 | "name": "Kelpsea Ignis", 989 | "id": "Kelpie_Fire" 990 | }, 991 | { 992 | "name": "Kelpsea Ignis (Boss)", 993 | "id": "BOSS_Kelpie_Fire" 994 | }, 995 | { 996 | "name": "Kikit", 997 | "id": "SmallArmadillo" 998 | }, 999 | { 1000 | "name": "Kikit (Boss)", 1001 | "id": "BOSS_SmallArmadillo" 1002 | }, 1003 | { 1004 | "name": "Killamari", 1005 | "id": "NegativeOctopus" 1006 | }, 1007 | { 1008 | "name": "Killamari (Boss)", 1009 | "id": "BOSS_NegativeOctopus" 1010 | }, 1011 | { 1012 | "name": "Killamari Primo", 1013 | "id": "NegativeOctopus_Neutral" 1014 | }, 1015 | { 1016 | "name": "Kingpaca", 1017 | "id": "KingAlpaca" 1018 | }, 1019 | { 1020 | "name": "Kingpaca (Boss)", 1021 | "id": "BOSS_KingAlpaca" 1022 | }, 1023 | { 1024 | "name": "Kingpaca Cryst", 1025 | "id": "KingAlpaca_Ice" 1026 | }, 1027 | { 1028 | "name": "Kingpaca Cryst (Boss)", 1029 | "id": "BOSS_KingAlpaca_Ice" 1030 | }, 1031 | { 1032 | "name": "Kitsun", 1033 | "id": "AmaterasuWolf" 1034 | }, 1035 | { 1036 | "name": "Kitsun (Boss)", 1037 | "id": "BOSS_AmaterasuWolf" 1038 | }, 1039 | { 1040 | "name": "Kitsun (Predator)", 1041 | "id": "PREDATOR_AmaterasuWolf" 1042 | }, 1043 | { 1044 | "name": "Kitsun Noct", 1045 | "id": "AmaterasuWolf_Dark" 1046 | }, 1047 | { 1048 | "name": "Kitsun Noct (Boss)", 1049 | "id": "BOSS_AmaterasuWolf_Dark" 1050 | }, 1051 | { 1052 | "name": "Knocklem", 1053 | "id": "WingGolem" 1054 | }, 1055 | { 1056 | "name": "Knocklem (Boss)", 1057 | "id": "BOSS_WingGolem" 1058 | }, 1059 | { 1060 | "name": "Knocklem (Oilrig)", 1061 | "id": "WingGolem_Oilrig" 1062 | }, 1063 | { 1064 | "name": "Lamball", 1065 | "id": "SheepBall" 1066 | }, 1067 | { 1068 | "name": "Lamball (Boss)", 1069 | "id": "BOSS_SheepBall" 1070 | }, 1071 | { 1072 | "name": "Lamball (Quest)", 1073 | "id": "Quest_Farmer03_SheepBall" 1074 | }, 1075 | { 1076 | "name": "Leezpunk", 1077 | "id": "LizardMan" 1078 | }, 1079 | { 1080 | "name": "Leezpunk (Boss)", 1081 | "id": "BOSS_LizardMan" 1082 | }, 1083 | { 1084 | "name": "Leezpunk (Oilrig)", 1085 | "id": "LizardMan_Oilrig" 1086 | }, 1087 | { 1088 | "name": "Leezpunk Ignis", 1089 | "id": "LizardMan_Fire" 1090 | }, 1091 | { 1092 | "name": "Leezpunk Ignis (Boss)", 1093 | "id": "BOSS_LizardMan_Fire" 1094 | }, 1095 | { 1096 | "name": "Lifmunk", 1097 | "id": "Carbunclo" 1098 | }, 1099 | { 1100 | "name": "Lifmunk (Boss)", 1101 | "id": "BOSS_Carbunclo" 1102 | }, 1103 | { 1104 | "name": "Loupmoon", 1105 | "id": "Werewolf" 1106 | }, 1107 | { 1108 | "name": "Loupmoon (Boss)", 1109 | "id": "BOSS_Werewolf" 1110 | }, 1111 | { 1112 | "name": "Loupmoon Cryst", 1113 | "id": "Werewolf_Ice" 1114 | }, 1115 | { 1116 | "name": "Loupmoon Cryst (Boss)", 1117 | "id": "BOSS_Werewolf_Ice" 1118 | }, 1119 | { 1120 | "name": "Loupmoon Cryst (Predator)", 1121 | "id": "PREDATOR_Werewolf_Ice" 1122 | }, 1123 | { 1124 | "name": "Lovander", 1125 | "id": "PinkLizard" 1126 | }, 1127 | { 1128 | "name": "Lovander (Boss)", 1129 | "id": "BOSS_PinkLizard" 1130 | }, 1131 | { 1132 | "name": "Lovander (Predator)", 1133 | "id": "PREDATOR_PinkLizard" 1134 | }, 1135 | { 1136 | "name": "Lullu", 1137 | "id": "LeafPrincess" 1138 | }, 1139 | { 1140 | "name": "Lullu (Boss)", 1141 | "id": "BOSS_LeafPrincess" 1142 | }, 1143 | { 1144 | "name": "Lunaris", 1145 | "id": "Mutant" 1146 | }, 1147 | { 1148 | "name": "Lunaris (Boss)", 1149 | "id": "BOSS_Mutant" 1150 | }, 1151 | { 1152 | "name": "Lyleen", 1153 | "id": "LilyQueen" 1154 | }, 1155 | { 1156 | "name": "Lyleen (Boss)", 1157 | "id": "BOSS_LilyQueen" 1158 | }, 1159 | { 1160 | "name": "Lyleen (Gym)", 1161 | "id": "GYM_LilyQueen" 1162 | }, 1163 | { 1164 | "name": "Lyleen (Gym)", 1165 | "id": "GYM_LilyQueen_2" 1166 | }, 1167 | { 1168 | "name": "Lyleen Noct", 1169 | "id": "LilyQueen_Dark" 1170 | }, 1171 | { 1172 | "name": "Lyleen Noct (Boss)", 1173 | "id": "BOSS_LilyQueen_Dark" 1174 | }, 1175 | { 1176 | "name": "Mammorest", 1177 | "id": "GrassMammoth" 1178 | }, 1179 | { 1180 | "name": "Mammorest (Boss)", 1181 | "id": "BOSS_GrassMammoth" 1182 | }, 1183 | { 1184 | "name": "Mammorest Cryst", 1185 | "id": "GrassMammoth_Ice" 1186 | }, 1187 | { 1188 | "name": "Mammorest Cryst (Boss)", 1189 | "id": "BOSS_GrassMammoth_Ice" 1190 | }, 1191 | { 1192 | "name": "Maraith", 1193 | "id": "GhostBeast" 1194 | }, 1195 | { 1196 | "name": "Maraith (Boss)", 1197 | "id": "BOSS_GhostBeast" 1198 | }, 1199 | { 1200 | "name": "Maraith (Predator)", 1201 | "id": "PREDATOR_GhostBeast" 1202 | }, 1203 | { 1204 | "name": "Mau", 1205 | "id": "Bastet" 1206 | }, 1207 | { 1208 | "name": "Mau (Boss)", 1209 | "id": "BOSS_Bastet" 1210 | }, 1211 | { 1212 | "name": "Mau Cryst", 1213 | "id": "Bastet_Ice" 1214 | }, 1215 | { 1216 | "name": "Mau Cryst (Boss)", 1217 | "id": "BOSS_Bastet_Ice" 1218 | }, 1219 | { 1220 | "name": "Melpaca", 1221 | "id": "Alpaca" 1222 | }, 1223 | { 1224 | "name": "Melpaca (Boss)", 1225 | "id": "BOSS_Alpaca" 1226 | }, 1227 | { 1228 | "name": "Menasting", 1229 | "id": "DarkScorpion" 1230 | }, 1231 | { 1232 | "name": "Menasting (Boss)", 1233 | "id": "BOSS_DarkScorpion" 1234 | }, 1235 | { 1236 | "name": "Menasting Terra", 1237 | "id": "DarkScorpion_Ground" 1238 | }, 1239 | { 1240 | "name": "Menasting Terra (Boss)", 1241 | "id": "BOSS_DarkScorpion_Ground" 1242 | }, 1243 | { 1244 | "name": "Mimog", 1245 | "id": "MimicDog" 1246 | }, 1247 | { 1248 | "name": "Mimog (Boss)", 1249 | "id": "BOSS_MimicDog" 1250 | }, 1251 | { 1252 | "name": "Moon Lord (Raid)", 1253 | "id": "RAID_YakushimaBoss002" 1254 | }, 1255 | { 1256 | "name": "Mossanda", 1257 | "id": "GrassPanda" 1258 | }, 1259 | { 1260 | "name": "Mossanda (Boss)", 1261 | "id": "BOSS_GrassPanda" 1262 | }, 1263 | { 1264 | "name": "Mossanda (Predator)", 1265 | "id": "PREDATOR_GrassPanda" 1266 | }, 1267 | { 1268 | "name": "Mossanda Lux", 1269 | "id": "GrassPanda_Electric" 1270 | }, 1271 | { 1272 | "name": "Mossanda Lux (Boss)", 1273 | "id": "BOSS_GrassPanda_Electric" 1274 | }, 1275 | { 1276 | "name": "Mozzarina", 1277 | "id": "CowPal" 1278 | }, 1279 | { 1280 | "name": "Mozzarina (Boss)", 1281 | "id": "BOSS_CowPal" 1282 | }, 1283 | { 1284 | "name": "Munchill", 1285 | "id": "IceCrocodile" 1286 | }, 1287 | { 1288 | "name": "Munchill (Boss)", 1289 | "id": "BOSS_IceCrocodile" 1290 | }, 1291 | { 1292 | "name": "Necromus", 1293 | "id": "BlackCentaur" 1294 | }, 1295 | { 1296 | "name": "Necromus (Boss)", 1297 | "id": "BOSS_BlackCentaur" 1298 | }, 1299 | { 1300 | "name": "Neptilius", 1301 | "id": "PoseidonOrca" 1302 | }, 1303 | { 1304 | "name": "Neptilius (Boss)", 1305 | "id": "BOSS_PoseidonOrca" 1306 | }, 1307 | { 1308 | "name": "Nitemary", 1309 | "id": "GhostRabbit" 1310 | }, 1311 | { 1312 | "name": "Nitemary (Boss)", 1313 | "id": "BOSS_GhostRabbit" 1314 | }, 1315 | { 1316 | "name": "Nitewing", 1317 | "id": "HawkBird" 1318 | }, 1319 | { 1320 | "name": "Nitewing (Boss)", 1321 | "id": "BOSS_HawkBird" 1322 | }, 1323 | { 1324 | "name": "Nox", 1325 | "id": "NightFox" 1326 | }, 1327 | { 1328 | "name": "Nox (Boss)", 1329 | "id": "BOSS_NightFox" 1330 | }, 1331 | { 1332 | "name": "Nyafia", 1333 | "id": "BadCatgirl" 1334 | }, 1335 | { 1336 | "name": "Nyafia (Boss)", 1337 | "id": "BOSS_BadCatgirl" 1338 | }, 1339 | { 1340 | "name": "Omascul", 1341 | "id": "MysteryMask" 1342 | }, 1343 | { 1344 | "name": "Omascul (Boss)", 1345 | "id": "BOSS_MysteryMask" 1346 | }, 1347 | { 1348 | "name": "Omascul (Predator)", 1349 | "id": "PREDATOR_MysteryMask" 1350 | }, 1351 | { 1352 | "name": "Orserk", 1353 | "id": "ThunderDragonMan" 1354 | }, 1355 | { 1356 | "name": "Orserk (Boss)", 1357 | "id": "BOSS_ThunderDragonMan" 1358 | }, 1359 | { 1360 | "name": "Orserk (Gym)", 1361 | "id": "GYM_ThunderDragonMan" 1362 | }, 1363 | { 1364 | "name": "Orserk (Gym)", 1365 | "id": "GYM_ThunderDragonMan_2" 1366 | }, 1367 | { 1368 | "name": "Paladius", 1369 | "id": "SaintCentaur" 1370 | }, 1371 | { 1372 | "name": "Paladius (Boss)", 1373 | "id": "BOSS_SaintCentaur" 1374 | }, 1375 | { 1376 | "name": "Palumba", 1377 | "id": "TropicalOstrich" 1378 | }, 1379 | { 1380 | "name": "Palumba (Boss)", 1381 | "id": "BOSS_TropicalOstrich" 1382 | }, 1383 | { 1384 | "name": "Pengullet", 1385 | "id": "Penguin" 1386 | }, 1387 | { 1388 | "name": "Pengullet (Boss)", 1389 | "id": "BOSS_Penguin" 1390 | }, 1391 | { 1392 | "name": "Pengullet Lux", 1393 | "id": "Penguin_Electric" 1394 | }, 1395 | { 1396 | "name": "Penking", 1397 | "id": "CaptainPenguin" 1398 | }, 1399 | { 1400 | "name": "Penking (Boss)", 1401 | "id": "BOSS_CaptainPenguin" 1402 | }, 1403 | { 1404 | "name": "Penking Lux", 1405 | "id": "CaptainPenguin_Black" 1406 | }, 1407 | { 1408 | "name": "Petallia", 1409 | "id": "FlowerDoll" 1410 | }, 1411 | { 1412 | "name": "Petallia (Boss)", 1413 | "id": "BOSS_FlowerDoll" 1414 | }, 1415 | { 1416 | "name": "Polapup", 1417 | "id": "IceSeal" 1418 | }, 1419 | { 1420 | "name": "Polapup (Boss)", 1421 | "id": "BOSS_IceSeal" 1422 | }, 1423 | { 1424 | "name": "Prixter", 1425 | "id": "ScorpionMan" 1426 | }, 1427 | { 1428 | "name": "Prixter (Boss)", 1429 | "id": "BOSS_ScorpionMan" 1430 | }, 1431 | { 1432 | "name": "Prixter (Predator)", 1433 | "id": "PREDATOR_ScorpionMan" 1434 | }, 1435 | { 1436 | "name": "Prunelia", 1437 | "id": "BlueberryFairy" 1438 | }, 1439 | { 1440 | "name": "Prunelia (Boss)", 1441 | "id": "BOSS_BlueberryFairy" 1442 | }, 1443 | { 1444 | "name": "Purple Slime", 1445 | "id": "YakushimaMonster001_Purple" 1446 | }, 1447 | { 1448 | "name": "Pyrin", 1449 | "id": "FireKirin" 1450 | }, 1451 | { 1452 | "name": "Pyrin (Boss)", 1453 | "id": "BOSS_FireKirin" 1454 | }, 1455 | { 1456 | "name": "Pyrin Noct", 1457 | "id": "FireKirin_Dark" 1458 | }, 1459 | { 1460 | "name": "Pyrin Noct (Boss)", 1461 | "id": "BOSS_FireKirin_Dark" 1462 | }, 1463 | { 1464 | "name": "Quivern", 1465 | "id": "SkyDragon" 1466 | }, 1467 | { 1468 | "name": "Quivern (Boss)", 1469 | "id": "BOSS_SkyDragon" 1470 | }, 1471 | { 1472 | "name": "Quivern Botan", 1473 | "id": "SkyDragon_Grass" 1474 | }, 1475 | { 1476 | "name": "Quivern Botan (Boss)", 1477 | "id": "BOSS_SkyDragon_Grass" 1478 | }, 1479 | { 1480 | "name": "Ragnahawk", 1481 | "id": "RedArmorBird" 1482 | }, 1483 | { 1484 | "name": "Ragnahawk (Boss)", 1485 | "id": "BOSS_RedArmorBird" 1486 | }, 1487 | { 1488 | "name": "Ragnahawk (Predator)", 1489 | "id": "PREDATOR_RedArmorBird" 1490 | }, 1491 | { 1492 | "name": "Rainbow Slime", 1493 | "id": "YakushimaMonster001_Rainbow" 1494 | }, 1495 | { 1496 | "name": "Rayhound", 1497 | "id": "ThunderDog" 1498 | }, 1499 | { 1500 | "name": "Rayhound (Boss)", 1501 | "id": "BOSS_ThunderDog" 1502 | }, 1503 | { 1504 | "name": "Rayhound (Predator)", 1505 | "id": "PREDATOR_ThunderDog" 1506 | }, 1507 | { 1508 | "name": "Red Slime", 1509 | "id": "YakushimaMonster001_Red" 1510 | }, 1511 | { 1512 | "name": "Reindrix", 1513 | "id": "IceDeer" 1514 | }, 1515 | { 1516 | "name": "Reindrix (Boss)", 1517 | "id": "BOSS_IceDeer" 1518 | }, 1519 | { 1520 | "name": "Relaxaurus", 1521 | "id": "LazyDragon" 1522 | }, 1523 | { 1524 | "name": "Relaxaurus (Boss)", 1525 | "id": "BOSS_LazyDragon" 1526 | }, 1527 | { 1528 | "name": "Relaxaurus (Predator)", 1529 | "id": "PREDATOR_LazyDragon" 1530 | }, 1531 | { 1532 | "name": "Relaxaurus Lux", 1533 | "id": "LazyDragon_Electric" 1534 | }, 1535 | { 1536 | "name": "Relaxaurus Lux (Boss)", 1537 | "id": "BOSS_LazyDragon_Electric" 1538 | }, 1539 | { 1540 | "name": "Reptyro", 1541 | "id": "VolcanicMonster" 1542 | }, 1543 | { 1544 | "name": "Reptyro (Boss)", 1545 | "id": "BOSS_VolcanicMonster" 1546 | }, 1547 | { 1548 | "name": "Reptyro Cryst", 1549 | "id": "VolcanicMonster_Ice" 1550 | }, 1551 | { 1552 | "name": "Reptyro Cryst (Boss)", 1553 | "id": "BOSS_VolcanicMonster_Ice" 1554 | }, 1555 | { 1556 | "name": "Reptyro Cryst (Predator)", 1557 | "id": "PREDATOR_VolcanicMonster_Ice" 1558 | }, 1559 | { 1560 | "name": "Ribbuny", 1561 | "id": "PinkRabbit" 1562 | }, 1563 | { 1564 | "name": "Ribbuny (Boss)", 1565 | "id": "BOSS_PinkRabbit" 1566 | }, 1567 | { 1568 | "name": "Ribbuny Botan", 1569 | "id": "PinkRabbit_Grass" 1570 | }, 1571 | { 1572 | "name": "Ribbuny Botan (Boss)", 1573 | "id": "BOSS_PinkRabbit_Grass" 1574 | }, 1575 | { 1576 | "name": "Robinquill", 1577 | "id": "RobinHood" 1578 | }, 1579 | { 1580 | "name": "Robinquill (Boss)", 1581 | "id": "BOSS_RobinHood" 1582 | }, 1583 | { 1584 | "name": "Robinquill Terra", 1585 | "id": "RobinHood_Ground" 1586 | }, 1587 | { 1588 | "name": "Robinquill Terra (Boss)", 1589 | "id": "BOSS_RobinHood_Ground" 1590 | }, 1591 | { 1592 | "name": "Rooby", 1593 | "id": "FlameBambi" 1594 | }, 1595 | { 1596 | "name": "Rooby (Boss)", 1597 | "id": "BOSS_FlameBambi" 1598 | }, 1599 | { 1600 | "name": "Rushoar", 1601 | "id": "Boar" 1602 | }, 1603 | { 1604 | "name": "Rushoar (Boss)", 1605 | "id": "BOSS_Boar" 1606 | }, 1607 | { 1608 | "name": "Selyne", 1609 | "id": "MoonQueen" 1610 | }, 1611 | { 1612 | "name": "Selyne (Boss)", 1613 | "id": "BOSS_MoonQueen" 1614 | }, 1615 | { 1616 | "name": "Selyne (Gym)", 1617 | "id": "GYM_MoonQueen" 1618 | }, 1619 | { 1620 | "name": "Selyne (Gym)", 1621 | "id": "GYM_MoonQueen_2" 1622 | }, 1623 | { 1624 | "name": "Selyne (Gym)", 1625 | "id": "GYM_MoonQueen_2_Servant" 1626 | }, 1627 | { 1628 | "name": "Shadowbeak", 1629 | "id": "BlackGriffon" 1630 | }, 1631 | { 1632 | "name": "Shadowbeak (Boss)", 1633 | "id": "BOSS_BlackGriffon" 1634 | }, 1635 | { 1636 | "name": "Shadowbeak (Gym)", 1637 | "id": "GYM_BlackGriffon" 1638 | }, 1639 | { 1640 | "name": "Shadowbeak (Gym)", 1641 | "id": "GYM_BlackGriffon_2" 1642 | }, 1643 | { 1644 | "name": "Shadowbeak (Gym)", 1645 | "id": "GYM_BlackGriffon_2_Avatar" 1646 | }, 1647 | { 1648 | "name": "Shroomer", 1649 | "id": "MushroomDragon" 1650 | }, 1651 | { 1652 | "name": "Shroomer (Boss)", 1653 | "id": "BOSS_MushroomDragon" 1654 | }, 1655 | { 1656 | "name": "Shroomer (Predator)", 1657 | "id": "PREDATOR_MushroomDragon" 1658 | }, 1659 | { 1660 | "name": "Shroomer Noct", 1661 | "id": "MushroomDragon_Dark" 1662 | }, 1663 | { 1664 | "name": "Shroomer Noct (Boss)", 1665 | "id": "BOSS_MushroomDragon_Dark" 1666 | }, 1667 | { 1668 | "name": "Sibelyx", 1669 | "id": "WhiteMoth" 1670 | }, 1671 | { 1672 | "name": "Sibelyx (Boss)", 1673 | "id": "BOSS_WhiteMoth" 1674 | }, 1675 | { 1676 | "name": "Silvegis", 1677 | "id": "WhiteShieldDragon" 1678 | }, 1679 | { 1680 | "name": "Silvegis (Boss)", 1681 | "id": "BOSS_WhiteShieldDragon" 1682 | }, 1683 | { 1684 | "name": "Smokie", 1685 | "id": "BlackPuppy" 1686 | }, 1687 | { 1688 | "name": "Smokie (Boss)", 1689 | "id": "BOSS_BlackPuppy" 1690 | }, 1691 | { 1692 | "name": "Sootseer", 1693 | "id": "CandleGhost" 1694 | }, 1695 | { 1696 | "name": "Sootseer (Boss)", 1697 | "id": "BOSS_CandleGhost" 1698 | }, 1699 | { 1700 | "name": "Sootseer (Predator)", 1701 | "id": "PREDATOR_CandleGhost" 1702 | }, 1703 | { 1704 | "name": "Sparkit", 1705 | "id": "ElecCat" 1706 | }, 1707 | { 1708 | "name": "Sparkit (Boss)", 1709 | "id": "BOSS_ElecCat" 1710 | }, 1711 | { 1712 | "name": "Splatterina", 1713 | "id": "GrimGirl" 1714 | }, 1715 | { 1716 | "name": "Splatterina (Boss)", 1717 | "id": "BOSS_GrimGirl" 1718 | }, 1719 | { 1720 | "name": "Splatterina (Predator)", 1721 | "id": "PREDATOR_GrimGirl" 1722 | }, 1723 | { 1724 | "name": "Starryon", 1725 | "id": "NightBlueHorse" 1726 | }, 1727 | { 1728 | "name": "Starryon (Boss)", 1729 | "id": "BOSS_NightBlueHorse" 1730 | }, 1731 | { 1732 | "name": "Starryon (Predator)", 1733 | "id": "PREDATOR_NightBlueHorse" 1734 | }, 1735 | { 1736 | "name": "Surfent", 1737 | "id": "Serpent" 1738 | }, 1739 | { 1740 | "name": "Surfent (Boss)", 1741 | "id": "BOSS_Serpent" 1742 | }, 1743 | { 1744 | "name": "Surfent Terra", 1745 | "id": "Serpent_Ground" 1746 | }, 1747 | { 1748 | "name": "Surfent Terra (Boss)", 1749 | "id": "BOSS_Serpent_Ground" 1750 | }, 1751 | { 1752 | "name": "Suzaku", 1753 | "id": "Suzaku" 1754 | }, 1755 | { 1756 | "name": "Suzaku (Boss)", 1757 | "id": "BOSS_Suzaku" 1758 | }, 1759 | { 1760 | "name": "Suzaku Aqua", 1761 | "id": "Suzaku_Water" 1762 | }, 1763 | { 1764 | "name": "Suzaku Aqua (Boss)", 1765 | "id": "BOSS_Suzaku_Water" 1766 | }, 1767 | { 1768 | "name": "Swee", 1769 | "id": "MopBaby" 1770 | }, 1771 | { 1772 | "name": "Swee (Boss)", 1773 | "id": "BOSS_MopBaby" 1774 | }, 1775 | { 1776 | "name": "Sweepa", 1777 | "id": "MopKing" 1778 | }, 1779 | { 1780 | "name": "Sweepa (Boss)", 1781 | "id": "BOSS_MopKing" 1782 | }, 1783 | { 1784 | "name": "Tanzee", 1785 | "id": "Monkey" 1786 | }, 1787 | { 1788 | "name": "Tanzee (Boss)", 1789 | "id": "BOSS_Monkey" 1790 | }, 1791 | { 1792 | "name": "Tarantriss", 1793 | "id": "PurpleSpider" 1794 | }, 1795 | { 1796 | "name": "Tarantriss (Boss)", 1797 | "id": "BOSS_PurpleSpider" 1798 | }, 1799 | { 1800 | "name": "Tarantriss (Predator)", 1801 | "id": "PREDATOR_PurpleSpider" 1802 | }, 1803 | { 1804 | "name": "Teafant", 1805 | "id": "Ganesha" 1806 | }, 1807 | { 1808 | "name": "Teafant (Boss)", 1809 | "id": "BOSS_Ganesha" 1810 | }, 1811 | { 1812 | "name": "Tocotoco", 1813 | "id": "ColorfulBird" 1814 | }, 1815 | { 1816 | "name": "Tocotoco (Boss)", 1817 | "id": "BOSS_ColorfulBird" 1818 | }, 1819 | { 1820 | "name": "Tombat", 1821 | "id": "CatBat" 1822 | }, 1823 | { 1824 | "name": "Tombat (Boss)", 1825 | "id": "BOSS_CatBat" 1826 | }, 1827 | { 1828 | "name": "True Eye of Cthulhu (Raid)", 1829 | "id": "RAID_YakushimaBoss001_Green" 1830 | }, 1831 | { 1832 | "name": "Turtacle", 1833 | "id": "TentacleTurtle" 1834 | }, 1835 | { 1836 | "name": "Turtacle (Boss)", 1837 | "id": "BOSS_TentacleTurtle" 1838 | }, 1839 | { 1840 | "name": "Turtacle Terra", 1841 | "id": "TentacleTurtle_Ground" 1842 | }, 1843 | { 1844 | "name": "Univolt", 1845 | "id": "Kirin" 1846 | }, 1847 | { 1848 | "name": "Univolt (Boss)", 1849 | "id": "BOSS_Kirin" 1850 | }, 1851 | { 1852 | "name": "Vaelet", 1853 | "id": "VioletFairy" 1854 | }, 1855 | { 1856 | "name": "Vaelet (Boss)", 1857 | "id": "BOSS_VioletFairy" 1858 | }, 1859 | { 1860 | "name": "Vanwyrm", 1861 | "id": "BirdDragon" 1862 | }, 1863 | { 1864 | "name": "Vanwyrm (Boss)", 1865 | "id": "BOSS_BirdDragon" 1866 | }, 1867 | { 1868 | "name": "Vanwyrm (Predator)", 1869 | "id": "PREDATOR_BirdDragon" 1870 | }, 1871 | { 1872 | "name": "Vanwyrm Cryst", 1873 | "id": "BirdDragon_Ice" 1874 | }, 1875 | { 1876 | "name": "Vanwyrm Cryst (Boss)", 1877 | "id": "BOSS_BirdDragon_Ice" 1878 | }, 1879 | { 1880 | "name": "Verdash", 1881 | "id": "GrassRabbitMan" 1882 | }, 1883 | { 1884 | "name": "Verdash (Boss)", 1885 | "id": "BOSS_GrassRabbitMan" 1886 | }, 1887 | { 1888 | "name": "Vixy", 1889 | "id": "CuteFox" 1890 | }, 1891 | { 1892 | "name": "Vixy (Boss)", 1893 | "id": "BOSS_CuteFox" 1894 | }, 1895 | { 1896 | "name": "Warsect", 1897 | "id": "HerculesBeetle" 1898 | }, 1899 | { 1900 | "name": "Warsect (Boss)", 1901 | "id": "BOSS_HerculesBeetle" 1902 | }, 1903 | { 1904 | "name": "Warsect Terra", 1905 | "id": "HerculesBeetle_Ground" 1906 | }, 1907 | { 1908 | "name": "Warsect Terra (Boss)", 1909 | "id": "BOSS_HerculesBeetle_Ground" 1910 | }, 1911 | { 1912 | "name": "Whalaska", 1913 | "id": "IceNarwhal" 1914 | }, 1915 | { 1916 | "name": "Whalaska (Boss)", 1917 | "id": "BOSS_IceNarwhal" 1918 | }, 1919 | { 1920 | "name": "Whalaska Ignis", 1921 | "id": "IceNarwhal_Fire" 1922 | }, 1923 | { 1924 | "name": "Wixen", 1925 | "id": "FoxMage" 1926 | }, 1927 | { 1928 | "name": "Wixen (Boss)", 1929 | "id": "BOSS_FoxMage" 1930 | }, 1931 | { 1932 | "name": "Wixen Noct", 1933 | "id": "FoxMage_Dark" 1934 | }, 1935 | { 1936 | "name": "Wixen Noct (Boss)", 1937 | "id": "BOSS_FoxMage_Dark" 1938 | }, 1939 | { 1940 | "name": "Woolipop", 1941 | "id": "SweetsSheep" 1942 | }, 1943 | { 1944 | "name": "Woolipop (Boss)", 1945 | "id": "BOSS_SweetsSheep" 1946 | }, 1947 | { 1948 | "name": "Wumpo", 1949 | "id": "Yeti" 1950 | }, 1951 | { 1952 | "name": "Wumpo (Boss)", 1953 | "id": "BOSS_Yeti" 1954 | }, 1955 | { 1956 | "name": "Wumpo (Predator)", 1957 | "id": "PREDATOR_Yeti" 1958 | }, 1959 | { 1960 | "name": "Wumpo Botan", 1961 | "id": "Yeti_Grass" 1962 | }, 1963 | { 1964 | "name": "Wumpo Botan (Boss)", 1965 | "id": "BOSS_Yeti_Grass" 1966 | }, 1967 | { 1968 | "name": "Xenogard", 1969 | "id": "WhiteAlienDragon" 1970 | }, 1971 | { 1972 | "name": "Xenogard (Boss)", 1973 | "id": "BOSS_WhiteAlienDragon" 1974 | }, 1975 | { 1976 | "name": "Xenogard (Summon)", 1977 | "id": "SUMMON_WhiteAlienDragon" 1978 | }, 1979 | { 1980 | "name": "Xenogard (Summon)", 1981 | "id": "SUMMON_WhiteAlienDragon_MAX" 1982 | }, 1983 | { 1984 | "name": "Xenolord", 1985 | "id": "DarkMechaDragon" 1986 | }, 1987 | { 1988 | "name": "Xenolord (Boss)", 1989 | "id": "BOSS_DarkMechaDragon" 1990 | }, 1991 | { 1992 | "name": "Xenolord (Raid)", 1993 | "id": "RAID_DarkMechaDragon" 1994 | }, 1995 | { 1996 | "name": "Xenolord (Raid)", 1997 | "id": "RAID_DarkMechaDragon_2" 1998 | }, 1999 | { 2000 | "name": "Xenovader", 2001 | "id": "DarkAlien" 2002 | }, 2003 | { 2004 | "name": "Xenovader (Boss)", 2005 | "id": "BOSS_DarkAlien" 2006 | }, 2007 | { 2008 | "name": "Xenovader (Oilrig)", 2009 | "id": "DarkAlien_Oilrig" 2010 | }, 2011 | { 2012 | "name": "Xenovader (Summon)", 2013 | "id": "SUMMON_DarkAlien" 2014 | }, 2015 | { 2016 | "name": "Xenovader (Summon)", 2017 | "id": "SUMMON_DarkAlien_MAX" 2018 | }, 2019 | { 2020 | "name": "Yakumo", 2021 | "id": "GuardianDog" 2022 | }, 2023 | { 2024 | "name": "Yakumo (Boss)", 2025 | "id": "BOSS_GuardianDog" 2026 | } 2027 | ] 2028 | } --------------------------------------------------------------------------------