From 839bb7de43f7e431268bf0f454a7e5a75bb342a3 Mon Sep 17 00:00:00 2001 From: Peritia Date: Fri, 26 Sep 2025 11:42:25 +0200 Subject: [PATCH] release: yet another Nix Minecraft Module --- README.md | 119 ++++- example/full/configuration.nix | 112 +++++ example/full/flake.nix | 34 ++ example/small/configuration.nix | 58 +++ example/small/flake.nix | 34 ++ flake.nix | 23 + .../minecraft-template-backup-routine.sh | 420 ++++++++++++++++ .../Scripts/minecraft-template-backup.sh | 134 +++++ minecraft/Scripts/minecraft-template-query.sh | 47 ++ minecraft/Scripts/minecraft-template-rcon.sh | 47 ++ minecraft/Scripts/minecraft-template-say.sh | 109 ++++ minecraft/default.nix | 11 + minecraft/minecraft.nix | 474 ++++++++++++++++++ 13 files changed, 1621 insertions(+), 1 deletion(-) create mode 100644 example/full/configuration.nix create mode 100644 example/full/flake.nix create mode 100644 example/small/configuration.nix create mode 100644 example/small/flake.nix create mode 100644 flake.nix create mode 100644 minecraft/Scripts/minecraft-template-backup-routine.sh create mode 100644 minecraft/Scripts/minecraft-template-backup.sh create mode 100644 minecraft/Scripts/minecraft-template-query.sh create mode 100644 minecraft/Scripts/minecraft-template-rcon.sh create mode 100644 minecraft/Scripts/minecraft-template-say.sh create mode 100644 minecraft/default.nix create mode 100644 minecraft/minecraft.nix diff --git a/README.md b/README.md index 9d4ee36..2792621 100644 --- a/README.md +++ b/README.md @@ -1 +1,118 @@ -# Nyx-Minecraft +# Yet another Nix Minecraft module + +This NixOS module extends [Infinidoge/nix-minecraft](https://github.com/Infinidoge/nix-minecraft) with additional features for managing multiple Minecraft servers declaratively. + +## Features + +- Declarative configuration of multiple Minecraft servers + +- Control over memory, operators, whitelist, symlinks, and server properties + +- Automatic **systemd services** and **timers** for scheduled jobs + +- Prebuilt helper scripts for administration: + + - `rcon` + - `query` + - `backup` + - `say` + - `backup-routine` + +- Support for: + + - Backups + - Logging + - Scheduled maintenance tasks + +## Helper Scripts + +For each server defined, helper scripts are generated into your system `$PATH`. +They follow the naming convention: + +```bash +minecraft-- +# Example: +minecraft-myserver-say "yellow" "hello" +``` + +You can inspect the generated scripts under: `minecraft/Scripts` + +## Configuration Options + +```nix +nyx-minecraft.service = { + enable = true; # Enable the enhanced servers (boolean) + eula = true; # I can't accept this for you + user = "minecraft"; # System user that owns/runs servers - DON'T CHANGE THIS + group = "minecraft"; # System group that owns/runs servers - DON'T CHANGE THIS + dataDir = "/srv/minecraft"; # Directory for Minecraft server data + + servers = { + myserver = { + enable = true; + memory.min = "2G"; # JVM minimum memory + memory.max = "4G"; # JVM maximum memory + + # This will be directly exposed to [Nix-Minecraft](https://github.com/Infinidoge/nix-minecraft/tree/master?tab=readme-ov-file#packages) + # Check their documentation for available options. + package = pkgs.minecraftServers.vanilla-1_20_4; + + autoStart = true; # Start server on boot + + whitelist = { # Declarative whitelist (UUIDs per user) + alice = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"; + bob = "ffffffff-1111-2222-3333-444444444444"; + }; + + operators = { # Declarative operator list + alice = { + uuid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"; + level = 4; + }; + }; + + properties = { # Declarative `server.properties` + serverPort = 25565; + motd = "Welcome to my NixOS server!"; + maxPlayers = 10; + }; + + schedules.backup = { # Systemd timer + service job + timer = "daily"; + code = "minecraft-myserver-backup"; + }; + }; + }; +}; +``` + +For more details, refer to the [official nix-minecraft documentation](https://github.com/Infinidoge/nix-minecraft), since most arguments are passed through. + +### Server Options + +- **memory.min / memory.max** — JVM memory allocation (e.g. `"2G"`) +- **package** — Minecraft server package (default: `pkgs.minecraft-server`) +- **autoStart** — Start on boot (`true` / `false`) +- **whitelist** — Declarative whitelist keyed by username with UUID +- **operators** — Operator list with permission levels (`0–4`) +- **symlinks** — Files/packages symlinked into server data directory +- **properties** — Declarative `server.properties` values (ports, motd, difficulty, etc.) +- **schedules** — Declarative scheduled jobs (with systemd timers and services) + +## Examples + +See the examples in: + +- `./example/full` +- `./example/small` + +## Warnings + +- **Use at your own risk** — verify backups and test schedules before relying on them +- Pin a known working version for stable usage +- Perform **manual backups before updates** + +## Contributing + +I am happy to help with issues and welcome pull requests for improvements. +Since I use this module personally, you can expect frequent updates. diff --git a/example/full/configuration.nix b/example/full/configuration.nix new file mode 100644 index 0000000..931b6c2 --- /dev/null +++ b/example/full/configuration.nix @@ -0,0 +1,112 @@ +{ + config, + lib, + pkgs, + ... +}: + +imports = [ + + # ... your imports + inputs.nyx-minecraft.nixosModules.minecraft-servers + ]; + + nyx-minecraft.service = { + enable = true; + eula = true; + # user # don't change this + # group # don't change this + dataDir = "srv/minecraft"; + + servers = { + testingServer = { + enable = true; + + memory = { + min = "2G"; + max = "4G"; + }; + + package = pkgs.minecraftServers.vanilla-1_20_4; + autoStart = true; + + whitelist = { + player1 = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"; + player2 = "ffffffff-1111-2222-3333-444444444444"; + }; + + operators = { + admin = { + uuid = "99999999-aaaa-bbbb-cccc-dddddddddddd"; + level = 4; + bypassesPlayerLimit = true; + }; + mod = { + uuid = "88888888-aaaa-bbbb-cccc-eeeeeeeeeeee"; + level = 2; + bypassesPlayerLimit = false; + }; + }; + + properties = { + serverPort = 25565; + difficulty = 2; + gamemode = 0; + maxPlayers = 20; + motd = "Welcome to the testingServer!"; + rconPassword = "superSecret123"; + hardcore = false; + levelSeed = "8675309"; + }; + + schedules = { + # note schedule can be enabled without the server being enabled + backup-hourly = { + enable = true; + # this is using systemD timers check the official Documentation + timer = "hourly"; + code = '' + minecraft-testingServer-backup-routine \ + --sleep 16 \ + --destination /srv/minecraft/backups/testingServer/hourly \ + --pure + ''; + }; + backup-daily = { + enable = true; + timer = "daily"; + code = '' + minecraft-testingServer-backup-routine \ + --sleep 60 \ + --destination /srv/minecraft/backups/testingServer/daily \ + --format zip + ''; + }; + backup-weekly = { + enable = true; + timer = "weekly"; + code = '' + minecraft-testingServer-backup-routine \ + --sleep 600 \ + --full \ + --destination /srv/minecraft/backups/testingServer/weekly \ + --format zip + ''; + }; + backup-monthly = { + enable = true; + timer = "monthly"; + code = '' + minecraft-testingServer-backup-routine \ + --sleep 960 \ + --full \ + --destination /srv/minecraft/backups/testingServer/monthly \ + --format zip + ''; + }; + + }; + }; + }; + }; +}; \ No newline at end of file diff --git a/example/full/flake.nix b/example/full/flake.nix new file mode 100644 index 0000000..3b4e4c7 --- /dev/null +++ b/example/full/flake.nix @@ -0,0 +1,34 @@ +{ + description = "EXAMPLE - Flake"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + # your own imports + nyx-minecraft.url = "github:Peritia-System/Nyx-Minecraft"; + nyx-minecraft.inputs.nixpkgs.follows = "nixpkgs"; + + }; + + outputs = inputs @ { + self, + nixpkgs, + nix-minecraft, + ... + }: { + nixosConfigurations = { + yourSystem = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + + specialArgs = { + inherit inputs self; + host = "yourSystem"; + }; + + modules = [ + nixos95.nixosModules.default + + ./configuration.nix + ]; + }; + }; +} diff --git a/example/small/configuration.nix b/example/small/configuration.nix new file mode 100644 index 0000000..07529e0 --- /dev/null +++ b/example/small/configuration.nix @@ -0,0 +1,58 @@ +{ + config, + lib, + pkgs, + ... +}: + +imports = [ + + # ... your imports + inputs.nyx-minecraft.nixosModules.minecraft-servers + ]; + +nyx-minecraft.service = { + enable = true; + eula = true; + # user # don't change this + # group # don't change this + dataDir = "srv/minecraft"; + + servers = { + testingServer = { + enable = true; + + memory = { + min = "2G"; + max = "4G"; + }; + + package = pkgs.minecraftServers.vanilla-1_20_4; + + # leaving whitelis out just deactivates whitelist + # leaving Operators out will just not set any operator + + # Leaving out a property will just set the default + properties = { + serverPort = 25565; + # note you don't need to set query or rcon port + # since they will be set 200 and 100 above the Serverport + }; + # you can leave them out than but here a simple example + schedules = { + # Hourly world-only, pure rsync, no restart + greeting-hourly = { + enable = true; + # note schedule can be enabled without the server being enabled + timer = "hourly"; + code = '' + minecraft-testingServer-say "yellow" "hello" + # now once an hour it will greet everyone in the server + ''; + }; + + }; + }; + }; + }; +}; \ No newline at end of file diff --git a/example/small/flake.nix b/example/small/flake.nix new file mode 100644 index 0000000..3b4e4c7 --- /dev/null +++ b/example/small/flake.nix @@ -0,0 +1,34 @@ +{ + description = "EXAMPLE - Flake"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + # your own imports + nyx-minecraft.url = "github:Peritia-System/Nyx-Minecraft"; + nyx-minecraft.inputs.nixpkgs.follows = "nixpkgs"; + + }; + + outputs = inputs @ { + self, + nixpkgs, + nix-minecraft, + ... + }: { + nixosConfigurations = { + yourSystem = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + + specialArgs = { + inherit inputs self; + host = "yourSystem"; + }; + + modules = [ + nixos95.nixosModules.default + + ./configuration.nix + ]; + }; + }; +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..7a8cc25 --- /dev/null +++ b/flake.nix @@ -0,0 +1,23 @@ +{ + description = "Nyx-Modules"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + nix-minecraft.url = "github:Infinidoge/nix-minecraft"; + }; + + outputs = { self, nixpkgs, nix-minecraft, ... }: { + nixosModules.minecraft-servers = { + config, + lib, + pkgs, + ... + }: { + imports = [ + ./minecraft + nix-minecraft.nixosModules.minecraft-servers + ]; + nixpkgs.overlays = [nix-minecraft.overlay]; + }; + }; +} diff --git a/minecraft/Scripts/minecraft-template-backup-routine.sh b/minecraft/Scripts/minecraft-template-backup-routine.sh new file mode 100644 index 0000000..f08a1ec --- /dev/null +++ b/minecraft/Scripts/minecraft-template-backup-routine.sh @@ -0,0 +1,420 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Injected by Nix +RSYNC_BIN="@RSYNC_BIN@" +DATA_DIR="@DATA_DIR@" +MCSTATUS_BIN="@MCSTATUS_BIN@" +MCRCON_BIN="@MCRCON_BIN@" +AWK_BIN="@AWK_BIN@" +QUERY_PORT="@QUERY_PORT@" +RCON_PORT="@RCON_PORT@" +RCON_PASSWORD="@RCON_PASSWORD@" +SERVER_NAME="@SERVER_NAME@" +TAR_BIN="@TAR_BIN@" +ZIP_BIN="@ZIP_BIN@" +UNZIP_BIN="@UNZIP_BIN@" +GZIP_BIN="@GZIP_BIN@" +ZSTD_BIN="@ZSTD_BIN@" +PV_BIN="@PV_BIN@" +DU_BIN="@DU_BIN@" +BZIP2_BIN="@BZIP2_BIN@" +XZ_BIN="@XZ_BIN@" + +# Convenience wrappers +rsync_cmd="$RSYNC_BIN" +awk_cmd="$AWK_BIN" +mcstatus_cmd="$MCSTATUS_BIN 127.0.0.1:${QUERY_PORT}" +mcrcon_cmd="$MCRCON_BIN -H 127.0.0.1 -P ${RCON_PORT} -p ${RCON_PASSWORD}" +tar_cmd="$TAR_BIN" +zip_cmd="$ZIP_BIN" +unzip_cmd="$UNZIP_BIN" +gzip_cmd="$GZIP_BIN" +zstd_cmd="$ZSTD_BIN" +pv_cmd="$PV_BIN" +du_cmd="$DU_BIN" +bzip2_cmd="$BZIP2_BIN" +xz_cmd="$XZ_BIN" + +# PATH extension +# (only figured that out later if you add it here it can actually just use the bin) +# So you can easily just switch out the "*_cmd" with the "normal" name +export PATH="$(dirname "$GZIP_BIN")":"$(dirname "$ZSTD_BIN")":"$(dirname "$PV_BIN")":"$(dirname "$DU_BIN")":"$(dirname "$BZIP2_BIN")":"$(dirname "$XZ_BIN")":"$PATH" + +# Defaults +REBOOT=false +SLEEP_TIME=0 +FULL=false +DESTINATION="/srv/minecraft/backups/unknown" +PURE=false +FORMAT="tar" +COMPRESSION="gzip" + +# Usage +usage() { + cat <] [--full] [--destination ] [--pure] + [--format ] [--compression ] + + --reboot Stop server before backup and start afterwards [DOES NOT WORK] + --sleep N Wait N seconds with countdown announcements + --full Backup entire server directory (default: world only) + --destination X Backup target directory (default: /srv/minecraft/backups/unknown) + --pure Use rsync to copy files (no compression, symlinks resolved) + --format X Archive format: tar or zip (ignored if --pure) + --compression X Compression for tar (default: gzip) +EOF + exit 1 +} + +# Argument parsing +echo "[DEBUG] Parsing command-line arguments..." +while [[ $# -gt 0 ]]; do + case "$1" in + --reboot) echo "[DEBUG] Flag: --reboot"; REBOOT=true; shift ;; + --sleep) echo "[DEBUG] Flag: --sleep $2"; SLEEP_TIME="$2"; shift 2 ;; + --full) echo "[DEBUG] Flag: --full"; FULL=true; shift ;; + --destination) echo "[DEBUG] Flag: --destination $2"; DESTINATION="$2"; shift 2 ;; + --pure) echo "[DEBUG] Flag: --pure"; PURE=true; shift ;; + --format) echo "[DEBUG] Flag: --format $2"; FORMAT="$2"; shift 2 ;; + --compression) echo "[DEBUG] Flag: --compression $2"; COMPRESSION="$2"; shift 2 ;; + --help) usage ;; + *) echo "[ERROR] Unknown option: $1"; usage ;; + esac +done + +# Restart if rebooting +if [[ "$REBOOT" == true ]]; then + echo "[DEBUG] Restarting server does not work" + echo "[DEBUG] The sudo can't be enabled due to it not being used by the same path as for the systemd" + echo "[DEBUG] and just using systemctl won't work either due to it not having the rights to stop it" + echo "[DEBUG] if you fix this pls make a PR" + +fi + + + +# Helpers + +say_with_color() { + local color="$1" + shift + local message="$*" + local code + + case "$color" in + black) code="§0" ;; + dark_blue) code="§1" ;; + dark_green) code="§2" ;; + dark_aqua) code="§3" ;; + dark_red) code="§4" ;; + dark_purple) code="§5" ;; + gold) code="§6" ;; + gray) code="§7" ;; + dark_gray) code="§8" ;; + blue) code="§9" ;; + green) code="§a" ;; + aqua) code="§b" ;; + red) code="§c" ;; + light_purple|pink) code="§d" ;; + yellow) code="§e" ;; + white) code="§f" ;; + obfuscated) code="§k" ;; + bold) code="§l" ;; + strikethrough) code="§m" ;; + underline) code="§n" ;; + italic) code="§o" ;; + reset) code="§r" ;; + *) code="" ;; + esac + + local full_message="${code}${message}§r" +# echo "[DEBUG] Sending RCON say: $full_message" + $mcrcon_cmd "say $full_message" +} + +say() { + echo "[INFO] $1" + say_with_color yellow "$1" +} + + + +countdown() { + local seconds="$1" + + #echo "[DEBUG] Starting countdown of $seconds seconds..." + say "Backup will start in $seconds seconds" + + while [ "$seconds" -gt 0 ]; do + #echo " $seconds" + + # Logic for when to speak updates + if [ "$seconds" -le 15 ]; then + say "$seconds" + elif [ "$seconds" -le 60 ] && (( seconds % 10 == 0 )); then + say "$seconds seconds remaining" + elif [ "$seconds" -le 120 ] && (( seconds % 30 == 0 )); then + say "$seconds seconds remaining" + elif [ "$seconds" -le 300 ] && (( seconds % 60 == 0 )); then + say "$seconds seconds remaining" + fi + + sleep 1 + ((seconds--)) + done + + echo + say "Countdown finished. Starting backup now." +} + + +do_backup() { + #echo "[DEBUG] Entering do_backup with args: $*" + local source="" + local destination="" + local compression="gzip" + local format="tar" + local pure=false + + # parse args + while [[ $# -gt 0 ]]; do + case "$1" in + --source) source="$2"; shift 2 ;; + --destination) destination="$2"; shift 2 ;; + --compression) compression="$2"; shift 2 ;; + --format) format="$2"; shift 2 ;; + --pure) pure=true; shift ;; + + *) echo "[ERROR] Unknown option to do_backup: $1"; return 1 ;; + esac + done + + + if [[ -z "$source" || -z "$destination" ]]; then + echo "[ERROR] Missing --source or --destination" + return 1 + fi + + local timestamp="$(date +%Y%m%d-%H%M%S)" + local full_source="$DATA_DIR/$source" + local basename="$(basename "$source")" + local archive="" + local ext="" + + + if [[ ! -d "$full_source" ]]; then + echo "[ERROR] Source directory not found: $full_source" + return 1 + fi + + mkdir -p "$destination" + + if [[ "$pure" == true ]]; then + local target_dir="$destination/${basename}-${timestamp}" + echo "[INFO] Performing pure rsync backup to $target_dir" + echo "#####" + + local last_percentage=-1 + + "$rsync_cmd" -rptgoDL --delete --info=progress2 --stats \ + "$full_source/" "$target_dir/" 2>&1 | \ + while IFS= read -r -d $'\r' chunk; do + # Extract percentage + if [[ $chunk =~ ([0-9]{1,3})% ]]; then + current_percentage=${BASH_REMATCH[1]} + # Print only if percentage changed + if [[ $current_percentage -ne $last_percentage ]]; then + echo -e "Progress: ${current_percentage}%\r" | say "$@" + last_percentage=$current_percentage + fi + fi + done + + + return 0 + fi + +#echo "[DEBUG] Using archive mode: format=$format compression=$compression" +# Archive/compression backup + +case "$format" in + tar) + # Map compression → extension and tar invocation (using only *_cmd vars) + case "$compression" in + none) + ext="tar" + tar_create=( "$tar_cmd" -cvf ) + ;; + gzip) + ext="tar.gz" + tar_create=( "$tar_cmd" --use-compress-program="$gzip_cmd" -cvf ) + ;; + bzip2) + ext="tar.bz2" + tar_create=( "$tar_cmd" --use-compress-program="$bzip2_cmd" -cvf ) + ;; + xz) + ext="tar.xz" + tar_create=( "$tar_cmd" --use-compress-program="$xz_cmd" -cvf ) + ;; + zstd) + ext="tar.zst" + tar_create=( "$tar_cmd" --use-compress-program="$zstd_cmd" -cvf ) + ;; + *) + echo "[ERROR] Unsupported tar compression: $compression" >&2 + exit 1 + ;; + esac + + archive="$destination/${basename}-${timestamp}.${ext}" + sources=( $source ) + for s in "${sources[@]}"; do + if [[ ! -e "$DATA_DIR/$s" ]]; then + echo "[ERROR] Source not found under DATA_DIR: $DATA_DIR/$s" >&2 + exit 1 + fi + done + + # Count total items for progress (files + dirs) + total_items=0 + for s in "${sources[@]}"; do + count=$(find "$DATA_DIR/$s" | wc -l) + total_items=$(( total_items + count )) + done + current=0 + + echo "[INFO] Creating $archive" + + last_percent=-1 + last_info_time=$(date +%s) + # seconds between info logs + interval=2 + + ( + "${tar_create[@]}" "$archive" -C "$DATA_DIR" "${sources[@]}" + ) 2>&1 | while read -r line; do + if [[ -n "$line" ]]; then + current=$(( current + 1 )) + if (( total_items > 0 )); then + percent=$(( current * 100 / total_items )) + + # echo full percent: + #echo "[DEBUG] Progress: ${percent}%" + now=$(date +%s) + if (( percent != last_percent )) && (( now - last_info_time >= interval )); then + #echo "[INFO] Progress: ${percent}%" + say "Progress: ${percent}%" + last_percent=$percent + last_info_time=$now + fi + fi + fi + done + + # Ensure 100% gets printed once at the end + if (( last_percent < 100 )); then + #echo "[INFO] Progress: 100%" + say "Progress: 100%" + + fi + + echo "[INFO] Tar archive created: $archive" + ;; + + zip) + ext="zip" + archive="$destination/${basename}-${timestamp}.${ext}" + echo "[INFO] Creating zip archive $archive" + + # Count both files and directories + total_items=$(find "$DATA_DIR/$source" | wc -l) + current=0 + + last_percent=-1 + last_info_time=$(date +%s) + interval=2 # seconds between info logs + + ( + cd "$DATA_DIR" + "$zip_cmd" -r "$archive" "$source" + ) 2>&1 | while read -r line; do + if [[ $line =~ adding: ]]; then + current=$((current+1)) + if (( total_items > 0 )); then + percent=$(( current * 100 / total_items )) + + #echo "[DEBUG] Progress: ${percent}%" + + now=$(date +%s) + if (( percent != last_percent )) && (( now - last_info_time >= interval )); then + #echo "[INFO] Progress: ${percent}%" + say "Progress: ${percent}%" + last_percent=$percent + last_info_time=$now + fi + fi + fi + done + + # Ensure 100% gets printed once at the end + if (( last_percent < 100 )); then + #echo "[INFO] Progress: 100%" + say "Progress: 100%" + fi + + echo "[INFO] Zip archive created: $archive" + ;; + + *) + echo "[ERROR] Unsupported format: $format" + return 1 + ;; +esac + +echo "[INFO] Backup completed: $archive" +return 0 + +} + + +# MAIN + +#echo "[DEBUG] FULL=$FULL" + +if [[ "$FULL" == true ]]; then + BACKUP_SOURCE="${SERVER_NAME}" + BACKUP_MODE="full server directory" + DESTINATION="${DESTINATION}/Full" +else + BACKUP_SOURCE="${SERVER_NAME}/world" + BACKUP_MODE="world folder only" + DESTINATION="${DESTINATION}/World" +fi + +say "Backup for ($BACKUP_MODE) initiated" + +# Pre-backup wait +if (( SLEEP_TIME > 0 )); then + countdown "$SLEEP_TIME" +fi + + +mkdir -p "$DESTINATION" + +echo "[INFO] Running backup of $BACKUP_MODE to $DESTINATION..." +if do_backup \ + --source "$BACKUP_SOURCE" \ + --destination "$DESTINATION" \ + $([[ "$PURE" == true ]] && echo "--pure") \ + --compression "$COMPRESSION" \ + --format "$FORMAT"; then + echo "[INFO] Backup finished successfully." +else + echo "[ERROR] Backup failed!" + exit 1 +fi + + +say "Backup ($BACKUP_MODE) completed successfully." diff --git a/minecraft/Scripts/minecraft-template-backup.sh b/minecraft/Scripts/minecraft-template-backup.sh new file mode 100644 index 0000000..85bbba3 --- /dev/null +++ b/minecraft/Scripts/minecraft-template-backup.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Injected by Nix +RSYNC_BIN="@RSYNC_BIN@" +DATA_DIR="@DATA_DIR@" +MCSTATUS_BIN="@MCSTATUS_BIN@" +MCRCON_BIN="@MCRCON_BIN@" +AWK_BIN="@AWK_BIN@" +QUERY_PORT="@QUERY_PORT@" +RCON_PORT="@RCON_PORT@" +RCON_PASSWORD="@RCON_PASSWORD@" +SERVER_NAME="@SERVER_NAME@" +TAR_BIN="@TAR_BIN@" +ZIP_BIN="@ZIP_BIN@" +UNZIP_BIN="@UNZIP_BIN@" +GZIP_BIN="@GZIP_BIN@" +ZSTD_BIN="@ZSTD_BIN@" +PV_BIN="@PV_BIN@" +DU_BIN="@DU_BIN@" +BZIP2_BIN="@BZIP2_BIN@" +XZ_BIN="@XZ_BIN@" + +# Convenience wrappers +rsync_cmd="$RSYNC_BIN" +awk_cmd="$AWK_BIN" +mcstatus_cmd="$MCSTATUS_BIN 127.0.0.1:${QUERY_PORT}" +mcrcon_cmd="$MCRCON_BIN -H 127.0.0.1 -P ${RCON_PORT} -p ${RCON_PASSWORD}" +tar_cmd="$TAR_BIN" +zip_cmd="$ZIP_BIN" +unzip_cmd="$UNZIP_BIN" +gzip_cmd="$GZIP_BIN" +zstd_cmd="$ZSTD_BIN" +pv_cmd="$PV_BIN" +du_cmd="$DU_BIN" +bzip2_cmd="$BZIP2_BIN" +xz_cmd="$XZ_BIN" + +# PATH extension +# (only figured that out later if you add it here it can actually just use the bin) +# So you can easily just switch out the "*_cmd" with the "normal" name +# export PATH="$(dirname "$GZIP_BIN")":"$(dirname "$ZSTD_BIN")":"$(dirname "$PV_BIN")":"$(dirname "$DU_BIN")":"$(dirname "$BZIP2_BIN")":"$(dirname "$XZ_BIN")":"$PATH" + + + +# Defaults +SOURCE="" +DESTINATION="" +COMPRESSION="gzip" +FORMAT="tar" +PURE=false + +usage() { + cat < --destination + [--compression ] [--format ] [--pure] + +Options: + --source Subfolder under \$DATA_DIR to back up (required) + --destination Backup destination path (required) + --compression Compression method for tar archives (default: gzip) + --format Archive format: tar or zip (default: tar) + --pure Perform plain rsync copy without compression + --help Show this help +EOF + exit 1 +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --source) SOURCE="$2"; shift 2;; + --destination) DESTINATION="$2"; shift 2;; + --compression) COMPRESSION="$2"; shift 2;; + --format) FORMAT="$2"; shift 2;; + --pure) PURE=true; shift 1;; + --help) usage;; + *) echo "Unknown option: $1"; usage;; + esac +done + +# Validation +if [[ -z "$SOURCE" || -z "$DESTINATION" ]]; then + echo "Error: --source and --destination are required." + usage +fi + +FULL_SOURCE="$DATA_DIR/$SOURCE" + +if [[ ! -d "$FULL_SOURCE" ]]; then + echo "Error: Source directory '$FULL_SOURCE' does not exist." + exit 1 +fi + +mkdir -p "$DESTINATION" + +TIMESTAMP="$(date +%Y%m%d-%H%M%S)" +BASENAME="$(basename "$SOURCE")" + +# Pure rsync backup +if [[ "$PURE" == true ]]; then + TARGET_DIR="$DESTINATION/${BASENAME}-${TIMESTAMP}" + echo "Performing pure rsync backup to $TARGET_DIR" + "$rsync_cmd" -rptgoDL --delete "$FULL_SOURCE/" "$TARGET_DIR/" + echo "Backup completed (pure): $TARGET_DIR" + exit 0 +fi + +# Archive/compression backup +case "$FORMAT" in + tar) + case "$COMPRESSION" in + gzip) EXT="tar.gz"; TAR_ARGS="-czf";; + bzip2) EXT="tar.bz2"; TAR_ARGS="-cjf";; + xz) EXT="tar.xz"; TAR_ARGS="-cJf";; + zstd) EXT="tar.zst"; TAR_ARGS="--zstd -cf";; + *) echo "Unsupported compression for tar: $COMPRESSION"; exit 1;; + esac + ARCHIVE="$DESTINATION/${BASENAME}-${TIMESTAMP}.${EXT}" + "$tar_cmd" -C "$DATA_DIR" $TAR_ARGS "$ARCHIVE" "$SOURCE" + ;; + zip) + EXT="zip" + ARCHIVE="$DESTINATION/${BASENAME}-${TIMESTAMP}.${EXT}" + (cd "$DATA_DIR" && "$zip_cmd" -r "$ARCHIVE" "$SOURCE") + ;; + *) + echo "Unsupported format: $FORMAT" + exit 1 + ;; +esac + + +echo "Backup completed: $ARCHIVE" diff --git a/minecraft/Scripts/minecraft-template-query.sh b/minecraft/Scripts/minecraft-template-query.sh new file mode 100644 index 0000000..1ef72e9 --- /dev/null +++ b/minecraft/Scripts/minecraft-template-query.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Injected by Nix +RSYNC_BIN="@RSYNC_BIN@" +DATA_DIR="@DATA_DIR@" +MCSTATUS_BIN="@MCSTATUS_BIN@" +MCRCON_BIN="@MCRCON_BIN@" +AWK_BIN="@AWK_BIN@" +QUERY_PORT="@QUERY_PORT@" +RCON_PORT="@RCON_PORT@" +RCON_PASSWORD="@RCON_PASSWORD@" +SERVER_NAME="@SERVER_NAME@" +TAR_BIN="@TAR_BIN@" +ZIP_BIN="@ZIP_BIN@" +UNZIP_BIN="@UNZIP_BIN@" +GZIP_BIN="@GZIP_BIN@" +ZSTD_BIN="@ZSTD_BIN@" +PV_BIN="@PV_BIN@" +DU_BIN="@DU_BIN@" +BZIP2_BIN="@BZIP2_BIN@" +XZ_BIN="@XZ_BIN@" + +# Convenience wrappers +rsync_cmd="$RSYNC_BIN" +awk_cmd="$AWK_BIN" +mcstatus_cmd="$MCSTATUS_BIN 127.0.0.1:${QUERY_PORT}" +mcrcon_cmd="$MCRCON_BIN -H 127.0.0.1 -P ${RCON_PORT} -p ${RCON_PASSWORD}" +tar_cmd="$TAR_BIN" +zip_cmd="$ZIP_BIN" +unzip_cmd="$UNZIP_BIN" +gzip_cmd="$GZIP_BIN" +zstd_cmd="$ZSTD_BIN" +pv_cmd="$PV_BIN" +du_cmd="$DU_BIN" +bzip2_cmd="$BZIP2_BIN" +xz_cmd="$XZ_BIN" + +# PATH extension +# (only figured that out later if you add it here it can actually just use the bin) +# So you can easily just switch out the "*_cmd" with the "normal" name +# export PATH="$(dirname "$GZIP_BIN")":"$(dirname "$ZSTD_BIN")":"$(dirname "$PV_BIN")":"$(dirname "$DU_BIN")":"$(dirname "$BZIP2_BIN")":"$(dirname "$XZ_BIN")":"$PATH" + + + +# Query the server +exec $mcstatus_cmd query diff --git a/minecraft/Scripts/minecraft-template-rcon.sh b/minecraft/Scripts/minecraft-template-rcon.sh new file mode 100644 index 0000000..2158f4a --- /dev/null +++ b/minecraft/Scripts/minecraft-template-rcon.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Injected by Nix +RSYNC_BIN="@RSYNC_BIN@" +DATA_DIR="@DATA_DIR@" +MCSTATUS_BIN="@MCSTATUS_BIN@" +MCRCON_BIN="@MCRCON_BIN@" +AWK_BIN="@AWK_BIN@" +QUERY_PORT="@QUERY_PORT@" +RCON_PORT="@RCON_PORT@" +RCON_PASSWORD="@RCON_PASSWORD@" +SERVER_NAME="@SERVER_NAME@" +TAR_BIN="@TAR_BIN@" +ZIP_BIN="@ZIP_BIN@" +UNZIP_BIN="@UNZIP_BIN@" +GZIP_BIN="@GZIP_BIN@" +ZSTD_BIN="@ZSTD_BIN@" +PV_BIN="@PV_BIN@" +DU_BIN="@DU_BIN@" +BZIP2_BIN="@BZIP2_BIN@" +XZ_BIN="@XZ_BIN@" + +# Convenience wrappers +rsync_cmd="$RSYNC_BIN" +awk_cmd="$AWK_BIN" +mcstatus_cmd="$MCSTATUS_BIN 127.0.0.1:${QUERY_PORT}" +mcrcon_cmd="$MCRCON_BIN -H 127.0.0.1 -P ${RCON_PORT} -p ${RCON_PASSWORD}" +tar_cmd="$TAR_BIN" +zip_cmd="$ZIP_BIN" +unzip_cmd="$UNZIP_BIN" +gzip_cmd="$GZIP_BIN" +zstd_cmd="$ZSTD_BIN" +pv_cmd="$PV_BIN" +du_cmd="$DU_BIN" +bzip2_cmd="$BZIP2_BIN" +xz_cmd="$XZ_BIN" + +# PATH extension +# (only figured that out later if you add it here it can actually just use the bin) +# So you can easily just switch out the "*_cmd" with the "normal" name +# export PATH="$(dirname "$GZIP_BIN")":"$(dirname "$ZSTD_BIN")":"$(dirname "$PV_BIN")":"$(dirname "$DU_BIN")":"$(dirname "$BZIP2_BIN")":"$(dirname "$XZ_BIN")":"$PATH" + + + +# Pass arguments directly to mcrcon +exec $mcrcon_cmd "$@" diff --git a/minecraft/Scripts/minecraft-template-say.sh b/minecraft/Scripts/minecraft-template-say.sh new file mode 100644 index 0000000..1a59823 --- /dev/null +++ b/minecraft/Scripts/minecraft-template-say.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Injected by Nix +RSYNC_BIN="@RSYNC_BIN@" +DATA_DIR="@DATA_DIR@" +MCSTATUS_BIN="@MCSTATUS_BIN@" +MCRCON_BIN="@MCRCON_BIN@" +AWK_BIN="@AWK_BIN@" +QUERY_PORT="@QUERY_PORT@" +RCON_PORT="@RCON_PORT@" +RCON_PASSWORD="@RCON_PASSWORD@" +SERVER_NAME="@SERVER_NAME@" +TAR_BIN="@TAR_BIN@" +ZIP_BIN="@ZIP_BIN@" +UNZIP_BIN="@UNZIP_BIN@" +GZIP_BIN="@GZIP_BIN@" +ZSTD_BIN="@ZSTD_BIN@" +PV_BIN="@PV_BIN@" +DU_BIN="@DU_BIN@" +BZIP2_BIN="@BZIP2_BIN@" +XZ_BIN="@XZ_BIN@" + +# Convenience wrappers +rsync_cmd="$RSYNC_BIN" +awk_cmd="$AWK_BIN" +mcstatus_cmd="$MCSTATUS_BIN 127.0.0.1:${QUERY_PORT}" +mcrcon_cmd="$MCRCON_BIN -H 127.0.0.1 -P ${RCON_PORT} -p ${RCON_PASSWORD}" +tar_cmd="$TAR_BIN" +zip_cmd="$ZIP_BIN" +unzip_cmd="$UNZIP_BIN" +gzip_cmd="$GZIP_BIN" +zstd_cmd="$ZSTD_BIN" +pv_cmd="$PV_BIN" +du_cmd="$DU_BIN" +bzip2_cmd="$BZIP2_BIN" +xz_cmd="$XZ_BIN" + +# PATH extension +# (only figured that out later if you add it here it can actually just use the bin) +# So you can easily just switch out the "*_cmd" with the "normal" name +# export PATH="$(dirname "$GZIP_BIN")":"$(dirname "$ZSTD_BIN")":"$(dirname "$PV_BIN")":"$(dirname "$DU_BIN")":"$(dirname "$BZIP2_BIN")":"$(dirname "$XZ_BIN")":"$PATH" + + + +# Argument parsing +if [[ $# -lt 2 ]]; then + echo "Usage: $0 " + echo "Example: $0 red 'Server restarting soon!'" + exit 1 +fi + +CODE_NAME="$1" +shift +MESSAGE="$*" + +# Map color/format names to Minecraft § codes +case "$CODE_NAME" in + # Colors + black) CODE="§0" ;; + dark_blue) CODE="§1" ;; + dark_green) CODE="§2" ;; + dark_aqua) CODE="§3" ;; + dark_red) CODE="§4" ;; + dark_purple) CODE="§5" ;; + gold) CODE="§6" ;; + gray) CODE="§7" ;; + dark_gray) CODE="§8" ;; + blue) CODE="§9" ;; + green) CODE="§a" ;; + aqua) CODE="§b" ;; + red) CODE="§c" ;; + light_purple|pink) CODE="§d" ;; + yellow) CODE="§e" ;; + white) CODE="§f" ;; + + # Bedrock-only extras + minecoin_gold) CODE="§g" ;; + material_quartz) CODE="§h" ;; + material_iron) CODE="§i" ;; + material_netherite) CODE="§j" ;; + material_redstone) CODE="§m" ;; + material_copper) CODE="§n" ;; + material_gold) CODE="§p" ;; + material_emerald) CODE="§q" ;; + material_diamond) CODE="§s" ;; + material_lapis) CODE="§t" ;; + material_amethyst) CODE="§u" ;; + + # Formatting + obfuscated) CODE="§k" ;; + bold) CODE="§l" ;; + strikethrough) CODE="§m" ;; + underline) CODE="§n" ;; + italic) CODE="§o" ;; + reset) CODE="§r" ;; + + *) + echo "Unknown code: $CODE_NAME" + echo "Available colors: black, dark_blue, dark_green, dark_aqua, dark_red, dark_purple, gold, gray, dark_gray, blue, green, aqua, red, light_purple, yellow, white" + echo "Formats: obfuscated, bold, strikethrough, underline, italic, reset" + exit 1 + ;; +esac + +FULL_MESSAGE="${CODE}${MESSAGE}§r" + +# Send via RCON +exec $mcrcon_cmd "say $FULL_MESSAGE" diff --git a/minecraft/default.nix b/minecraft/default.nix new file mode 100644 index 0000000..8dcc810 --- /dev/null +++ b/minecraft/default.nix @@ -0,0 +1,11 @@ +{ + config, + lib, + pkgs, + inputs, + ... +}: { + imports = [ + ./minecraft.nix + ]; +} diff --git a/minecraft/minecraft.nix b/minecraft/minecraft.nix new file mode 100644 index 0000000..1d8335d --- /dev/null +++ b/minecraft/minecraft.nix @@ -0,0 +1,474 @@ +# Enhanced Minecraft Server Module +# +# This module provides: +# - A wrapper around the Infinidoge/nix-minecraft module +# - Declarative configuration for multiple servers, including memory, operators, whitelist, and symlinks +# - Automatic systemd service and timer generation for scheduled jobs +# - Prebuilt helper scripts (rcon, query, backup, say, backup-routine) for server administration +# - Support for backups, logging, and scheduled maintenance tasks +# +# Configuration Options: +# enable – Enable the enhanced Minecraft servers (boolean) +# user – System user that owns and runs the servers (string) - DON'T CHANGE THIS +# group – System group that owns and runs the servers (string) - DON'T CHANGE THIS +# dataDir – Directory to store Minecraft server data (path) +# servers – Attribute set of servers, keyed by name. Each server can define: +# memory.min / memory.max – JVM memory allocation (strings, e.g. "2G") +# package – Minecraft server package to use (package) +# autoStart – Start server at boot (boolean) +# whitelist – Declarative whitelist (UUIDs per user) +# operators – Declarative operator list with permission levels +# symlinks – Files or packages symlinked into the server data directory +# properties – Declarative `server.properties` values (ports, motd, difficulty, etc.) +# schedules – Declarative scheduled jobs with systemd timers and services +# +# +# Info: +# I am happy to help if you have Issues and am happy to see a PR for any change +# I ll use it personally too so you can expect frequent updates +# +# +# ToDo: +# Schedule to restart a server using systemD. I have not figured that out yet. +# what i do know is that `sudo` does not work neither does it work if you just tell the server to restart. +# +{ + config, + lib, + pkgs, + ... +}: +with lib; let + cfg = config.nyx-minecraft.service; + minecraftUUID = types.strMatching "[0-9a-fA-F-]{36}"; + + # Setup the Scripts for the server + scriptDir = ./Scripts; + mkScript = serverName: serverCfg: scriptType: let + templateFile = scriptDir + "/minecraft-template-${scriptType}.sh"; + rawText = builtins.readFile templateFile; + scriptText = + builtins.replaceStrings + [ + "@DATA_DIR@" + "@RSYNC_BIN@" + "@MCSTATUS_BIN@" + "@MCRCON_BIN@" + "@AWK_BIN@" + "@QUERY_PORT@" + "@RCON_PORT@" + "@RCON_PASSWORD@" + "@SERVER_NAME@" + "@TAR_BIN@" + "@ZIP_BIN@" + "@UNZIP_BIN@" + "@GZIP_BIN@" + "@ZSTD_BIN@" + "@PV_BIN@" + "@DU_BIN@" + "@BZIP2_BIN@" + "@XZ_BIN@" + ] + # If you add anything here make sure to add it at the systemd service too + [ + cfg.dataDir + "${pkgs.rsync}/bin/rsync" + "${pkgs.mcstatus}/bin/mcstatus" + "${pkgs.mcrcon}/bin/mcrcon" + "${pkgs.gawk}/bin/awk" + (toString (serverCfg.properties.serverPort + 200)) + (toString (serverCfg.properties.serverPort + 100)) + serverCfg.properties.rconPassword + serverName + "${pkgs.gnutar}/bin/tar" + "${pkgs.zip}/bin/zip" + "${pkgs.unzip}/bin/unzip" + "${pkgs.gzip}/bin/gzip" + "${pkgs.zstd}/bin/zstd" + "${pkgs.pv}/bin/pv" + "${pkgs.coreutils}/bin/du" + "${pkgs.bzip2}/bin/bzip2" + "${pkgs.xz}/bin/xz" + ] + rawText; + in + pkgs.writeShellScriptBin "minecraft-${serverName}-${scriptType}" scriptText; +in { + # Note most of the options get directly exposed + # to nix-minecraft which makes it almost an + # Drop in replacement + options.nyx-minecraft.service = { + enable = mkEnableOption "Enable enhanced Minecraft servers with backup, logging, and admin helpers."; + + eula = mkEnableOption '' + Whether you agree to + + Mojang's EULA. This option must be set to + true to run Minecraft server. + ''; + + user = mkOption { + type = types.str; + default = "minecraft"; + description = '' + Name of the user to create and run servers under. + It is recommended to leave this as the default, as it is + the same user as . + ''; + internal = true; + visible = false; + }; + + group = mkOption { + type = types.str; + default = "minecraft"; + description = '' + Name of the group to create and run servers under. + In order to modify the server files your user must be a part of this + group. If you are using the tmux management system (the default), you also need to be a part of this group to attach to the tmux socket. + It is recommended to leave this as the default, as it is + the same group as . + ''; + }; + + dataDir = mkOption { + type = types.path; + default = "/srv/minecraft"; + description = '' + Directory to store the Minecraft servers. + Each server will be under a subdirectory named after + the server name in this directory, such as /srv/minecraft/servername. ''; + }; + + servers = mkOption { + type = types.attrsOf (types.submodule ({name, ...}: { + options = { + enable = mkEnableOption "Enable this Server"; + + memory = mkOption { + type = types.submodule { + options = { + min = mkOption { + type = types.str; + default = "2G"; + description = "Min JVM memory."; + }; + max = mkOption { + type = types.str; + default = "2G"; + description = "Max JVM memory."; + }; + }; + }; + default = { + min = "2G"; + max = "2G"; + }; + description = "JVM memory settings for this server."; + }; + + package = mkOption { + description = "The Minecraft server package to use."; + type = types.package; + default = pkgs.minecraft-server; + defaultText = literalExpression "pkgs.minecraft-server"; + example = "pkgs.minecraftServers.vanilla-1_18_2"; + }; + + autoStart = mkOption { + type = types.bool; + default = true; + description = '' + Whether to start this server on boot. + If set to false, it can still be started with + systemctl start minecraft-server-servername. + Requires the server to be enabled. + ''; + }; + + whitelist = mkOption { + type = types.attrsOf minecraftUUID; + default = {}; + description = '' + Whitelisted players, only has an effect when + enabled via + by setting white-list to true. + + To use a non-declarative whitelist, enable the whitelist and don't fill in this value. + As long as it is empty, no whitelist file is generated. + ''; + example = literalExpression '' + { + username1 = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; + username2 = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"; + } + ''; + }; + + symlinks = mkOption { + type = types.attrsOf (types.either types.path types.package); + default = {}; + description = '' + Things to symlink into this server's data directory. + Can be used to declaratively manage arbitrary files (e.g., mods, configs). + ''; + }; + + operators = mkOption { + type = types.attrsOf ( + types.coercedTo minecraftUUID (v: {uuid = v;}) ( + types.submodule { + options = { + uuid = mkOption { + type = minecraftUUID; + description = "The operator's UUID"; + example = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; + }; + level = mkOption { + type = types.ints.between 0 4; + description = "The operator's permission level"; + default = 4; + }; + bypassesPlayerLimit = mkOption { + type = types.bool; + description = "If true, the operator can join the server even if the player limit has been reached"; + default = false; + }; + }; + } + ) + ); + default = {}; + description = "Operators with permission levels."; + }; + + properties = mkOption { + type = types.submodule { + options = { + serverPort = mkOption { + type = types.int; + default = 25565; + description = "Server Port"; + }; + difficulty = mkOption { + type = types.int; + default = 2; + description = "Difficulty in numbers: 0=Peaceful, 1=Easy, 2=Normal, 3=Hard"; + }; + gamemode = mkOption { + type = types.int; + default = 0; + description = "Gamemode: 0=Survival, 1=Creative"; + }; + maxPlayers = mkOption { + type = types.int; + default = 5; + description = "How many players can join the server"; + }; + motd = mkOption { + type = types.str; + default = "NixOS Minecraft server!"; + description = "Message displayed when selecting the server"; + }; + rconPassword = mkOption { + type = types.str; + default = "superSecret"; + description = "Password for Rcon"; + }; + hardcore = mkOption { + type = types.bool; + default = false; + description = "Enable Hardcore mode"; + }; + levelSeed = mkOption { + type = types.str; + default = "42"; + description = "World seed (default is the answer to the universe)"; + }; + }; + }; + default = {}; + description = "Declarative Minecraft server.properties values."; + }; + + schedules = mkOption { + type = types.attrsOf (types.submodule ({name, ...}: { + options = { + enable = mkOption { + type = types.bool; + default = true; + description = "Whether this schedule is active."; + }; + + timer = mkOption { + type = types.str; + example = "hourly"; + description = "Systemd timer unit specifier (e.g., hourly, daily, weekly)."; + }; + + code = mkOption { + type = types.lines; + description = "Shell code to execute when the schedule fires."; + }; + + # Not properly enough tested + #customEnviorment = mkOption { + # type = types.lines; + # description = "to expose binaries or other things to the SystemD service"; + # example = "ZSTD_BIN=${pkgs.zstd}/bin/zstd"; + #}; + }; + })); + default = {}; + description = "Scheduled jobs for this Minecraft server."; + }; + }; + })); + default = {}; + description = "Servers to run under the enhanced Minecraft service."; + }; + }; + + # Wrapper for nix-minecraft + config = mkIf cfg.enable { + services.minecraft-servers = { + enable = true; + eula = cfg.eula; + openFirewall = true; + user = cfg.user; + group = cfg.group; + dataDir = cfg.dataDir; + + servers = + lib.mapAttrs (serverName: serverCfg: { + enable = serverCfg.enable; + package = serverCfg.package; + jvmOpts = "-Xmx${serverCfg.memory.max} -Xms${serverCfg.memory.min}"; + autoStart = serverCfg.autoStart; + + symlinks = serverCfg.symlinks; + whitelist = serverCfg.whitelist; + operators = serverCfg.operators; + + serverProperties = { + enable-rcon = true; + enable-command-block = true; + allow-flight = true; + enable-query = true; + server-port = serverCfg.properties.serverPort; + difficulty = serverCfg.properties.difficulty; + gamemode = serverCfg.properties.gamemode; + max-players = serverCfg.properties.maxPlayers; + motd = serverCfg.properties.motd; + "rcon.password" = serverCfg.properties.rconPassword; + "rcon.port" = serverCfg.properties.serverPort + 100; + "query.port" = serverCfg.properties.serverPort + 200; + + hardcore = serverCfg.properties.hardcore; + level-seed = serverCfg.properties.levelSeed; + }; + }) + cfg.servers; + }; + + # Schedule logic + systemd.services = lib.mkMerge ( + lib.mapAttrsToList ( + serverName: serverCfg: + lib.mapAttrs' (scheduleName: scheduleCfg: let + # yes this will be building the scripts twice but thsi + # way the path is accessible by the SystemD service + rconBin = mkScript serverName serverCfg "rcon"; + queryBin = mkScript serverName serverCfg "query"; + backupBin = mkScript serverName serverCfg "backup"; + sayBin = mkScript serverName serverCfg "say"; + routineBin = mkScript serverName serverCfg "backup-routine"; + in { + name = "minecraft-${serverName}-${scheduleName}"; + value = { + description = "Minecraft ${serverName} scheduled job: ${scheduleName}"; + serviceConfig = { + Type = "oneshot"; + User = cfg.user; + Group = cfg.group; + Environment = [ + "RCON_BIN=${rconBin}/bin/minecraft-${serverName}-rcon" + "QUERY_BIN=${queryBin}/bin/minecraft-${serverName}-query" + "BACKUP_BIN=${backupBin}/bin/minecraft-${serverName}-backup" + "SAY_BIN=${sayBin}/bin/minecraft-${serverName}-say" + "ROUTINE_BIN=${routineBin}/bin/minecraft-${serverName}-backup-routine" + "ZSTD_BIN=${pkgs.zstd}/bin/zstd" + # add more bin here + + + + # Not properly enough tested + # + #scheduleCfg.customEnviorment + ]; + ExecStart = pkgs.writeShellScript "minecraft-${serverName}-${scheduleName}.sh" '' + #!/usr/bin/env bash + echo "hi — available helpers:" + echo "if you want any custom scripts or" + echo "packages for your script you need to ExecStart" + echo " $RCON_BIN" + echo " $QUERY_BIN" + echo " $BACKUP_BIN" + echo " $SAY_BIN" + echo " $ROUTINE_BIN" + + # this is so it can use the scripts the same way you would run them: + minecraft-${serverName}-query() { $QUERY_BIN "$@"; } + minecraft-${serverName}-rcon() { $RCON_BIN "$@"; } + minecraft-${serverName}-backup() { $BACKUP_BIN "$@"; } + minecraft-${serverName}-say() { $SAY_BIN "$@"; } + minecraft-${serverName}-backup-routine() { $ROUTINE_BIN "$@"; } + minecraft-${serverName}-query + + # her your code will go: + ${scheduleCfg.code} + + ''; + }; + + # If you have this on: + # wantedBy = [ "multi-user.target" ]; + # it will run the service on each rebuild. + # i dont want that you can enable it if you like + }; + }) serverCfg.schedules + ) + cfg.servers + ); + + # the timers to actually run the SystemD service + systemd.timers = lib.mkMerge ( + lib.mapAttrsToList ( + serverName: serverCfg: + lib.mapAttrs' (scheduleName: scheduleCfg: { + name = "minecraft-${serverName}-${scheduleName}"; + value = { + description = "Timer for Minecraft ${serverName} schedule ${scheduleName}"; + wantedBy = ["timers.target"]; + timerConfig.OnCalendar = scheduleCfg.timer; + }; + }) + serverCfg.schedules + ) + cfg.servers + ); + + # this is building the scripts for the user + # Those are the prewritten scripts from the ./Script dir + environment.systemPackages = lib.flatten ( + lib.mapAttrsToList (serverName: serverCfg: [ + (mkScript serverName serverCfg "rcon") + (mkScript serverName serverCfg "query") + (mkScript serverName serverCfg "backup") + (mkScript serverName serverCfg "say") + (mkScript serverName serverCfg "backup-routine") + ]) + cfg.servers + ); + }; +}