324 lines
10 KiB
Python
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)")
|