126 lines
4.9 KiB
Bash
Executable File
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
|