From 0556d0ca16ad02900100f8830ae6d3b8eaab3de0 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 18 Mar 2026 22:04:08 -0400 Subject: [PATCH] Port validator fixes, training audit, God Soul to shrink-world MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ported from mc_aigod_paper.py (vanilla-compatible, no Paper/plugins): - 5 validator fixes: @s→player, NBT enchant→components, strip invalid components, hallucinated effects, hallucinated commands - Training audit logger → /var/log/mc_training_audit_shrink.jsonl - God Soul document loading (/etc/god_soul.md) - Prayer title flash ("Your prayers have been answered!") Co-Authored-By: Claude Opus 4.6 (1M context) --- mc_aigod.py | 195 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 194 insertions(+), 1 deletion(-) diff --git a/mc_aigod.py b/mc_aigod.py index 0db522d..146079d 100644 --- a/mc_aigod.py +++ b/mc_aigod.py @@ -1373,9 +1373,169 @@ def fix_weather_command(cmd: str) -> str: log.warning(f"Fixed weather syntax: '{cmd}' -> '{fixed}'") return fixed +# --- Validator fixes (ported from mc_aigod_paper.py) --- + +def fix_at_s_selector(cmd: str, fallback_player: str) -> str: + if '@s' not in cmd: return cmd + fixed = cmd.replace('@s', fallback_player) + if fixed != cmd: log.warning(f"Fixed @s selector: '{cmd}' -> '{fixed}'") + return fixed + +def fix_nbt_enchantment_syntax(cmd: str) -> str: + if 'Enchantments' not in cmd: return cmd + m = re.match(r'^(give\s+\S+\s+)(minecraft:\w+)\{Enchantments:\[(.+?)\]\}(.*)$', cmd, re.I) + if m: + pre, item, body, rest = m.groups() + enchants = {} + for eid, lvl in re.findall(r'id:[\"\']?(?:minecraft:)?([a-z_]+)[\"\']?\s*,\s*lvl[\"\']?:?\s*[\"\']?(\d+)', body): + enchants[eid] = lvl + if enchants: + ench_str = ','.join(f'{k}:{v}' for k, v in enchants.items()) + fixed = f"{pre}{item}[enchantments={{{ench_str}}}]{rest}" + log.warning(f"Fixed NBT enchant: '{cmd}' -> '{fixed}'") + return fixed + m = re.match(r'^(give\s+\S+\s+)(?:minecraft:)?(\w+)\{Enchantments:\{(.+?)\}\}(.*)$', cmd, re.I) + if m: + pre, item, body, rest = m.groups() + enchants = {} + for eid, lvl in re.findall(r'(?:minecraft:)?([a-z_]+):(\d+)', body): + enchants[eid] = lvl + if enchants: + ench_str = ','.join(f'{k}:{v}' for k, v in enchants.items()) + fixed = f"{pre}minecraft:{item}[enchantments={{{ench_str}}}]{rest}" + log.warning(f"Fixed NBT enchant: '{cmd}' -> '{fixed}'") + return fixed + return cmd + +_INVALID_COMPONENTS = re.compile( + r',?\s*(?:display|durability|enc|Durability|Display|Lore|CustomModelData|HideFlags' + r'|Paper|Unbreakable|Displayname|CustomName)(?::\{[^}]*\}|=[^,\]}\s]+|:[^,\]}\s]+)', re.I) + +def fix_invalid_item_components(cmd: str) -> str: + if not cmd.startswith('give ') or '[' not in cmd: return cmd + m = re.search(r'(\[.+?\])', cmd) + if not m: return cmd + bracket = m.group(1) + cleaned = _INVALID_COMPONENTS.sub('', bracket) + cleaned = re.sub(r'\{,', '{', cleaned) + cleaned = re.sub(r',\}', '}', cleaned) + if cleaned in ('[]', '[,]'): cleaned = '' + if cleaned != bracket: + fixed = cmd[:m.start()] + cleaned + cmd[m.end():] + log.warning(f"Stripped invalid components: '{cmd}' -> '{fixed}'") + return fixed + return cmd + +VALID_EFFECTS = { + 'speed', 'slowness', 'haste', 'mining_fatigue', 'strength', 'instant_health', + 'instant_damage', 'jump_boost', 'nausea', 'regeneration', 'resistance', + 'fire_resistance', 'water_breathing', 'invisibility', 'blindness', 'night_vision', + 'hunger', 'weakness', 'poison', 'wither', 'health_boost', 'absorption', + 'saturation', 'glowing', 'levitation', 'luck', 'unluck', 'slow_falling', + 'conduit_power', 'dolphins_grace', 'bad_omen', 'hero_of_the_village', 'darkness', +} +EFFECT_ALIASES = { + 'invincibility': 'resistance', 'invulnerability': 'resistance', + 'invulnerable': 'resistance', 'invincible': 'resistance', + 'experience_boost': 'luck', 'slow_speed': 'slowness', + 'fire': 'fire_resistance', 'healing': 'instant_health', + 'damage': 'instant_damage', +} + +def fix_hallucinated_effect(cmd: str) -> str: + m = re.match(r'^(effect\s+give\s+\S+\s+minecraft:)(\w+)(.*)$', cmd) + if not m: return cmd + pre, effect, rest = m.groups() + if effect in VALID_EFFECTS: return cmd + replacement = EFFECT_ALIASES.get(effect) + if replacement: + fixed = f"{pre}{replacement}{rest}" + log.warning(f"Fixed hallucinated effect: '{effect}' -> '{replacement}'") + return fixed + return cmd + +COMMAND_ALIASES = { + r'^setworldborder\s+(\d+)': lambda m: f"worldborder set {m.group(1)}", + r'^setspawn\b': lambda m: "spawnpoint", + r'^heal\s+(.+)': lambda m: f"effect give {m.group(1)} minecraft:instant_health 1 1", + r'^commands?\b': lambda m: None, +} + +def fix_hallucinated_command(cmd: str) -> str: + for pattern, fixer in COMMAND_ALIASES.items(): + m = re.match(pattern, cmd, re.I) + if m: + result = fixer(m) + if result is None: + log.warning(f"Dropped hallucinated command: '{cmd}'") + return "" + log.warning(f"Fixed hallucinated command: '{cmd}' -> '{result}'") + return result + return cmd + + +# --- God Soul --- +_GOD_SOUL = "" +try: + with open(os.path.join(os.path.dirname(__file__) or ".", "god_soul.md")) as _f: + _GOD_SOUL = _f.read() +except FileNotFoundError: + try: + with open("/etc/god_soul.md") as _f: + _GOD_SOUL = _f.read() + except FileNotFoundError: + pass + + +# --- Training Audit --- +_audit_lock = threading.Lock() + +def write_training_audit(player, mode, user_message, commands_generated, + commands_executed, message, context, config, rcon_results=None): + audit_path = config.get("training_audit_path", "/var/log/mc_training_audit.jsonl") + server_ctx = { + "server_type": config.get("server_type", "vanilla"), + "version": "1.21.x", + "online_players": context.get("online_players", []), + } + entry = { + "timestamp": time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), + "source": "live_playtest", + "category": "command_gen", + "mode": mode, + "player": player, + "input": {"user_message": user_message, "server_context": server_ctx}, + "output": { + "commands_generated": commands_generated or [], + "commands_executed": commands_executed or [], + "message": message or "", + }, + "rcon_results": rcon_results or [], + "needs_review": True, + } + try: + parent = os.path.dirname(audit_path) + if parent: os.makedirs(parent, exist_ok=True) + with _audit_lock: + with open(audit_path, 'a', encoding='utf-8') as f: + f.write(json.dumps(entry, ensure_ascii=False) + '\n') + except Exception as e: + log.error(f"Training audit write failed: {e}") + + def validate_command(cmd, online_players, fallback_player, config=None): - """Replace placeholders, auto-fix common give syntax errors, check safe prefix.""" + """Replace placeholders, auto-fix common syntax errors, check safe prefix.""" resolved = cmd.replace("{player}", fallback_player).replace("{target}", fallback_player) + resolved = resolved.strip() + if resolved.startswith("/"): resolved = resolved[1:] + # New fixes + resolved = fix_hallucinated_command(resolved) + if not resolved: return "", False + resolved = fix_at_s_selector(resolved, fallback_player) + resolved = fix_nbt_enchantment_syntax(resolved) + resolved = fix_invalid_item_components(resolved) + resolved = fix_hallucinated_effect(resolved) + # Existing fixes resolved = fix_give_command(resolved) resolved = fix_effect_command(resolved) resolved = fix_weather_command(resolved) @@ -1433,6 +1593,20 @@ def execute_response(response, context, config, praying_player=None): ) time.sleep(0.2) + # Flash title on the praying player's screen + if praying_player and (commands or message): + try: + rcon( + f'title {praying_player} times 5 40 15', + config["rcon_host"], config["rcon_port"], config["rcon_password"] + ) + rcon( + f'title {praying_player} title {{"text":"Your prayers have been answered!","color":"gold","bold":true}}', + config["rcon_host"], config["rcon_port"], config["rcon_password"] + ) + except Exception: + pass + fallback = praying_player or (context["online_players"][0] if context["online_players"] else "") max_cmds = config.get("max_commands_per_response", 6) @@ -1680,6 +1854,16 @@ def process_sudo(player, prompt, config): add_sudo_history(player, prompt, commands, executed) + # Training audit + online = players_online(config) + write_training_audit( + player=player, mode="sudo", + user_message=f"sudo {prompt}", + commands_generated=commands, + commands_executed=executed, + message="", context={"online_players": online}, config=config, + ) + # --------------------------------------------------------------------------- # Prayer handler # --------------------------------------------------------------------------- @@ -1784,6 +1968,15 @@ def process_prayer(player, prayer, config, cooldowns): if god_msg: add_prayer_memory(player, prayer, god_msg, config) + # Training audit + write_training_audit( + player=player, mode="god", + user_message=f"pray {prayer}", + commands_generated=response.get("commands") or [], + commands_executed=response.get("commands") or [], + message=god_msg, context=context, config=config, + ) + # --------------------------------------------------------------------------- # Divine intervention timer # ---------------------------------------------------------------------------