From 04c5db22322d617b5c3e585f8bcd627844afcd27 Mon Sep 17 00:00:00 2001
From: DerGrumpf
Date: Wed, 9 Oct 2024 18:29:51 +0200
Subject: [PATCH] Added Docker Support
---
bot/Dockerfile | 2 +-
bot/bot.py | 9 +-
bot/cogs/minecraft.py | 344 ++++++++++++++++++++++++++----------------
bot/cogs/spawner.py | 242 +++++++++++++++++++++++++++++
bot/compose.yml | 19 ++-
bot/mods.txt | 58 +++++++
bot/requirements.txt | 26 +++-
bot/spawner.py | 7 +
postgres/compose.yml | 9 +-
9 files changed, 554 insertions(+), 162 deletions(-)
create mode 100644 bot/cogs/spawner.py
create mode 100644 bot/mods.txt
create mode 100644 bot/spawner.py
diff --git a/bot/Dockerfile b/bot/Dockerfile
index c21f683..5877c8f 100644
--- a/bot/Dockerfile
+++ b/bot/Dockerfile
@@ -5,4 +5,4 @@ RUN pip install --upgrade pip
RUN pip install -r requirements.txt
ADD . .
-CMD ["python", "main.py"]
+CMD ["python", "bot.py"]
diff --git a/bot/bot.py b/bot/bot.py
index c929153..e374aaf 100644
--- a/bot/bot.py
+++ b/bot/bot.py
@@ -3,6 +3,7 @@ from discord.ext import commands
from cogs.minecraft import Minecraft
from cogs.user_management import UserManager
+from cogs.spawner import Spawner
# Setup Environment
import os
@@ -12,11 +13,6 @@ load_dotenv()
# Discord Stuff
TOKEN = os.environ['TOKEN']
-# Server Stuff
-RCON_SERVER = os.environ['RCON_SERVER']
-RCON_PASS = os.environ['RCON_PASS']
-RCON_PORT = int(os.environ['RCON_PORT'])
-
# Setup Basic Permission
intents = discord.Intents.default()
intents.message_content = True
@@ -25,8 +21,9 @@ intents.message_content = True
bot = commands.Bot(command_prefix='-', intents=intents)
async def setup():
- await bot.add_cog(Minecraft(bot, RCON_SERVER, RCON_PASS, RCON_PORT))
+ await bot.add_cog(Minecraft(bot))
await bot.add_cog(UserManager(bot))
+ await bot.add_cog(Spawner(bot))
@bot.event
async def on_ready():
diff --git a/bot/cogs/minecraft.py b/bot/cogs/minecraft.py
index db10c72..c0de23f 100644
--- a/bot/cogs/minecraft.py
+++ b/bot/cogs/minecraft.py
@@ -2,79 +2,14 @@ import discord
from discord.ext import commands
from mcrcon import MCRcon
import enum
-from statemachine import StateMachine, State
-from statemachine.states import States
+from transitions import Machine
+from cogs.spawner import containers
-class BorderWarsSession(StateMachine):
- "A workflow maschine for managing InGame States"
- Nothing = State(initial=True)
- Initialization = State()
- Safe = State()
- Fight = State()
- SuddenDeath = State()
- End = State()
-
- init_game = Nothing.to(Initialization)
- start_game = Initialization.to(Safe)
- start_fight = Safe.to(Fight)
- start_last_round = Fight.to(SuddenDeath)
- end_game = Fight.to(End) | SuddenDeath.to(End)
-
- abort = Initialization.to(Nothing) | Safe.to(Nothing) | Fight.to(Nothing) | SuddenDeath.to(Nothing) | End.to(Nothing) | Nothing.to(Nothing)
- reset = End.to(Initialization)
-
- @Initialization.enter
- def initialization(self) -> list:
-
-
- @Safe.enter
- def safe(self) -> list:
- # Chat countdown
- return [
- '''/title @a subtitle ["",{"text":"bei ","color":"blue"},{"text":"BORDER WARS!","bold":true,"color":"red"}]''',
- '''/title @a title {"text":"Viel Glück!","bold":true,"color":"blue"}''',
- "playsound minecraft:entity.wither.spawn ambient @a 0 64 080",
- "/worldborder set 1000",
- "/gamerule keepInventory true"
- ]
-
- @Fight.enter
- def fight(self) -> list:
- # Timer Starten
- return [
- '''/title @a subtitle {"text":"ÜBERLEBEN!","bold":true,"color":"red"}''',
- '''/title @a title {"text":"Möge der beste","color":"blue"}''',
- "/playsound minecraft:item.totem.use ambient @a 0 64 0 80",
- "/worldborder set 75 3600",
- "/gamerule keepInventory false"
- ]
-
- @SuddenDeath.enter
- def death(self) -> list:
- # Timer Starten
- return [
- '''/title @a title ["",{"text":"Sudden ","color":"dark_blue"},{"text":"DEATH!","bold":true,"color":"red"}]''',
- "/playsound minecraft:entity.ender_dragon.growl ambient @a 0 64 0 80",
- "/worldborder set 5 600"
- ]
-
- @End.enter
- def end(self, playername: str) -> list:
- return [
- "/worldborder center 0 0",
- "/worldborder set 75",
- "/gamerule keepInventory true",
- '''/title @a subtitle ["",{"text":"''' + playername + '''","bold":true,"color":"red"},{"text":" gewinnt","color":"dark_blue"}]''',
- '''/title @a title {"text":"ENDE!","color":"dark_blue"}''',
- "/playsound minecraft:entity.ender_dragon_death ambient @a 0 64 0 80",
- ]
-
-
-class Whitelist(StateMachine):
+class Whitelist:
"A workflow machine for managing Whitelist states"
- On = State(initial=True)
- Off = State()
- toggle = On.to(Off) | Off.to(On)
+ On = None
+ Off = None
+ toggle = None
class RCON(MCRcon):
def __init__(self, ip: str, secret: str, port: int = 31066):
@@ -86,50 +21,43 @@ class RCON(MCRcon):
cmds = "/whitelist {}".format(self.whitelist.current_state.id)
print(cmds)
- def _sendcmd(self, cmds: str | list) -> None:
+ def sendcmd(self, cmds) -> None:
if isinstance(cmds, str):
- return self.server.command(str)
+ return self.command(str)
if isinstance(cmds, list):
- return [self.server.commands(cmd) for cmd in cmds]
+ return [self.command(cmd) for cmd in cmds]
def __del__(self):
self.disconnect()
+class States(enum.Enum):
+ NOTHING = 0
+ INIT = 1
+ SAFE = 2
+ FIGHT = 3
+ SUDDENDEATH = 4
+ END = 5
+
class Minecraft(commands.Cog):
- def __init__(self, bot: commands.Bot, ip: str, secret: str, port: int = 31066):
+ def __init__(self, bot: commands.Bot):
self.bot = bot
- self.server = RCON(ip, secret, port)
- self.session = BorderWarsSession()
- self.whitelist = Whitelist()
+ self.servers = dict()
+
+ transitions = [
+ ['init_game', States.NOTHING, States.INIT],
+ ['start_game', States.INIT, States.SAFE],
+ ['start_fight', States.SAFE, States.FIGHT],
+ ['start_last_round', States.FIGHT, States.SUDDENDEATH],
+ ['end_game', States.FIGHT, States.END],
+ ['end_game', States.SUDDENDEATH, States.END],
+ ['reset', States.END, States.INIT],
+ ['abort', '*', States.NOTHING]
+ ]
- @commands.hybrid_command(name='whitelist')
- async def whitelist(self, ctx: commands.Context):
- """
- Toggles Servers Whitelist
-
- Parameters
- ----------
- ctx: commands.Context
- The context of the command invocation
- """
- await self.whitelist.activate_initial_state()
- await ctx.send("Whitelist")
-
- @commands.hybrid_command(name='start')
- async def start(self, ctx: commands.Context):
- """
- Starts a Border Wars Session
-
- Parameters
- ----------
- ctx: commands.Context
- The context of the command invocation
- """
- cmds =
- await ctx.send("Start")
+ self.machine = Machine(states=States, transitions=transitions, initial=States.NOTHING)
@commands.hybrid_command(name='init')
- async def init(self, ctx: commands.Context):
+ async def init(self, ctx: commands.Context, server_name: str):
"""
Initialize a Border Wars session
@@ -137,18 +65,165 @@ class Minecraft(commands.Cog):
----------
ctx: commands.Context
The context of the command invocation
+ server_name: str
+ Server on which the Session should be initialized
"""
+ server_name = server_name.title()
+
+ c = None
+ for container in containers:
+ if server_name == container.name:
+ c = container
+ break
+
+ if not c:
+ await ctx.send("---The server doesn't run---")
+ return
+
+ conn = RCON(str(c.ip), c.rcon_pass, c.rcon_port)
+ self.servers[server_name] = conn
+
cmds = [
+ "/effect give @a minecraft:resistance infinite 255 true",
+ "/effect give @a minecraft:saturation infinite 4 true",
+ "/tp @a 0 200 0",
+ "/gamemode adventure @a",
"/worldborder center 0 0",
"/worldborder set 5",
"/whitelist off"
]
- await self.session.activate_initial_state()
- await self.session.init_game()
- await ctx.send(self.session.current_state)
+
+ conn.sendcmd(cmds)
+ await ctx.send("init Border Wars Game")
+
+ @commands.hybrid_command(name='safe')
+ async def safe(self, ctx: commands.Context, server_name: str):
+ """
+ Switches to Safe Phase on a Border Wars session
+
+ Parameters
+ ----------
+ ctx: commands.Context
+ The context of the command invocation
+ server_name: str
+ Server on which the Session should be initialized
+ """
+ server_name = server_name.title()
+
+ c = None
+ for container in containers:
+ if server_name == container.name:
+ c = container
+ break
+
+ if not c:
+ await ctx.send("---The server doesn't run---")
+ return
+
+ conn = self.servers.get(server_name)
+
+ if not conn:
+ await ctx.send("---Border Wars Session not Initialized---")
+ return
+
+
+ cmds = [
+ '''/title @a subtitle ["",{"text":"bei ","color":"blue"},{"text":"BORDER WARS!","bold":true,"color":"red"}]''',
+ '''/title @a title {"text":"Viel Glück!","bold":true,"color":"blue"}''',
+ "playsound minecraft:entity.wither.spawn ambient @a 0 64 080",
+ "/worldborder set 1000",
+ "/gamerule keepInventory true",
+ "/gamemode survival @a",
+ "/effect clear @a"
+ ]
+
+ conn.sendcmd(cmds)
+ await ctx.send("Switched to Safe Phase")
+
+ @commands.hybrid_command(name='fight')
+ async def fight(self, ctx: commands.Context, server_name: str):
+ """
+ Switches to Fight Phase on a Border Wars session
+
+ Parameters
+ ----------
+ ctx: commands.Context
+ The context of the command invocation
+ server_name: str
+ Server on which the Session should be initialized
+ """
+ server_name = server_name.title()
+
+ c = None
+ for container in containers:
+ if server_name == container.name:
+ c = container
+ break
+
+ if not c:
+ await ctx.send("---The server doesn't run---")
+ return
+
+ conn = self.servers.get(server_name)
+
+ if not conn:
+ await ctx.send("---Border Wars Session not Initialized---")
+ return
+
+
+ cmds = [
+ '''/title @a subtitle {"text":"ÜBERLEBEN!","bold":true,"color":"red"}''',
+ '''/title @a title {"text":"Möge der beste","color":"blue"}''',
+ "/playsound minecraft:item.totem.use ambient @a 0 64 0 80",
+ "/worldborder set 75 3600",
+ "/gamerule keepInventory false"
+ ]
+
+ conn.sendcmd(cmds)
+ await ctx.send("Switched to Fight Phase")
+
+ @commands.hybrid_command(name='death')
+ async def death(self, ctx: commands.Context, server_name: str):
+ """
+ Switches to Sudden Death Phase on a Border Wars session
+
+ Parameters
+ ----------
+ ctx: commands.Context
+ The context of the command invocation
+ server_name: str
+ Server on which the Session should be initialized
+ """
+ server_name = server_name.title()
+
+ c = None
+ for container in containers:
+ if server_name == container.name:
+ c = container
+ break
+
+ if not c:
+ await ctx.send("---The server doesn't run---")
+ return
+
+ conn = self.servers.get(server_name)
+
+ if not conn:
+ await ctx.send("---Border Wars Session not Initialized---")
+ return
+
+
+ cmds = [
+ '''/title @a title ["",{"text":"Sudden ","color":"dark_blue"},{"text":"DEATH!","bold":true,"color":"red"}]''',
+ "/playsound minecraft:entity.ender_dragon.growl ambient @a 0 64 0 80",
+ "/worldborder set 5 600"
+ ]
+
+ conn.sendcmd(cmds)
+ await ctx.send("Switched to Sudden Death Phase")
@commands.hybrid_command(name='end')
- async def end(self, ctx: commands.Context):
+ async def end(self, ctx: commands.Context, server_name: str, playername: str):
"""
Ends a Border Wars session
@@ -156,30 +231,41 @@ class Minecraft(commands.Cog):
----------
ctx: commands.Context
The context of the command invocation
+ server_name: str
+ Server on which the Session should be initialized
+ playername: str
+ Player which is announced as the Winner
"""
- await ctx.send("End")
+ server_name = server_name.title()
- @commands.hybrid_command(name='rules')
- async def rules(self, ctx: commands.Context):
- """
- Displays the Border Wars rules
+ c = None
+ for container in containers:
+ if server_name == container.name:
+ c = container
+ break
+
+ if not c:
+ await ctx.send("---The server doesn't run---")
+ return
+
+ conn = self.servers.get(server_name)
+
+ if not conn:
+ await ctx.send("---Border Wars Session not Initialized---")
+ return
- Parameters
- ----------
- ctx: commands.Context
- The context of the command invocation
- """
- await ctx.send("Rules")
- @commands.hybrid_command(name='custom')
- async def custom(self, ctx: commands.Context):
- """
- Register a custom command
+ cmds = [
+ "/worldborder center 0 0",
+ "/worldborder set 75",
+ "/gamerule keepInventory true",
+ '''/title @a subtitle ["",{"text":"''' + playername + '''","bold":true,"color":"red"},{"text":" gewinnt","color":"dark_blue"}]''',
+ '''/title @a title {"text":"ENDE!","color":"dark_blue"}''',
+ "/playsound minecraft:entity.ender_dragon_death ambient @a 0 64 0 80",
+ ]
- Parameters
- ----------
- ctx: commands.Context
- The context of the command invocation
- """
- await ctx.send("Custom")
+ conn.sendcmd(cmds)
+ await ctx.send("Ended Border Wars Session")
+
+
diff --git a/bot/cogs/spawner.py b/bot/cogs/spawner.py
new file mode 100644
index 0000000..603fe56
--- /dev/null
+++ b/bot/cogs/spawner.py
@@ -0,0 +1,242 @@
+import discord
+from discord.ext import commands
+import docker
+import random
+import socket
+from contextlib import closing
+from datetime import datetime
+import pytz
+from dataclasses import dataclass
+from ipaddress import IPv4Address
+import secrets
+import asyncio
+
+@dataclass
+class Server:
+ container: None
+ name: str
+ ip: IPv4Address
+ port: int
+ players: int
+ rcon_pass: str
+ rcon_port: int
+
+# Global List of all running Containers
+containers = list()
+
+def seed_generator():
+ seed = random.randrange(1_000_000_000, 100_000_000_000_000)
+ if random.randrange(0,2) == 0:
+ seed *= -1
+ return str(seed)
+
+def find_free_port():
+ with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
+ s.bind(('', 0))
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ return s.getsockname()[1]
+
+class Spawner(commands.Cog):
+ def __init__(self, bot: commands.Bot):
+ self.bot = bot
+ self.client = docker.from_env()
+ self.client.images.pull('itzg/minecraft-server:latest')
+
+ @commands.hybrid_command(name='spawn')
+ async def spawn(self,
+ ctx: commands.Context,
+ server_name: str,
+ world_url: str = None,
+ seed: str = None,
+ enable_command_blocks: bool = False,
+ max_players: int = 10,
+ enable_hardcore: bool = False,
+ ):
+ '''
+ Spawns a standard defined Minecraft Server
+ Either from a World Download Link or a Seed
+
+ Parameters
+ ----------
+ ctx: commands.Context
+ The context of the command invocation
+ server_name: str
+ Name of the Server
+ world_url: str
+ Download link of a minecraft world (Should be a downloadable ZIPP Archive
+ seed: str
+ Seed to generate a World from
+ enable_command_blocks: bool
+ Enable or disable command Block
+ max_players: int
+ Maximum Number of Players who can join the Server
+ enable_hardcore: bool
+ Enables Hardcore Minecraft
+ '''
+ embed = discord.Embed(
+ title="Starting Server",
+ description=f'''
+ Setting up {server_name}
+
+ This could take up to **5 minutes**
+ ''',
+ color=discord.Color.random(),
+ timestamp=datetime.now(pytz.timezone('Europe/Berlin'))
+ )
+
+ start = await ctx.send(embed=embed)
+
+ port = find_free_port()
+ server_name = server_name.title()
+ passwd = secrets.token_hex(32)
+ rcon_port = find_free_port()
+
+ env = {
+ "EULA": "true",
+ "TYPE": "FABRIC",
+ "VERSION": "1.21.1",
+ "SERVER_NAME": server_name,
+ "LEVEL": server_name,
+ "ONLINE_MODE": "true",
+ "TZ": "Europe/Berlin",
+ "MOTD": "\u00a7d\u00a7khhh\u00a76Powered by\u00a7b Garde Studios\u00a76!\u00a7d\u00a7khhh",
+
+ "OVERRIDE_SERVER_PROPERTIES": "true",
+ "ENABLE_COMMAND_BLOCK": enable_command_blocks,
+ "GAMEMODE": "survival",
+ "FORCE_GAMEMODE": "true",
+
+ "RCON_PASSWORD": passwd,
+ "RCON_PORT": rcon_port,
+ "BROADCAST_CONSOLE_TO_OPS": "false",
+ "BROADCAST_RCON_TO_OPS": "false",
+ "SERVER_PORT": port,
+
+ "FORCE_REDOWNLOAD": "true",
+ "INIT_MEMORY": "500M",
+ "MAX_MEMORY": "2G",
+ "USE_AIKAR_FLAGS": "true",
+
+ #"MODS_FILE": "/extras/mods.txt",
+ "OPS_FILE": "https://git.cyperpunk.de/Garde-Studios/Uno-MC/raw/branch/main/ops.json",
+ "SYNC_SKIP_NEWER_IN_DESTINATION": "false",
+ "MAX_PLAYERS": max_players,
+
+ "ANNOUNCE_PLAYER_ACHIEVMENTS": "true",
+ "HARDCORE": enable_hardcore,
+
+ "SNOOPER_ENABLED": "false",
+ "SPAWN_PROTECTION": 0,
+ "VIEW_DISTANCE": 12,
+ "ALLOW_FLIGHT": "false",
+
+ # "RESOURCE_PACK": "",
+ # "RESOURCE_PACK_SHA1": "",
+ }
+
+
+ if not seed and not world_url:
+ seed = seed_generator()
+ if seed:
+ env["SEED"] = seed
+ if world_url:
+ env["WORLD"] = world_url
+
+ container = self.client.containers.run(
+ image='itzg/minecraft-server:latest',
+ environment=env,
+ detach=True,
+ hostname=server_name,
+ name=server_name,
+ network_mode='bridge',
+ ports={port:port, rcon_port:rcon_port},
+ restart_policy={"Name": "always"},
+ volumes={'mods.txt': {'bind': '/extras/mods.txt', 'mode': 'ro'}}
+ )
+
+ net = self.client.networks.get('bot_rcon')
+ net.connect(container)
+
+ ip = self.client.containers.get(server_name).attrs['NetworkSettings']['Networks']['bot_rcon']['IPAddress']
+ server = Server(container, server_name, IPv4Address(ip), port, max_players, passwd, rcon_port)
+
+ containers.append(server)
+
+ embed = discord.Embed(
+ title="Success",
+ description=f'''
+ **{server_name}** has started!
+
+ **Connection URL**:
+ garde-studios.de:{port}
+ ''',
+ color=discord.Color.random(),
+ timestamp=datetime.now(pytz.timezone('Europe/Berlin'))
+ )
+ await start.delete()
+ await ctx.send(embed=embed)
+
+ @commands.hybrid_command(name='servers')
+ async def servers(self, ctx: commands.Context):
+ '''
+ List all currently Running Servers
+
+ Parameters
+ ----------
+ ctx: commands.Context
+ The context of the command invocation
+ '''
+
+ embed = discord.Embed(
+ title="Currently Running Servers",
+ description="List of all currently running Minecraft Servers",
+ color=discord.Color.random(),
+ timestamp=datetime.now(pytz.timezone('Europe/Berlin'))
+ )
+
+ for container in containers:
+ desc = f'''
+ *Status*: {container.container.status}
+ *URL*: garde-studios.de:{container.port}
+ '''
+ embed.add_field(name=f'{container.name} 0/{container.players}', value=desc)
+ await ctx.send(embed=embed)
+
+ @commands.hybrid_command(name='kill')
+ async def kill(self, ctx: commands.Context, server_name: str):
+ '''
+ Kill & remove currently Running Servers
+
+ Parameters
+ ----------
+ ctx: commands.Context
+ The context of the command invocation
+ server_name: str
+ Name of the server that should be removed
+ '''
+
+ server_name = server_name.title()
+
+ # Check if Server exist
+ rm = str()
+ conn = None
+ for container in containers:
+ if container.name == server_name:
+ rm = server_name
+ conn = container
+ break
+
+ if not rm:
+ await ctx.send("---Server not found---")
+ return
+
+ conn.container.remove(force=True)
+ #self.client.volumes.get(server_name).remove()
+ containers.remove(conn)
+
+ await ctx.send(f"Server {server_name} killed successfully")
+
+if __name__ == '__main__':
+ for _ in range(10):
+ print("|", seed_generator(), "|")
+ print("Port:", find_free_port())
diff --git a/bot/compose.yml b/bot/compose.yml
index b9b088e..96be4da 100644
--- a/bot/compose.yml
+++ b/bot/compose.yml
@@ -1,16 +1,15 @@
services:
bot:
+ container_name: bot
build:
context: .
dockerfile: Dockerfile
- # volumes:
- # - bot_data:/home
+ volumes:
+ - ./bot_data:/home
+ - /var/run/docker.sock:/var/run/docker.sock
+ networks:
+ - rcon
-
-volumes:
- bot_data:
- driver: local
- driver_opts:
- type: none
- device: ./data
- o: bind
+
+networks:
+ rcon: {}
diff --git a/bot/mods.txt b/bot/mods.txt
new file mode 100644
index 0000000..ab29fc9
--- /dev/null
+++ b/bot/mods.txt
@@ -0,0 +1,58 @@
+# Fabric API
+https://cdn.modrinth.com/data/P7dR8mSH/versions/bK6OgzFj/fabric-api-0.102.1%2B1.21.1.jar
+
+# Cloth Config API
+https://cdn.modrinth.com/data/9s6osm5g/versions/7jtvrmVP/cloth-config-15.0.130-fabric.jar
+
+# Moonlight Lib
+#https://cdn.modrinth.com/data/twkfQtEc/versions/tP7HsFBI/moonlight-1.21-2.14.12-fabric.jar
+
+# Yungs API
+https://cdn.modrinth.com/data/Ua7DFN59/versions/Nx7XHO30/YungsApi-1.21-Fabric-5.0.0.jar
+
+# Performance
+https://cdn.modrinth.com/data/gvQqBUqZ/versions/5szYtenV/lithium-fabric-mc1.21.1-0.13.0.jar
+https://cdn.modrinth.com/data/fALzjamp/versions/dPliWter/Chunky-1.4.16.jar
+https://cdn.modrinth.com/data/s86X568j/versions/uT1cdd3k/ChunkyBorder-1.2.18.jar
+https://cdn.modrinth.com/data/LFJf0Klb/versions/7e8Rxgsk/ce-2.1.1.jar
+
+# Proxy
+#https://cdn.modrinth.com/data/8dI2tmqs/versions/AQhF7kvw/FabricProxy-Lite-2.9.0.jar
+
+# Monitoring
+#https://cdn.modrinth.com/data/dbVXHSlv/versions/YcE9H1C5/fabricexporter-1.0.11.jar
+#https://cdn.modrinth.com/data/l6YH9Als/versions/qTSaozEL/spark-1.10.97-fabric.jar
+
+# World Edit
+https://cdn.modrinth.com/data/1u6JkXh5/versions/vBzkrSYP/worldedit-mod-7.3.6.jar
+
+# Dynmap
+# https://cdn.modrinth.com/data/fRQREgAc/versions/ipBhc6VW/Dynmap-3.7-beta-6-fabric-1.21.jar
+
+# World Guard
+https://cdn.modrinth.com/data/py6EMmAJ/versions/xpvSS4oW/yawp-0.0.2.10-alpha2.jar
+https://cdn.modrinth.com/data/ohNO6lps/versions/gtorYSGm/ForgeConfigAPIPort-v21.1.0-1.21.1-Fabric.jar
+
+# Permission Management
+#https://cdn.modrinth.com/data/Vebnzrzj/versions/oLykW1F8/LuckPerms-Fabric-5.4.139.jar
+
+# Custom
+#https://cdn.modrinth.com/data/HjmxVlSr/versions/2Z4xpeH5/YungsBetterMineshafts-1.20.4-Fabric-4.4.0.jar
+#https://cdn.modrinth.com/data/o1C1Dkj5/versions/4RpKnxDR/YungsBetterDungeons-1.20.4-Fabric-4.4.0.jar
+#https://cdn.modrinth.com/data/3dT9sgt4/versions/V46v23Uz/YungsBetterOceanMonuments-1.20.4-Fabric-3.4.0.jar
+#https://cdn.modrinth.com/data/kidLKymU/versions/Y05JQWx3/YungsBetterStrongholds-1.20.4-Fabric-4.4.0.jar
+#https://cdn.modrinth.com/data/Z2mXHnxP/versions/QplnGAIz/YungsBetterNetherFortresses-1.20.4-Fabric-2.4.0.jar
+#https://cdn.modrinth.com/data/t5FRdP87/versions/3CEVoaSN/YungsBetterWitchHuts-1.20.4-Fabric-3.4.0.jar
+#https://cdn.modrinth.com/data/XNlO7sBv/versions/Rnvv7pHS/YungsBetterDesertTemples-1.20.4-Fabric-3.4.0.jar
+#https://cdn.modrinth.com/data/Ht4BfYp6/versions/tx2e5Fjp/YungsBridges-1.20.4-Fabric-4.4.0.jar
+#https://cdn.modrinth.com/data/ZYgyPyfq/versions/F1adMKW8/YungsExtras-1.20.4-Fabric-4.4.0.jar
+#https://cdn.modrinth.com/data/z9Ve58Ih/versions/DIG3Vtjv/YungsBetterJungleTemples-1.20.4-Fabric-2.4.0.jar
+#https://cdn.modrinth.com/data/2BwBOmBQ/versions/mRCm0pL5/YungsBetterEndIsland-1.20.4-Fabric-2.4.0.jar
+https://cdn.modrinth.com/data/klXONLDA/versions/FQrJz3KA/villagesandpillages-fabric-mc1.21.1-1.0.1.jar
+https://cdn.modrinth.com/data/tpehi7ww/versions/QmeQn0Mp/dungeons-and-taverns-v4.3.jar
+#https://cdn.modrinth.com/data/fgmhI8kH/versions/5D3oDlLM/%5BFabric%5DCTOV-3.5.1.jar
+
+#https://cdn.modrinth.com/data/7tKn1fLd/versions/65K7sgmM/MobCaptains-v3.2.1.jar
+
+https://cdn.modrinth.com/data/cnIatHrN/versions/BfXSBkjs/universal_shops-1.7.1%2B1.21.jar
+
diff --git a/bot/requirements.txt b/bot/requirements.txt
index c7b829a..a6dbfe8 100644
--- a/bot/requirements.txt
+++ b/bot/requirements.txt
@@ -1,14 +1,24 @@
-aiohttp==3.9.5
+aiohappyeyeballs==2.4.0
+aiohttp==3.10.5
aiosignal==1.3.1
-attrs==23.2.0
+async-timeout==4.0.3
+attrs==24.2.0
+certifi==2024.8.30
+charset-normalizer==3.3.2
+discord==2.3.2
discord.py==2.4.0
+docker==7.1.0
frozenlist==1.4.1
-greenlet==3.0.3
-idna==3.7
+greenlet==3.1.1
+idna==3.10
mcrcon==0.7.0
-multidict==6.0.5
+multidict==6.1.0
python-dotenv==1.0.1
-python-statemachine==2.3.1
-SQLAlchemy==2.0.31
+pytz==2024.2
+requests==2.32.3
+six==1.16.0
+SQLAlchemy==2.0.35
+transitions==0.9.2
typing_extensions==4.12.2
-yarl==1.9.4
+urllib3==2.2.3
+yarl==1.12.1
diff --git a/bot/spawner.py b/bot/spawner.py
new file mode 100644
index 0000000..b975b4c
--- /dev/null
+++ b/bot/spawner.py
@@ -0,0 +1,7 @@
+import docker
+
+client = docker.from_env()
+
+print(client.containers.run("itzg/minecraft-server", detach=True))
+
+
diff --git a/postgres/compose.yml b/postgres/compose.yml
index 0819cb9..94651ef 100644
--- a/postgres/compose.yml
+++ b/postgres/compose.yml
@@ -7,12 +7,5 @@ services:
ports:
- "5432:5432"
volumes:
- - db_data:/var/lib/postgresql/data
+ - ./db_data:/var/lib/postgresql/data
-volumes:
- db_data:
- driver: local
- driver_opts:
- type: none
- device: ./data
- o: bind