7a31e500e4
Prod deployment: - paper-ai and shrink-world switched from gemma3n:e4b to qwen3.5:9b - Error correction: detects RCON errors (<--[HERE]), asks model to fix, retries - Broadened error patterns: Unknown game mode, Unknown enchantment, etc. - Fixed fire fallback matching "firework" as fire intent - Fixed command format examples (WRONG vs RIGHT in prompt) - max_tokens bumped to 600 for command calls - Removed template workflow commands from sudo prompt Dev server: - Gemini 2.5 Flash ($0.15/$0.60 per M tokens) replaces Flash Lite - 10 bots for ~$1-1.5/hr training data generation - Dynamic pricing by model name in cost tracker Branding: - Rajdhani Bold as official Mortdecai font - Logo variants: mortdecai + mortdec.ai in 6 fonts - Whitelist page updated with Mortdecai branding + mortdec.ai domain Whitelist UUID fix: - Looks up real Mojang UUID via api.mojang.com - Patches all whitelist.json files directly - No more offline-mode UUID mismatches WorldEdit schematics: - 77 schematics installed (villages, bridges, lighthouses, parks, etc.) Mortdecai v4 training in progress: 63% complete on steel141 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
321 lines
13 KiB
Python
321 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Minecraft whitelist web app — lightweight self-service whitelisting.
|
|
Sethian Dark theme. minecraft.sethpc.xyz
|
|
"""
|
|
|
|
import html
|
|
import json
|
|
import os
|
|
import re
|
|
import socket
|
|
import struct
|
|
import time
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
from urllib.parse import parse_qs
|
|
|
|
PORT = 8099
|
|
INVITE_KEY = os.environ.get("INVITE_KEY", "REDACTED_INVITE_KEY")
|
|
|
|
SERVERS = [
|
|
{"name": "Paper AI", "host": "127.0.0.1", "rcon_port": 25577, "rcon_pass": "REDACTED_RCON", "show": True, "address": "sethpc.xyz:25567", "desc": "Full AI stack — pray to God, sudo commands, divine interventions"},
|
|
{"name": "Shrink World", "host": "127.0.0.1", "rcon_port": 25576, "rcon_pass": "REDACTED_RCON", "show": True, "address": "sethpc.xyz:25566", "desc": "Survival challenge — border shrinks on death, 5x creepers, pray for help"},
|
|
{"name": "Vanilla", "host": "127.0.0.1", "rcon_port": 25575, "rcon_pass": "REDACTED_RCON", "show": False, "address": None, "desc": None},
|
|
]
|
|
|
|
WHITELIST_LOG = "/var/log/mc_whitelist.log"
|
|
|
|
LOGO_URL = "https://git.sethpc.xyz/Seth/Mortdecai/raw/branch/master/branding/mortdec_ai_rajdhani.png"
|
|
|
|
|
|
def rcon_command(cmd, host, port, password):
|
|
try:
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
s.settimeout(5)
|
|
s.connect((host, port))
|
|
def send_packet(req_id, ptype, payload):
|
|
data = struct.pack("<ii", req_id, ptype) + payload.encode("utf-8") + b"\x00\x00"
|
|
s.sendall(struct.pack("<i", len(data)) + data)
|
|
def recv_packet():
|
|
raw = s.recv(4)
|
|
if len(raw) < 4: return None
|
|
length = struct.unpack("<i", raw)[0]
|
|
data = s.recv(length)
|
|
return data[8:-2].decode("utf-8", errors="replace")
|
|
send_packet(1, 3, password)
|
|
time.sleep(0.1)
|
|
recv_packet()
|
|
send_packet(2, 2, cmd)
|
|
time.sleep(0.2)
|
|
result = recv_packet()
|
|
s.close()
|
|
return result or ""
|
|
except Exception as e:
|
|
return f"ERROR: {e}"
|
|
|
|
|
|
def lookup_mojang_uuid(username):
|
|
"""Look up a player's real Mojang UUID from the API."""
|
|
try:
|
|
import urllib.request
|
|
url = f"https://api.mojang.com/users/profiles/minecraft/{username}"
|
|
req = urllib.request.Request(url, headers={"User-Agent": "MortdecaiWhitelist/1.0"})
|
|
resp = urllib.request.urlopen(req, timeout=5)
|
|
data = json.loads(resp.read())
|
|
raw = data.get("id", "")
|
|
if len(raw) == 32:
|
|
# Format as UUID with dashes
|
|
return f"{raw[:8]}-{raw[8:12]}-{raw[12:16]}-{raw[16:20]}-{raw[20:]}"
|
|
return raw
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def whitelist_player(username):
|
|
"""Whitelist player on all servers, fixing UUIDs to real Mojang UUIDs."""
|
|
results = {}
|
|
|
|
# Get real UUID from Mojang
|
|
uuid = lookup_mojang_uuid(username)
|
|
|
|
for srv in SERVERS:
|
|
# RCON whitelist add (creates entry, possibly with wrong offline UUID)
|
|
result = rcon_command(f"whitelist add {username}", srv["host"], srv["rcon_port"], srv["rcon_pass"])
|
|
results[srv["name"]] = result.strip()
|
|
|
|
# Fix UUIDs in whitelist.json files directly
|
|
if uuid:
|
|
WHITELIST_FILES = [
|
|
"/opt/paper-ai-25567/whitelist.json",
|
|
"/opt/mcsmanager/daemon/data/InstanceData/shrinkborder1234567890abcdef12345/whitelist.json",
|
|
"/opt/mcsmanager/daemon/data/InstanceData/d39f55861cb34204a92a18a9e1c78ca6/whitelist.json",
|
|
]
|
|
for wl_path in WHITELIST_FILES:
|
|
try:
|
|
with open(wl_path) as f:
|
|
wl = json.load(f)
|
|
changed = False
|
|
for entry in wl:
|
|
if entry["name"].lower() == username.lower() and entry["uuid"] != uuid:
|
|
entry["uuid"] = uuid
|
|
entry["name"] = username
|
|
changed = True
|
|
if changed:
|
|
with open(wl_path, "w") as f:
|
|
json.dump(wl, f, indent=2)
|
|
except Exception:
|
|
pass
|
|
|
|
# Reload whitelists
|
|
for srv in SERVERS:
|
|
try:
|
|
rcon_command("whitelist reload", srv["host"], srv["rcon_port"], srv["rcon_pass"])
|
|
except Exception:
|
|
pass
|
|
|
|
return results
|
|
|
|
|
|
def is_valid_username(name):
|
|
return bool(re.match(r'^[a-zA-Z0-9_]{3,16}$', name))
|
|
|
|
|
|
PAGE = """<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Mortdecai — Minecraft AI</title>
|
|
<style>
|
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
background: #1a1a1a; color: #e0e0e0; min-height: 100vh; }}
|
|
a {{ color: #D35400; text-decoration: none; }}
|
|
a:hover {{ color: #e65c00; }}
|
|
|
|
.header {{ background: #000; padding: 1rem; text-align: center; border-bottom: 2px solid #D35400; }}
|
|
.header img {{ height: 40px; vertical-align: middle; border: none; }}
|
|
.header span {{ font-size: 1.2rem; font-weight: 700; color: #D35400; margin-left: 0.5rem; vertical-align: middle; display: none; }}
|
|
|
|
.container {{ max-width: 600px; margin: 0 auto; padding: 1.5rem; }}
|
|
|
|
.hero {{ text-align: center; margin: 2rem 0 1.5rem; }}
|
|
.hero h1 {{ font-size: 1.6rem; color: #D35400; margin-bottom: 0.5rem; }}
|
|
.hero p {{ color: #ccc; font-size: 0.95rem; line-height: 1.5; }}
|
|
|
|
.mission {{ background: #252525; border: 1px solid #333; border-radius: 8px;
|
|
padding: 1.2rem; margin: 1.5rem 0; }}
|
|
.mission h2 {{ color: #D35400; font-size: 1rem; margin-bottom: 0.5rem; }}
|
|
.mission p {{ color: #ccc; font-size: 0.85rem; line-height: 1.5; margin-bottom: 0.5rem; }}
|
|
.mission ul {{ color: #ccc; font-size: 0.85rem; line-height: 1.6; padding-left: 1.2rem; }}
|
|
.mission code {{ background: #333; padding: 0.15rem 0.4rem; border-radius: 3px; color: #D35400; font-size: 0.85rem; }}
|
|
|
|
.card {{ background: #252525; border: 1px solid #333; border-radius: 8px;
|
|
padding: 1.5rem; margin: 1.5rem 0; }}
|
|
.card h2 {{ color: #D35400; font-size: 1.1rem; margin-bottom: 1rem; text-align: center; }}
|
|
|
|
label {{ display: block; font-size: 0.85rem; color: #aaa; margin-bottom: 0.3rem; margin-top: 1rem; }}
|
|
input {{ width: 100%; padding: 0.6rem; border-radius: 6px; border: 1px solid #444;
|
|
background: #2a2a2a; color: #fff; font-size: 1rem; }}
|
|
input:focus {{ outline: none; border-color: #D35400; box-shadow: 0 0 5px rgba(211,84,0,0.5); }}
|
|
|
|
button {{ width: 100%; padding: 0.7rem; border-radius: 6px; border: none;
|
|
background: #D35400; color: #fff; font-size: 1rem; font-weight: 600;
|
|
cursor: pointer; margin-top: 1.5rem; }}
|
|
button:hover {{ background: #e65c00; }}
|
|
|
|
.error {{ color: #ff6b6b; font-size: 0.85rem; margin-top: 0.5rem; text-align: center; }}
|
|
|
|
.success {{ background: #1a3a1a; border: 1px solid #2d7a2d; border-radius: 8px;
|
|
padding: 1.2rem; margin-top: 1.5rem; }}
|
|
.success h2 {{ color: #4caf50; font-size: 1.1rem; margin-bottom: 0.5rem; }}
|
|
|
|
.server {{ background: #252525; border: 1px solid #333; border-left: 3px solid #D35400;
|
|
border-radius: 6px; padding: 0.8rem; margin-top: 0.8rem; }}
|
|
.server .name {{ color: #D35400; font-weight: 600; font-size: 0.95rem; }}
|
|
.server .addr {{ font-family: monospace; color: #fff; font-size: 1.1rem; margin: 0.3rem 0;
|
|
background: #1a1a1a; padding: 0.3rem 0.6rem; border-radius: 4px; display: inline-block; }}
|
|
.server .desc {{ color: #999; font-size: 0.8rem; margin-top: 0.3rem; }}
|
|
|
|
.commands {{ background: #252525; border: 1px solid #333; border-radius: 8px;
|
|
padding: 1rem; margin-top: 1rem; }}
|
|
.commands h3 {{ color: #D35400; font-size: 0.9rem; margin-bottom: 0.5rem; }}
|
|
.cmd {{ font-family: monospace; background: #1a1a1a; padding: 0.2rem 0.5rem;
|
|
border-radius: 3px; color: #D35400; }}
|
|
.commands p {{ font-size: 0.8rem; color: #bbb; margin-bottom: 0.3rem; }}
|
|
|
|
.footer {{ text-align: center; color: #555; font-size: 0.75rem; margin: 2rem 0 1rem; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="header">
|
|
<img src="{logo}" alt="logo" class="noborder">
|
|
<span>mortdec.ai</span>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="hero">
|
|
<h1>Mortdecai</h1>
|
|
<p>An AI runs on this server that listens to in-game chat and does things in the world based on what you say.</p>
|
|
</div>
|
|
|
|
<div class="mission">
|
|
<h2>What Is This?</h2>
|
|
<p>There's an AI character playing <strong>God</strong> on the server. It runs on local hardware — no cloud, no OpenAI — using a small open-source model we're actively training.</p>
|
|
<p>Every interaction you have gets logged as training data to improve the model. The more you play, the smarter it gets.</p>
|
|
<h2 style="margin-top: 0.8rem;">What Can You Do?</h2>
|
|
<ul>
|
|
<li><code>pray <message></code> — Talk to God. Pray for items, smite your enemies, or say something offensive and get punished.</li>
|
|
<li><code>sudo <request></code> — Natural language commands. "sudo give me a diamond sword" just works.</li>
|
|
<li><code>bug_log <description></code> — Report when something goes wrong. Helps us fix the AI.</li>
|
|
</ul>
|
|
<h2 style="margin-top: 0.8rem;">What We Need</h2>
|
|
<p>Try to break it. Ask for weird things. Confuse it. Phrase things in ways nobody would expect. Every interaction — good or bad — makes the model better.</p>
|
|
</div>
|
|
|
|
{content}
|
|
|
|
<div class="footer">
|
|
Java Edition 1.21.x · Whitelisted · Hosted on a homelab
|
|
</div>
|
|
</div>
|
|
|
|
</body>
|
|
</html>"""
|
|
|
|
FORM = """
|
|
<div class="card">
|
|
<h2>Join the Server</h2>
|
|
<form method="POST">
|
|
<label>Minecraft Java Edition Username</label>
|
|
<input name="username" placeholder="Your username" required autofocus>
|
|
<label>Invite Key</label>
|
|
<input name="key" type="password" placeholder="Paste the key you were given" required>
|
|
<button type="submit">Get Whitelisted</button>
|
|
</form>
|
|
{error}
|
|
</div>
|
|
"""
|
|
|
|
def success_content(username, results):
|
|
servers = ""
|
|
for srv in SERVERS:
|
|
if not srv["show"]:
|
|
continue
|
|
servers += f"""
|
|
<div class="server">
|
|
<div class="name">{srv['name']}</div>
|
|
<div class="addr">{srv['address']}</div>
|
|
<div class="desc">{srv['desc']}</div>
|
|
</div>"""
|
|
|
|
return f"""
|
|
<div class="success">
|
|
<h2>Welcome, {html.escape(username)}!</h2>
|
|
<p style="color:#ccc; font-size:0.9rem;">You're whitelisted. Add these servers in Minecraft:</p>
|
|
{servers}
|
|
</div>
|
|
|
|
<div class="commands">
|
|
<h3>Quick Start</h3>
|
|
<p><span class="cmd">pray lord give me tools</span> — ask God for help</p>
|
|
<p><span class="cmd">sudo give me a diamond sword</span> — direct command translation</p>
|
|
<p><span class="cmd">sudo set time to day</span> — world commands work too</p>
|
|
<p><span class="cmd">bug_log it gave me the wrong item</span> — report issues</p>
|
|
</div>"""
|
|
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def log_message(self, fmt, *args):
|
|
pass
|
|
|
|
def do_GET(self):
|
|
content = FORM.format(error="")
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "text/html")
|
|
self.end_headers()
|
|
self.wfile.write(PAGE.format(content=content, logo=LOGO_URL).encode())
|
|
|
|
def do_POST(self):
|
|
length = int(self.headers.get("Content-Length", 0))
|
|
body = self.rfile.read(length).decode()
|
|
params = parse_qs(body)
|
|
username = params.get("username", [""])[0].strip()
|
|
key = params.get("key", [""])[0].strip()
|
|
|
|
if key != INVITE_KEY:
|
|
content = FORM.format(error='<p class="error">Invalid invite key.</p>')
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "text/html")
|
|
self.end_headers()
|
|
self.wfile.write(PAGE.format(content=content, logo=LOGO_URL).encode())
|
|
return
|
|
|
|
if not is_valid_username(username):
|
|
content = FORM.format(error='<p class="error">Invalid username. 3-16 characters, letters/numbers/underscore only.</p>')
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "text/html")
|
|
self.end_headers()
|
|
self.wfile.write(PAGE.format(content=content, logo=LOGO_URL).encode())
|
|
return
|
|
|
|
results = whitelist_player(username)
|
|
try:
|
|
with open(WHITELIST_LOG, "a") as f:
|
|
f.write(json.dumps({"time": time.strftime("%Y-%m-%dT%H:%M:%SZ"), "username": username, "results": results}) + "\n")
|
|
except:
|
|
pass
|
|
|
|
content = success_content(username, results)
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "text/html")
|
|
self.end_headers()
|
|
self.wfile.write(PAGE.format(content=content, logo=LOGO_URL).encode())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print(f"Whitelist app running on port {PORT}")
|
|
print(f"Invite key: {INVITE_KEY}")
|
|
HTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
|