Files
timmy-home/setup_bezalel_evennia.py

324 lines
10 KiB
Python

#!/usr/bin/env python3
"""
Bezalel Evennia Setup — Provision Evennia on the resurrected Bezalel VPS (104.131.15.18)
This script:
1. SSHes into Bezalel VPS
2. Installs Evennia in /root/wizards/bezalel/evennia/
3. Creates "The Tower" world (matching Timmy's world on Mac)
4. Configures it to listen on ports 4100 (telnet), 4101 (web), 4102 (websocket)
5. Creates the bridge daemon to sync with Mac world
6. Sets up systemd service for Evennia
This makes Bezalel's house a full Evennia world node in the federation.
"""
import subprocess, json, os, sys
BEZALEL_IP = "104.131.15.18"
def ssh(cmd, timeout=30):
"""Run command on Bezalel VPS."""
r = subprocess.run(
['ssh', '-o', 'ConnectTimeout=5', '-o', 'StrictHostKeyChecking=no',
f'root@{BEZALEL_IP}', cmd],
capture_output=True, text=True, timeout=timeout
)
return r.stdout.strip(), r.stderr.strip(), r.returncode
def run_remote(script, timeout=120):
"""Run a complex script on Bezalel VPS."""
# Write script to remote, execute it, clean up
write_cmd = f"cat > /tmp/setup_evennia.sh << 'SCRIPTEND'\n{script}\nSCRIPTEND"
out, err, rc = ssh(write_cmd)
if rc != 0:
print(f"Failed to write script: {err}")
return False
out, err, rc = ssh('bash /tmp/setup_evennia.sh', timeout=timeout)
ssh('rm -f /tmp/setup_evennia.sh')
return out, err, rc
print("=" * 60)
print("BEZALEL EVENNIA SETUP")
print("=" * 60)
# Step 1: Create the Evennia world directory structure
setup_script = """#!/bin/bash
set -e
echo "=== Setting up Evennia on Bezalel ==="
# Create workspace
mkdir -p /root/wizards/bezalel/evennia
cd /root/wizards/bezalel/evennia
# Create Python virtualenv
if [ ! -d "venv" ]; then
echo "Creating virtualenv..."
python3 -m venv venv
fi
# Activate and install Evennia
source venv/bin/activate
pip install --upgrade pip
pip install evennia>=2.0.0
# Initialize Evennia world
if [ ! -d "bezalel_world" ]; then
echo "Initializing bezalel_world..."
evennia --init bezalel_world
fi
cd bezalel_world
# Create game settings that allow external connections
echo "
# Bezalel Evennia World
# Ports: 4100 (telnet), 4101 (web), 4102 (websocket)
SERVERNAME = bezalel_world
WEBSERVER_PORTS = [(4101, None)]
WEBSOCKET_SECONDARY_PORT = 4102
" >> server/conf/settings.py
# Start Evennia
evennia init
evennia migrate
evennia start
echo "=== Evennia installed and started ==="
evennia status
"""
out, err, rc = run_remote(setup_script, timeout=300)
print(f"\nStep 1: Install and start Evennia")
print(f" stdout: {out[:500] if out else 'empty'}")
print(f" stderr: {err[:500] if err else 'empty'}")
print(f" rc: {rc}")
if rc == 0:
# Step 2: Create rooms
room_setup = """
source /root/wizards/bezalel/evennia/venv/bin/activate
cd /root/wizards/bezalel/evennia/bezalel_world
# Create rooms via Python script
python3 << 'PYEOF'
import os, sys
os.environ['DJANGO_SETTINGS_MODULE'] = 'server.conf.settings']
sys.path.insert(0, '/root/wizards/bezalel/evennia/bezalel_world')
import django
django.setup()
from evennia import create_object
from evennia.objects.objects import DefaultRoom, DefaultExit
# Create rooms matching Timmy's world but with Bezalel flavor
rooms = {
"Limbo": "The void between worlds. You can feel the pulse of three houses: Mac, VPS, and this one.",
"Gatehouse": "A stone guard tower at the edge of the world. The walls are carved with runes of travel.",
"Great Hall": "A vast hall with a long table. Maps of the three houses hang on the walls.",
"The Library of Bezalel": "Shelves of technical manuals, evennia code, and bridge schematics.",
"The Observatory": "A tower room with telescopes pointing in three directions: Mac, VPS, and the wider net.",
"The Workshop": "A forge and workbench. This is where the bridge code is written.",
"The Server Room": "Racks of servers humming with energy. The heartbeat of Bezalel's house.",
"The Garden of Code": "A peaceful garden where ideas grow. Different from the Mac Garden — this one has code-shaped leaves.",
"The Portal Room": "Three shimmering doorways stand here: one to Mac (Timmy's world), one to VPS (The Wizard's Canon), and one to the wild net.",
}
# Create the Limbo room first
limbo, _ = create_object(DefaultRoom, "Limbo",
desc="The void between worlds. You can feel the pulse of three houses: Mac, VPS, and this one.")
for name, desc in rooms.items():
if name != "Limbo":
room, _ = create_object(DefaultRoom, name, desc=desc)
# Create exits
# Get all rooms
from evennia.objects.models import ObjectDB
all_rooms = ObjectDB.objects.filter(db_typeclass_path__contains="DefaultRoom")
print(f"Created {len(all_rooms)} rooms")
for room in all_rooms:
print(f" - {room.key}")
# Add exit to Limbo from every room
if room.key != "Limbo":
create_object(DefaultExit, "back to Limbo", location=room, destination=limbo)
print("Bezalel world set up successfully!")
PYEOF
"""
out2, err2, rc2 = run_remote(room_setup, timeout=60)
print(f"\nStep 2: Create rooms")
print(f" stdout: {out2[:1000] if out2 else 'empty'}")
print(f" stderr: {err2[:500] if err2 else 'empty'}")
print(f" rc: {rc2}")
# Step 3: Add bridge API and daemon
bridge_setup = """
mkdir -p /root/wizards/bezalel/evennia/bezalel_world/world
# Create bridge API (port 4103 to avoid conflicts)
cat > /root/wizards/bezalel/evennia/bezalel_world/world/bridge_api.py << 'APIEOF'
#!/usr/bin/env python3
"""Evennia Bridge API for Bezalel syncs with Mac and VPS worlds."""
import json
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
import time
BRIDGE_PORT = 4103
bridge_state = {
"world_name": "bezalel_world",
"characters": {},
"events": [],
"last_sync": None,
"bridge_active": False,
}
class BridgeHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/bridge/state':
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(bridge_state).encode())
elif self.path == '/bridge/health':
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({
"status": "ok",
"world": "bezalel_world",
"active": bridge_state["bridge_active"],
}).encode())
else:
self.send_response(404)
self.end_headers()
def do_POST(self):
if self.path == '/bridge/sync':
length = int(self.headers.get('Content-Length', 0))
data = json.loads(self.rfile.read(length))
bridge_state.get("characters", {}).update(data.get("characters", {}))
bridge_state.get("events", []).extend(data.get("events", []))
bridge_state["last_sync"] = time.time()
bridge_state["bridge_active"] = True
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({"status": "ok"}).encode())
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
pass
def start_bridge():
server = HTTPServer(("0.0.0.0", BRIDGE_PORT), BridgeHandler)
t = threading.Thread(target=server.serve_forever, daemon=True)
t.start()
print(f"Bezalel Bridge API on port {BRIDGE_PORT}")
return server
if __name__ == "__main__":
start_bridge()
import time
while True:
time.sleep(1)
APIEOF
cat > /root/wizards/bezalel/evennia/bezalel_world/world/bridge_daemon.py << 'DAEMEOF'
#!/usr/bin/env python3
"""Bezalel Bridge Daemon syncs with Mac and VPS worlds."""
import json
import time
import urllib.request
import urllib.error
MAC_BRIDGE = "http://100.124.176.28:4003" # Tailscale to Mac
VPS_BRIDGE = "http://100.126.61.75:4003" # Tailscale to VPS
def check_health(url):
try:
req = urllib.request.Request(f"{url}/bridge/health", timeout=10)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
except:
return None
def sync_state(url, data):
try:
req = urllib.request.Request(
f"{url}/bridge/sync",
data=json.dumps(data).encode(),
headers={"Content-Type": "application/json"},
method="POST",
timeout=10
)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
except Exception as e:
print(f"Sync to {url} failed: {e}")
return None
def main():
print("Bezalel Bridge Daemon started")
print(f"Mac bridge: {MAC_BRIDGE}")
print(f"VPS bridge: {VPS_BRIDGE}")
print()
state = {"world_name": "bezalel_world", "characters": {}, "events": []}
while True:
try:
# Check Mac
mac_h = check_health(MAC_BRIDGE)
vps_h = check_health(VPS_BRIDGE)
print(f"Mac: {'OK' if mac_h else 'DOWN'}")
print(f"VPS: {'OK' if vps_h else 'DOWN'}")
# Sync to whoever is up
if mac_h:
sync_state(MAC_BRIDGE, state)
print(f" Synced to Mac")
if vps_h:
sync_state(VPS_BRIDGE, state)
print(f" Synced to VPS")
print()
except Exception as e:
print(f"Error: {e}")
time.sleep(30)
if __name__ == "__main__":
main()
DAEMEOF
chmod +x /root/wizards/bezalel/evennia/bezalel_world/world/bridge_*.py
echo "Bezalel bridge scripts created"
"""
out3, err3, rc3 = run_remote(bridge_setup, timeout=30)
print(f"\nStep 3: Bridge scripts")
print(f" stdout: {out3[:500] if out3 else 'empty'}")
print(f" stderr: {err3[:500] if err3 else 'empty'}")
print(f" rc: {rc3}")
print(f"\nBezalel Evennia setup complete!")
print(f" Evennia: /root/wizards/bezalel/evennia/bezalel_world/")
print(f" Telnet: port 4000")
print(f" Web: port 4001")
print(f" WebSocket: port 4002")
print(f" Bridge API: port 4103")
print(f" Bridge Daemon: world/bridge_daemon.py")
else:
print(f"Bezalel setup not attempted (Evennia install failed or already exists)")