Files
minecraft-ai-god/scripts/shrink_godkit.py
T
Seth 8ee8be9cc0 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
2026-03-15 19:02:16 -04:00

162 lines
5.9 KiB
Python

#!/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