Nyx-Minecraft/minecraft/minecraft.nix

474 lines
17 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
<link xlink:href="https://account.mojang.com/documents/minecraft_eula">
Mojang's EULA</link>. This option must be set to
<literal>true</literal> 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 <option>services.minecraft-server</option>.
'';
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 <option>services.minecraft-server</option>.
'';
};
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 <literal>/srv/minecraft/servername</literal>. '';
};
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 <literal>false</literal>, it can still be started with
<literal>systemctl start minecraft-server-servername</literal>.
Requires the server to be enabled.
'';
};
whitelist = mkOption {
type = types.attrsOf minecraftUUID;
default = {};
description = ''
Whitelisted players, only has an effect when
enabled via <option>services.minecraft-servers.<name>.serverProperties</option>
by setting <literal>white-list</literal> to <literal>true</literal>.
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
);
};
}