create-mekanism-modular/tools/live-reload.sh

126 lines
4.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Push a placement change into the running game.
#
# live-reload.sh <saved-file>
#
# The mod jar is deliberately NOT touched. NeoForge holds every mod jar open for
# the whole session, and `cp` rewrites a file in place - same inode - so
# overwriting the jar under a running game invalidates its view of its own
# archive. The symptom is not a crash but modules quietly going missing:
#
# could not find module cmmodular:armor/socket/arm_left substituting with empty module
#
# ...and every part built on them vanishing off the wearer, with the jar on disk
# still passing `unzip -t`. Only a restart clears it.
#
# So the module JSON goes into the world's own datapack folder instead, which is
# read fresh on every `/reload` and loads after the mod's built-in packs, so the
# copy there wins. Editing is live and nothing is held open.
#
# Models and textures under `assets/` are resourcepack, not datapack, and are
# still read from the jar - those need a restart. The script says so rather than
# pretending F3+T reached them.
set -uo pipefail
REPO=/home/themiro/code.dev/create-mekanism-modular
INSTANCES=/home/themiro/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances
export YDOTOOL_SOCKET=${YDOTOOL_SOCKET:-/run/user/1000/.ydotool_socket}
SAVED=${1:-}
PY=$REPO/tools/.venv/bin/python
MCWIN="Minecraft NeoForge"
PACK=cmmodular-live
say() { printf '%s\n' "$*"; }
# `pgrep -f java.*minecraft` also matches a Gradle daemon - the word turns up in
# a classpath - so the instance is identified by the Prism path in the cmdline
# and nothing else. Guessing wrong here means reloading an instance nobody is
# looking at, or claiming the game is up when it is not.
INSTANCE=""
for p in $(pgrep -x java 2>/dev/null); do
c=$(tr '\0' ' ' < /proc/$p/cmdline 2>/dev/null)
case "$c" in
*PrismLauncher/instances/*)
INSTANCE=${c#*PrismLauncher/instances/}
INSTANCE=${INSTANCE%%/*}
break ;;
esac
done
[ -z "$INSTANCE" ] && { say "game not running (no Prism instance in any java process)"; exit 0; }
MC="$INSTANCES/$INSTANCE/minecraft"
WORLD=$(ls -t "$MC/saves" 2>/dev/null | head -1)
[ -z "$WORLD" ] && { say "no world in $INSTANCE"; exit 1; }
case "$SAVED" in
*"/assets/"*)
say "NOTE: $(basename "$SAVED") is a resourcepack file (models/textures)."
say " Those load from the jar and cannot be hot-swapped - restart to see it."
exit 0 ;;
esac
# Validate before shipping: a malformed module is dropped silently by MIAPI and
# looks exactly like the part being mispositioned.
if [ -n "$SAVED" ] && ! "$PY" -c "import json,sys;json.load(open(sys.argv[1]))" "$SAVED" 2>/tmp/live-json.err; then
say "INVALID JSON - not deploying"; sed 's/^/ /' /tmp/live-json.err; exit 1
fi
DEST="$MC/saves/$WORLD/datapacks/$PACK"
FIRST=0; [ -d "$DEST" ] || FIRST=1 # a pack added mid-session needs enabling
mkdir -p "$DEST"
cat > "$DEST/pack.mcmeta" <<'META'
{
"pack": {
"description": "cmmodular live placement edits - overrides the jar's modules while iterating",
"pack_format": 48
}
}
META
rm -rf "$DEST/data"
for p in "$REPO"/src/main/resources/packs/*/data; do
cp -r "$p/." "$DEST/data/" 2>/dev/null || { mkdir -p "$DEST/data"; cp -r "$p/." "$DEST/data/"; }
done
say "staged -> saves/$WORLD/datapacks/$PACK"
wmctrl -a "$MCWIN" >/dev/null 2>&1 || { say "no game window; datapack staged"; exit 0; }
sleep 0.6
menu_open() {
grim /tmp/live-reload-probe.png 2>/dev/null || return 1
"$PY" - <<'EOF'
import sys
import numpy as np
from PIL import Image
a=np.array(Image.open('/tmp/live-reload-probe.png').convert('RGB'))
h,w,_=a.shape
b=a[h//6:h//2, w//3:2*w//3]
g=((abs(b[...,0].astype(int)-b[...,1])<12)&(abs(b[...,1].astype(int)-b[...,2])<12)
&(b[...,0]>110)&(b[...,0]<210)).mean()
sys.exit(0 if g>0.15 else 1)
EOF
}
# One Escape closes chat, closes the menu, or opens it from gameplay; after it
# the only ambiguity left is whether the menu is up, which is checked. Without
# this a stray open chat eats the T and the command posts as literal "t/reload".
ydotool key 1:1 1:0; sleep 0.7
if menu_open; then ydotool key 1:1 1:0; sleep 0.7; fi
# QWERTZ: ydotool speaks US keycodes, so the two punctuation marks these
# commands need are sent by position - '/' is shift+7, '-' is where US puts '/'.
slash() { ydotool key 42:1 8:1 8:0 42:0; sleep 0.2; }
dash() { ydotool key 53:1 53:0; sleep 0.2; }
ydotool key 20:1 20:0; sleep 0.7 # T -> chat
slash
if [ "$FIRST" = 1 ]; then
# A datapack that appeared after the world was loaded is present but not
# enabled; `/reload` alone will not pick it up. Enabling reloads too.
ydotool type -d 28 "datapack enable file"; slash
ydotool type -d 28 "cmmodular"; dash; ydotool type -d 28 "live"
sleep 0.4; ydotool key 28:1 28:0; sleep 2.0
say "enabled datapack $PACK (first run this session)"
else
ydotool type -d 28 "reload"; sleep 0.4
ydotool key 28:1 28:0; sleep 1.5
say "/reload sent - $(basename "${SAVED:-all modules}") live"
fi