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())