Last active
December 18, 2024 06:38
-
-
Save AfroThundr3007730/a3a01297705d65ed8c327539b5a55c46 to your computer and use it in GitHub Desktop.
Generates a coordinate grid to move a player to generate a Minecraft map
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Generates a coordinate grid to move a player and generate a Minecraft map | |
# Can run commands live via screen or RCON, or generate a command list | |
# SPDX-License-Identifier: GPL-3.0-or-later | |
# For issues or updated versions of this script, browse to the following URL: | |
# https://gist.github.com/AfroThundr3007730/a3a01297705d65ed8c327539b5a55c46 | |
# Dependencies: screen python3(rcon) journalctl tail pkill | |
set -euo pipefail | |
shopt -s extdebug | |
mc_genmap.set_globals() { | |
# Identify ourselves | |
declare -gr \ | |
AUTHOR=AfroThundr \ | |
BASENAME=${0##*/} \ | |
MODIFIED=20241217 \ | |
VERSION=0.7.4 | |
# Overridden via corresponding arguments | |
declare -g \ | |
DIMENSION=overworld \ | |
HEIGHT=160 \ | |
INTERVAL=16 \ | |
SLEEP=1 | |
# Overridden via environment variables | |
declare -g \ | |
HOST=${HOST:=localhost} \ | |
PORT=${PORT:=25575} \ | |
SNAME=${SNAME:=mc} | |
# Prevent string evaluation (SLEEP can be decimal) | |
declare -gi \ | |
HEIGHT INTERVAL RESUME \ | |
BOX_X1 BOX_X2 BOX_Z1 BOX_Z2 \ | |
EBOX_X1 EBOX_X2 EBOX_Z1 EBOX_Z2 \ | |
ORIGIN_X ORIGIN_Z | |
# All the rest should be present in this group | |
declare -g \ | |
QUIET LOGFILE PLAYER \ | |
COMMAND PASS PASSFILE \ | |
OUTPUT MOVE TYPE START STOP \ | |
WD_RUN WD_JOURNAL WD_LOGFILE | |
# Full program usage | |
declare -gr _mc_genmap_help_text=" | |
Generates Minecraft map by moving player in a coordinate grid. | |
Can run the commands via local screen or remote RCON session. | |
Or alternatively, generates a teleport command list to be run. | |
Usage: $BASENAME -h | [-v] [-q] [-l <logfile>] -p <player> | |
-x1 <x_begin> -x2 <x_end> [-z1 <z_begin> -z2 <z_end>] | |
[-ex1 <ex_begin> -ex2 <ex_end> [-ez1 <ez_begin> -ez2 <ez_end>]] | |
[-ox <x_origin> -oz <z_origin>] [-d <dimension>] | |
[-y <height>] [-i <interval>] [-r <step>] [-s <seconds>] | |
[-o <output_file> | -m -t <type> [-b -c <command>] [-k]] | |
[-wd -wj <mc_unit> | -wl <mc_logpath>] | |
Options: | |
-v Prints version information | |
-h Display this help text | |
-q Suppress console output | |
-l Specify log file path | |
Core arguments: | |
-p Name of the player to be moved | |
-x1 X component of bounding box start coordinate | |
-x2 X component of bounding box end coordinate | |
-z1 Z component of bounding box start coordinate | |
If not specified, reuses -x1 argument | |
-z2 Z component of bounding box end coordinate | |
If not specified, reuses -x2 argument | |
Exclusion box arguments: | |
-ex1 Behaves like -x1 for exclusion box | |
-ex2 Behaves like -x2 for exclusion box | |
-ez1 Behaves like -z1 for exclusion box | |
If not specified, reuses -ex1 argument | |
-ez2 Behaves like -z2 for exclusion box | |
If not specified, reuses -ex2 argument | |
Origin arguments: | |
-ox X coordinate on which to center the box | |
If specified, all coordinates become relative | |
-oz Z coordinate on which to center the box | |
If specified, all coordinates become relative | |
Additional arguments: | |
-d Dimension ID to generate movement grid on | |
If not specified, defaults to 'overworld' | |
-y Y coordinate (altitude) of player | |
If not specified, defaults to 160 | |
-i Interval in blocks to increment each step | |
If not specified, defualts to 16 | |
-r Step on which to resume generation | |
Specify output step of a previous run | |
-s Seconds to sleep after each move | |
If not specified, defaults to 1 | |
Command arguments: | |
-o Output file to write generated command list | |
If not specified and -m unspecified, uses STDOUT | |
-m Move player instead of printing coordinates | |
This requires launching a server and logging in | |
-t Type of command to execute (screen or rcon) | |
Must be present if -m is specified | |
-b Start the Minecraft server before beginning | |
Requires a command to launch server | |
-c Mincraft server start command to execute | |
Must be quoted, or use COMMAND instead | |
-k Stop Minecraft server after completion | |
Can be used in either command mode | |
Watchdog arguments: | |
-wd Enable watchdog to manage server load | |
Requires some form of access to server logs | |
-wj Use journalctl to follow a units logs | |
The next positional argument is a unit name | |
-wl Specify a log file to follow directly | |
The next positional argument is the log path | |
Config variables: | |
Used if type is 'rcon': | |
HOST Set the RCON server hostname | |
If not specified, defaults to 'localhost' | |
PORT Set the RCON server port | |
If not specified, defaults to 25575 | |
PASS Set the RCON server password | |
Recommended to use PASSFILE instead | |
Used if type is 'screen': | |
COMMAND The MC server launch command | |
Can be used instead of -c | |
SNAME The screen server session name | |
If not specified, defaults to 'mc' | |
" | |
# Inline RCON client | |
declare -gr _mc_genmap_rcon=' | |
from asyncio import run | |
from datetime import datetime, UTC | |
from os import environ | |
from signal import signal, SIGINT, SIGTERM | |
from sys import argv, exit | |
from rcon.exceptions import EmptyResponse | |
from rcon.source import rcon | |
def dprint(str, end=None): | |
print(datetime.now(UTC).strftime("%FT%TZ") + | |
": " + str, end=end, flush=True) | |
def end(*_): | |
dprint("RCON: Terminating.") | |
exit(0) | |
signal(SIGINT, end) | |
signal(SIGTERM, end) | |
try: | |
_ = run(rcon(command=argv[1], | |
host=environ.get("HOST"), | |
port=int(environ.get("PORT")), | |
passwd=environ.get("PASS"))) | |
except EmptyResponse: | |
dprint("RCON: Command timeout.") | |
except: | |
dprint("RCON: Exception occurred.") | |
exit(1) | |
' | |
# Inline watchdog governor | |
declare -gr _mc_genmap_watchdog=' | |
from datetime import datetime, UTC | |
from os import kill | |
from signal import signal, SIGCONT, SIGINT, SIGSTOP, SIGTERM | |
from sys import argv, exit, stdin | |
from time import sleep | |
def dprint(str, end=None): | |
print(datetime.now(UTC).strftime("%FT%TZ") + | |
": " + str, end=end, flush=True) | |
def end(*_): | |
dprint("WD: Terminating.") | |
exit(0) | |
signal(SIGINT, end) | |
signal(SIGTERM, end) | |
invalid = False | |
for ln in stdin: | |
if len(ln) <= 1: | |
continue | |
if "keep up" in ln and not invalid: | |
dprint("Pausing due to server load.", end="") | |
kill(int(argv[1]), SIGSTOP) | |
sleep(10) | |
print(" Resuming.") | |
kill(int(argv[1]), SIGCONT) | |
if argv[2] in ln and "left the game" in ln: | |
invalid = True | |
dprint("Pausing due to player left.") | |
kill(int(argv[1]), SIGSTOP) | |
sleep(10) | |
if "Crash Report" in ln or "Stopping server" in ln: | |
invalid = True | |
sleep(1) | |
dprint("Pausing due to server stop.") | |
kill(int(argv[1]), SIGSTOP) | |
sleep(60) | |
if argv[2] in ln and "joined the game" in ln: | |
invalid = False | |
dprint("Resuming after player join.") | |
sleep(10) | |
kill(int(argv[1]), SIGCONT) | |
' | |
} | |
mc_genmap.parse_args() { | |
(($# == 0)) && utils.say -h \ | |
'No arguments specified, use -h for help.' && exit 1 | |
while (($# > 0)); do | |
case "$1" in | |
-v) utils.say -h \ | |
"$BASENAME: Version $VERSION, updated $MODIFIED by $AUTHOR" && | |
shift && (($# > 0)) || exit 0 ;; | |
-h) utils.say -h "$_mc_genmap_help_text" && exit 0 ;; | |
-q) QUIET=true && shift ;; | |
-l) utils.test_arg "$1" "$2" && LOGFILE=$2 && shift 2 ;; | |
-p) utils.test_arg "$1" "$2" && PLAYER=$2 && shift 2 ;; | |
-x1) utils.test_arg "$1" "$2" && BOX_X1=$2 && shift 2 ;; | |
-x2) utils.test_arg "$1" "$2" && BOX_X2=$2 && shift 2 ;; | |
-z1) utils.test_arg "$1" "$2" && BOX_Z1=$2 && shift 2 ;; | |
-z2) utils.test_arg "$1" "$2" && BOX_Z2=$2 && shift 2 ;; | |
-ex1) utils.test_arg "$1" "$2" && EBOX_X1=$2 && shift 2 ;; | |
-ex2) utils.test_arg "$1" "$2" && EBOX_X2=$2 && shift 2 ;; | |
-ez1) utils.test_arg "$1" "$2" && EBOX_Z1=$2 && shift 2 ;; | |
-ez2) utils.test_arg "$1" "$2" && EBOX_Z2=$2 && shift 2 ;; | |
-x) utils.test_arg "$1" "$2" && ORIGIN_X=$2 && shift 2 ;; | |
-z) utils.test_arg "$1" "$2" && ORIGIN_Z=$2 && shift 2 ;; | |
-d) utils.test_arg "$1" "$2" && DIMENSION=$2 && shift 2 ;; | |
-y) utils.test_arg "$1" "$2" && HEIGHT=$2 && shift 2 ;; | |
-i) utils.test_arg "$1" "$2" && INTERVAL=$2 && shift 2 ;; | |
-r) utils.test_arg "$1" "$2" && RESUME=$2 && shift 2 ;; | |
-s) utils.test_arg "$1" "$2" && SLEEP=$2 && shift 2 ;; | |
-o) utils.test_arg "$1" "$2" && OUTPUT=$2 && shift 2 ;; | |
-m) MOVE=true && shift ;; | |
-t) utils.test_arg "$1" "$2" && TYPE=$2 && shift 2 ;; | |
-b) START=true && shift ;; | |
-c) utils.test_arg "$1" "$2" && COMMAND=$2 && shift 2 ;; | |
-k) STOP=true && shift ;; | |
-wd) WD_RUN=true && shift ;; | |
-wj) utils.test_arg "$1" "$2" && WD_JOURNAL=$2 && shift 2 ;; | |
-wl) utils.test_arg "$1" "$2" && WD_LOGFILE=$2 && shift 2 ;; | |
*) utils.say -h \ | |
'Invalid argument specified, use -h for help.' && exit 1 ;; | |
esac | |
done | |
} | |
utils.die() { | |
utils.say -e "${@:-'Something happened. :P'}" | |
exit 1 | |
} | |
utils.say() { | |
(($# == 1 || $# == 2)) || return | |
[[ $1 == -h ]] && printf '\e[34m%s\n' "$2" && return | |
[[ $1 == -e ]] && printf '\e[31m' && local fd=2 && shift | |
[[ ${TZOFFSET:-} ]] || utils.get_tz_offset_secs -s | |
[[ ${QUIET:-} ]] || | |
printf '%(%FT%TZ)T: %s\n' $((EPOCHSECONDS - TZOFFSET)) "$1" >&"${fd:-1}" | |
[[ ${LOGFILE:-} ]] && | |
printf '%(%FT%TZ)T: %s\n' $((EPOCHSECONDS - TZOFFSET)) "$1" >>"$LOGFILE" | |
printf '\e[m' | |
} | |
utils.get_tz_offset_secs() { | |
local off && printf -v off '\n%(%z)T' -1 | |
off=${off: -5:1}$((${off: -4:2} * 3600 + ${off: -2:2} * 60)) | |
[[ $1 == -s ]] || printf '%s\n' "$off" && declare -gi TZOFFSET="$off" | |
} | |
utils.seconds_to_hms() { | |
local -i in=${1:-$(</dev/stdin)} | |
((in / 86400 > 0)) && printf '%d.' $((in / 86400)) | |
printf '%.02d:%.02d:%.02d\n' \ | |
$((in % 86400 / 3600)) $((in % 3600 / 60)) $((in % 60)) | |
} | |
utils.sleep() { | |
[[ ${_fd:-} ]] || { exec {_fd}<> <(:) && utils.sleep; } | |
read -r -t "${1:-0}" -u "$_fd" || : | |
} | |
utils.string_xnor() { | |
(($# == 2)) || return 2 | |
[[ $1 && $2 || ! $1 && ! $2 ]] | |
} | |
utils.test_arg() { | |
(($# == 2)) || return | |
[[ ${#2} -gt 0 && ($2 == "${2#-}" || $2 =~ ^-?[\.0-9]+) ]] || | |
utils.die "Option '$1' expects an argument, but we got: '$2'" | |
} | |
mc_genmap.translate_coords() { | |
: "${BOX_Z1:=BOX_X1}" | |
: "${BOX_Z2:=BOX_X2}" | |
: "${EBOX_Z1:=${EBOX_X1:=0}}" | |
: "${EBOX_Z2:=${EBOX_X2:=0}}" | |
[[ ! ${ORIGIN_X:-} || ! ${ORIGIN_Z:-} ]] || { | |
((BOX_X1 += ORIGIN_X, BOX_X2 += ORIGIN_X)) | |
((BOX_Z1 += ORIGIN_Z, BOX_Z2 += ORIGIN_Z)) | |
((EBOX_X1 += ORIGIN_X, EBOX_X2 += ORIGIN_X)) | |
((EBOX_Z1 += ORIGIN_Z, EBOX_Z2 += ORIGIN_Z)) | |
} | |
} | |
mc_genmap.validate_args() { | |
# Check for missing arguments | |
[[ ${PLAYER:-} && ${#PLAYER} -le 16 ]] || | |
utils.die 'Player name invalid. Constraint: 3 <= length(name) <= 16' | |
utils.string_xnor "${MOVE:-}" "${TYPE:-}" || | |
utils.die 'Type and move flags must be specified together.' | |
[[ ${TYPE:-} == screen ]] && | |
! utils.string_xnor "${START:-}" "${COMMAND:-}" && | |
utils.die 'COMMAND must be specified if using screen in start mode.' | |
[[ ${TYPE:-} == rcon && ! ${PASS:-} && ! ${PASSFILE:-} ]] && | |
utils.die 'RCON mode chosen but PASS or PASSFILE must be specified.' | |
[[ ${WD_RUN:-} && ! ${WD_JOURNAL:-} && ! ${WD_LOGFILE:-} ]] && | |
utils.die 'Watchdog enabled but neither -wj or -wl were specified.' | |
{ [[ ${BOX_X1:-} && ${BOX_X2:-} ]] && | |
utils.string_xnor "${BOX_Z1:-}" "${BOX_Z2:-}"; } || | |
utils.die 'Invalid bounding box. Specify: x1,x2,z1,z2 || x1,x2' | |
[[ ! ${EBOX_X1:-} ]] || { [[ ${EBOX_X1:-} && ${EBOX_X2:-} ]] && | |
utils.string_xnor "${EBOX_Z1:-}" "${EBOX_Z2:-}"; } || | |
utils.die 'Invalid exclusion box. Specify: ex1,ex2,ez1,ez2 || ex1,ex2' | |
utils.string_xnor "${ORIGIN_X:-}" "${ORIGIN_Z:-}" || | |
utils.die 'Invalid origin coordinates. Specify: x,z' | |
# Check for valid arguments | |
mc_genmap.translate_coords | |
( ((BOX_X1 < BOX_X2 && BOX_Z1 < BOX_Z2)) && | |
( ((EBOX_X1 == EBOX_X2 && EBOX_Z1 == EBOX_Z2)) || | |
((EBOX_X1 < EBOX_X2 && EBOX_Z1 < EBOX_Z2)))) || | |
utils.die 'Invalid coordinates. Constraint: *x1 < *x2, *z1 < *z2' | |
( ((BOX_X2 - BOX_X1 >= 256 && BOX_Z2 - BOX_Z1 >= 256)) && | |
( ((EBOX_X2 - EBOX_X1 == 0 && EBOX_Z2 - EBOX_Z1 == 0)) || | |
((EBOX_X2 - EBOX_X1 >= 64 && EBOX_Z2 - EBOX_Z1 >= 64)))) || | |
utils.die 'Invalid box size. Constraint: BBOX > 256, EBOX > 64 || 0' | |
((64 <= HEIGHT && HEIGHT <= 255)) || | |
utils.die 'Invalid height. Constraint: 64 <= height <= 255' | |
((16 <= INTERVAL && INTERVAL <= (BOX_X2 - BOX_X1) / 2)) || | |
utils.die 'Invalid interval. Constraint: 16 <= interval <= (x2-x1)/2' | |
(((${SLEEP%%.*} > 0 || ${SLEEP##*.} > 0) && ${SLEEP%%.*} <= 10)) || | |
utils.die 'Invalid sleep. Constraint: 0 <= sleep <= 10' | |
# Check for functionality | |
[[ ${TYPE:-} != rcon ]] || python3 -c 'import rcon' 2>&- || | |
utils.die 'The python3 RCON module is needed but missing.' | |
[[ ${TYPE:-} != rcon ]] || _test=1 mc_genmap.rcon_cmd '/help' || | |
utils.die 'Error during RCON test. Check your settings.' | |
[[ ${TYPE:-} != screen ]] || command -V screen &>/dev/null || | |
utils.die 'The screen binary is needed but missing.' | |
[[ ! ${WD_JOURNAL:-} ]] || command -V journalctl &>/dev/null || | |
utils.die 'The journalctl binary is needed but missing.' | |
} | |
mc_genmap.exec_die() { | |
(($? == 130)) && return | |
[[ ! ${_test:-} ]] || return | |
utils.die "Error in $TYPE command, aborting." | |
} | |
mc_genmap.screen_start() { | |
[[ ! ${MOVE:-} || $TYPE != screen || ! ${START:-} ]] && return | |
screen -ls "$SNAME" >&- && return | |
utils.say "Launching server with command: '$COMMAND'" | |
screen -dmS "$SNAME" -- sh -c "$COMMAND" || TYPE=launch mc_genmap.exec_die | |
utils.say 'Server launched.' | |
[[ ${WD_RUN:-} ]] || { | |
utils.say 'Press any key when player is logged in.' | |
read -r -s -n 1 && return | |
} && { | |
utils.say 'Waiting for player to login.' | |
kill -STOP $BASHPID | |
} | |
} | |
mc_genmap.screen_cmd() { | |
(($# == 1)) || return | |
screen -S "$SNAME" -X stuff "$1\n" 2>&- || mc_genmap.exec_die | |
} | |
mc_genmap.rcon_cmd() { | |
(($# == 1)) || return | |
[[ ${PASSFILE:-} ]] && read -r PASS <"$PASSFILE" && export PASS=${PASS##*=} | |
python3 <(printf '%s' "$_mc_genmap_rcon") "$1" 2>&- || mc_genmap.exec_die | |
} | |
mc_genmap.exec_cmd() { | |
(($# == 1)) || return | |
[[ ${MOVE:-} ]] && { | |
[[ $TYPE == screen ]] && mc_genmap.screen_cmd "$1" || : | |
[[ $TYPE == rcon ]] && mc_genmap.rcon_cmd "$1" || : | |
} || printf '%s\n' "$1" >>"${OUTPUT:-/dev/stdout}" | |
} | |
mc_genmap.move_player() { | |
(($# == 0 || $# == 3)) || return | |
[[ ${MOVE:-} && $# -eq 3 ]] && utils.say "Step: $steps, Coords: ($x, $z)" | |
mc_genmap.exec_cmd "$( | |
printf '/execute as %s in minecraft:%s run tp %s %s %s %s' \ | |
"$PLAYER" "$DIMENSION" "$PLAYER" "${1:-~}" "${2:-~}" "${3:-~}" | |
)" | |
[[ ${MOVE:-} && $# -eq 3 ]] && { | |
utils.sleep "$SLEEP" && mc_genmap.at_z_edge && utils.sleep "$SLEEP" | |
((SECONDS % 3600 <= ${SLEEP%%.*})) && utils.say 'Autosaving.' && | |
mc_genmap.exec_cmd '/save-all' | |
} || return 0 | |
} | |
mc_genmap.outside_ebox() { | |
((x < EBOX_X1 || x > EBOX_X2 || z < EBOX_Z1 || z > EBOX_Z2)) | |
} | |
mc_genmap.at_z_edge() { | |
((z == BOX_Z1 || BOX_Z2 - z < INTERVAL)) || | |
( ((EBOX_X1 <= x && x <= EBOX_X2)) && | |
( (((EBOX_Z1 > z ? EBOX_Z1 - z : z - EBOX_Z1) <= INTERVAL)) || | |
(((EBOX_Z2 > z ? EBOX_Z2 - z : z - EBOX_Z2) <= INTERVAL)))) | |
} | |
mc_genmap.walk_grid() { | |
local -i steps=0 x z | |
utils.say 'Starting movement grid generation.' | |
mc_genmap.screen_start | |
mc_genmap.exec_cmd "/gamemode spectator $PLAYER" | |
utils.say "Moving to correct dimension: $DIMENSION" | |
mc_genmap.move_player | |
((${RESUME:=0} > 0)) && utils.say "Resuming generation from step: $RESUME" | |
for ((x = BOX_X1; x <= BOX_X2; x += INTERVAL)); do | |
for ((z = BOX_Z1; z <= BOX_Z2; z += INTERVAL)); do | |
mc_genmap.outside_ebox || continue | |
((++steps >= RESUME)) || continue | |
mc_genmap.move_player "$x" "$HEIGHT" "$z" | |
done | |
done | |
utils.say 'Grid walk complete, returning to origin.' | |
mc_genmap.move_player | |
utils.say \ | |
"Total steps: $steps, Total time: $(utils.seconds_to_hms $SECONDS)" | |
[[ ${STOP:-} ]] && utils.say 'Stopping the server.' && | |
mc_genmap.exec_cmd '/stop' | |
utils.say 'Generation complete.' | |
} | |
mc_genmap.governor() { | |
(($# == 1)) || return | |
utils.say 'Launching watchdog governor.' | |
[[ ${WD_JOURNAL:-} ]] && local cmd="journalctl -n 1 -o cat -fu $WD_JOURNAL" | |
[[ ${WD_LOGFILE:-} ]] && local cmd="tail -Fn 1 $WD_LOGFILE" | |
$cmd 2>&1 | python3 <(printf '%s' "$_mc_genmap_watchdog") "$1" "$PLAYER" || | |
TYPE=watchdog mc_genmap.exec_die | |
} | |
mc_genmap.generate() { | |
mc_genmap.set_globals | |
mc_genmap.parse_args "$@" | |
mc_genmap.validate_args | |
[[ ${WD_RUN:-} ]] || { | |
mc_genmap.walk_grid | |
return | |
} && { | |
trap '2>&- pkill -2 -P $(2>&- jobs -p %2)' EXIT INT TERM | |
mc_genmap.walk_grid & | |
mc_genmap.governor "$(jobs -p %1)" & | |
wait %1 | |
} | |
} | |
# Only execute if not being sourced | |
[[ ${BASH_SOURCE[0]} == "$0" ]] || return 0 && mc_genmap.generate "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
MC Map Generator TODO
Refactor generate_map to split command and move modesCheck if exclusion box outside of bounding boxAllow different x and z intervals, pause on new columnAlternate resume with coordinates instead of steputils.say -e
utils.say -h
Implement generic check and add rcon_checkSet watchdog (and grid_walk) to guard on INTERNALset -euo pipefail; shopt -s extdebug
kill -STOP -##
)kill -TERM %2
/save-all
on completion and every n steps.player[,dim]:[y]:x1,x2[,z1,z2]:[ex1,ex2[,ez1,ez2]]:[ox,oz]:[int]:[sleep]:[resume]
AfroThundr,overworld::-5000,5000:::100:10:
-br <num> [-er <num>]
This would mean allowing specifying resume coords.
-c1 <x1,z1> -c2 <x2,z2> [-ec1 <ex1,ez1> -ec2 <ex2,ez2>]