Initial commit — Minecraft AI God plugin
- mc_aigod.py: main watcher script (log tail, RCON, two-call LLM) - Two-call LLM split: qwen3-coder:30b for commands, gemma3:12b for messages - Divine intervention timer (Poisson process, configurable avg/day) - Prayer memory (persistent, last 10 exchanges) - Rolling server log context (last 20 min events) - Live player context (inventory with rarity, health, food, pos, XP) - /pray and bible chat detection (no slash — vanilla 1.21 compatible) - Login notice, bible help system - debug_commands toggle (in-game command display via tellraw) - Auto-fix for transposed give command syntax - JSON repair fallback for truncated LLM responses - Sentence-aware message chunking for long responses - mc-aigod.service systemd unit - mc_aigod_shrink.json example config - README.md full implementation guide - Minecraft_Ai_God.md full design document
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Minecraft God Mode Watcher (mc1 - slingshooter08)
|
||||
After=mcsm-daemon.service
|
||||
Requires=mcsm-daemon.service
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/mc_godmode_watch.sh
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Minecraft Shrink World Auto Kit on Join
|
||||
After=mcsm-daemon.service
|
||||
Requires=mcsm-daemon.service
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/shrink_godkit.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Sets slingshooter08 to creative mode on server 1 (mc1).
|
||||
Called by mc_godmode_watch.sh whenever slingshooter08 joins.
|
||||
|
||||
Deployed to: /usr/local/bin/mc_godmode_rcon.py on CT 644
|
||||
"""
|
||||
import socket, struct, time, sys
|
||||
|
||||
PLAYER = 'slingshooter08'
|
||||
|
||||
def rcon(cmd, host='127.0.0.1', port=25575, password='REDACTED_RCON'):
|
||||
s = socket.socket()
|
||||
s.settimeout(5)
|
||||
s.connect((host, port))
|
||||
def pkt(i, t, p):
|
||||
p = p.encode() + b'\x00\x00'
|
||||
return struct.pack('<iii', len(p)+8, i, t) + p
|
||||
s.sendall(pkt(1, 3, password))
|
||||
time.sleep(0.2)
|
||||
s.recv(4096)
|
||||
s.sendall(pkt(2, 2, cmd))
|
||||
time.sleep(0.2)
|
||||
r = s.recv(4096)
|
||||
s.close()
|
||||
return r[12:-2].decode()
|
||||
|
||||
print(rcon(f'gamemode creative {PLAYER}'))
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Watches mc1 log for slingshooter08 joining and applies creative mode.
|
||||
# Deployed to: /usr/local/bin/mc_godmode_watch.sh on CT 644
|
||||
# Managed by: mc-godmode.service
|
||||
|
||||
LOG=/opt/mcsmanager/daemon/data/InstanceData/d39f55861cb34204a92a18a9e1c78ca6/logs/latest.log
|
||||
PLAYER=slingshooter08
|
||||
|
||||
tail -F "$LOG" | grep --line-buffered "joined the game" | while read line; do
|
||||
if echo "$line" | grep -qi "$PLAYER"; then
|
||||
sleep 1
|
||||
python3 /usr/local/bin/mc_godmode_rcon.py
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Shrink-world server watcher:
|
||||
- Gives full kit to every player on FIRST login only
|
||||
- Sends stats on join, on death, and every hour
|
||||
- Tracks total deaths and current world border size
|
||||
|
||||
Deployed to: /usr/local/bin/shrink_godkit.py on CT 644
|
||||
Managed by: mc-shrink-kit.service
|
||||
"""
|
||||
import socket, struct, time, sys, subprocess, re, threading, json, os
|
||||
|
||||
LOG = '/opt/mcsmanager/daemon/data/InstanceData/shrinkborder1234567890abcdef12345/logs/latest.log'
|
||||
RCON_HOST = '127.0.0.1'
|
||||
RCON_PORT = 25576
|
||||
RCON_PASS = 'REDACTED_RCON'
|
||||
KIT_RECORD = '/opt/mcsmanager/daemon/data/InstanceData/shrinkborder1234567890abcdef12345/kit_given.json'
|
||||
|
||||
total_deaths = 0
|
||||
|
||||
def rcon(cmd):
|
||||
try:
|
||||
s = socket.socket()
|
||||
s.settimeout(5)
|
||||
s.connect((RCON_HOST, RCON_PORT))
|
||||
def pkt(i, t, p):
|
||||
p = p.encode() + b'\x00\x00'
|
||||
return struct.pack('<iii', len(p)+8, i, t) + p
|
||||
s.sendall(pkt(1, 3, RCON_PASS))
|
||||
time.sleep(0.2)
|
||||
s.recv(4096)
|
||||
s.sendall(pkt(2, 2, cmd))
|
||||
time.sleep(0.2)
|
||||
r = s.recv(4096)
|
||||
s.close()
|
||||
return r[12:-2].decode()
|
||||
except Exception as e:
|
||||
print(f'RCON error: {e}', file=sys.stderr)
|
||||
return ''
|
||||
|
||||
def load_kit_record():
|
||||
if os.path.exists(KIT_RECORD):
|
||||
with open(KIT_RECORD) as f:
|
||||
return set(json.load(f))
|
||||
return set()
|
||||
|
||||
def save_kit_record(players):
|
||||
with open(KIT_RECORD, 'w') as f:
|
||||
json.dump(list(players), f)
|
||||
|
||||
kit_given = load_kit_record()
|
||||
|
||||
def get_border_size():
|
||||
try:
|
||||
r = rcon('worldborder get')
|
||||
m = re.search(r'([\d.]+) block', r)
|
||||
if m:
|
||||
return float(m.group(1))
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
def get_online_players():
|
||||
try:
|
||||
r = rcon('list')
|
||||
m = re.search(r'There are (\d+) of a max of \d+ players online:(.*)', r)
|
||||
if m:
|
||||
count = int(m.group(1))
|
||||
names = [n.strip() for n in m.group(2).split(',') if n.strip()]
|
||||
return count, names
|
||||
except:
|
||||
pass
|
||||
return 0, []
|
||||
|
||||
def broadcast_stats(context=''):
|
||||
border = get_border_size()
|
||||
count, players = get_online_players()
|
||||
border_str = f'{border:.0f}x{border:.0f}' if border else 'unknown'
|
||||
player_str = ', '.join(players) if players else 'none'
|
||||
|
||||
lines = [
|
||||
f'tellraw @a ["",{{"text":"--- World Stats ---","color":"gold","bold":true}}]',
|
||||
f'tellraw @a ["",{{"text":"Border: ","color":"yellow"}},{{"text":"{border_str} blocks","color":"white"}}]',
|
||||
f'tellraw @a ["",{{"text":"Total Deaths: ","color":"yellow"}},{{"text":"{total_deaths}","color":"white"}}]',
|
||||
f'tellraw @a ["",{{"text":"Online ({count}): ","color":"yellow"}},{{"text":"{player_str}","color":"white"}}]',
|
||||
]
|
||||
if context:
|
||||
lines.insert(0, f'tellraw @a ["",{{"text":"{context}","color":"aqua","italic":true}}]')
|
||||
|
||||
for cmd in lines:
|
||||
rcon(cmd)
|
||||
time.sleep(0.05)
|
||||
|
||||
def give_kit(player):
|
||||
print(f'Giving kit to {player}')
|
||||
cmds = [
|
||||
f'gamemode survival {player}',
|
||||
f'give {player} netherite_helmet[enchantments={{protection:4,unbreaking:3,mending:1,respiration:3,aqua_affinity:1}}]',
|
||||
f'give {player} netherite_chestplate[enchantments={{protection:4,unbreaking:3,mending:1}}]',
|
||||
f'give {player} netherite_leggings[enchantments={{protection:4,unbreaking:3,mending:1}}]',
|
||||
f'give {player} netherite_boots[enchantments={{protection:4,unbreaking:3,mending:1,feather_falling:4,depth_strider:3}}]',
|
||||
f'give {player} netherite_sword[enchantments={{sharpness:5,unbreaking:3,mending:1,looting:3,fire_aspect:2,sweeping_edge:3}}]',
|
||||
f'give {player} bow[enchantments={{power:5,unbreaking:3,infinity:1,flame:1,punch:2}}]',
|
||||
f'give {player} netherite_pickaxe[enchantments={{efficiency:5,unbreaking:3,mending:1,fortune:3}}]',
|
||||
f'give {player} netherite_axe[enchantments={{efficiency:5,unbreaking:3,mending:1,sharpness:5}}]',
|
||||
f'give {player} arrow 64',
|
||||
f'give {player} golden_apple 64',
|
||||
f'give {player} totem_of_undying 4',
|
||||
f'give {player} ender_pearl 16',
|
||||
f'give {player} cooked_beef 64',
|
||||
f'tellraw {player} ["",{{"text":"Welcome to the Shrinking World! ","color":"gold","bold":true}},{{"text":"You have been given a full kit. Good luck!","color":"yellow"}}]',
|
||||
]
|
||||
for cmd in cmds:
|
||||
result = rcon(cmd)
|
||||
print(f' {result}')
|
||||
time.sleep(0.05)
|
||||
|
||||
def hourly_broadcast():
|
||||
while True:
|
||||
time.sleep(3600)
|
||||
print('Sending hourly stats...')
|
||||
broadcast_stats('[ Hourly Update ]')
|
||||
|
||||
t = threading.Thread(target=hourly_broadcast, daemon=True)
|
||||
t.start()
|
||||
|
||||
proc = subprocess.Popen(['tail', '-F', LOG], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
|
||||
|
||||
print('Shrink-world watcher started.')
|
||||
|
||||
for line in (proc.stdout or []):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Player joined
|
||||
join_match = re.search(r'\[Server thread/INFO\].*?(\w+) joined the game', line)
|
||||
if join_match:
|
||||
player = join_match.group(1)
|
||||
print(f'JOIN: {player}')
|
||||
time.sleep(1)
|
||||
if player.lower() not in kit_given:
|
||||
give_kit(player)
|
||||
kit_given.add(player.lower())
|
||||
save_kit_record(kit_given)
|
||||
else:
|
||||
print(f' Kit already given to {player}, skipping')
|
||||
rcon(f'tellraw {player} ["",{{"text":"Welcome back, {player}!","color":"gold"}}]')
|
||||
time.sleep(0.5)
|
||||
broadcast_stats(f'{player} joined the game')
|
||||
continue
|
||||
|
||||
# Player died
|
||||
death_match = re.search(r'\[Server thread/INFO\]: (\w+) (died|was slain|was shot|drowned|fell|burned|blew up|suffocated|starved|was killed|hit the ground|went up in flames|tried to swim in lava|walked into a cactus|was poked|was squashed|was struck by lightning)', line)
|
||||
if death_match:
|
||||
player = death_match.group(1)
|
||||
total_deaths += 1
|
||||
print(f'DEATH: {player} (total: {total_deaths})')
|
||||
time.sleep(0.5)
|
||||
broadcast_stats(f'{player} died!')
|
||||
continue
|
||||
Reference in New Issue
Block a user