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
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
*.pyc
|
||||
__pycache__/
|
||||
GITEA_API.md
|
||||
aigod_memory.json
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
# Minecraft Quick Command Reference
|
||||
|
||||
## RCON — Send a command from CT 644
|
||||
|
||||
### Server 1 (mc1, port 25575)
|
||||
```bash
|
||||
ssh pve112 "pct exec 644 -- python3 -c \"
|
||||
import socket, struct, time
|
||||
def rcon(cmd):
|
||||
s = socket.socket(); s.settimeout(5); s.connect(('127.0.0.1', 25575))
|
||||
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,'REDACTED_RCON')); 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()
|
||||
print(rcon('YOUR COMMAND HERE'))
|
||||
\""
|
||||
```
|
||||
|
||||
### Server 2 (shrink-world, port 25576)
|
||||
Same as above but use port `25576` and password `REDACTED_RCON`.
|
||||
|
||||
---
|
||||
|
||||
## Player Commands
|
||||
|
||||
| Action | Command |
|
||||
|---|---|
|
||||
| Teleport player | `tp <player> <x> <y> <z>` |
|
||||
| Give item | `give <player> <item> <count>` |
|
||||
| Set gamemode | `gamemode creative/survival/spectator <player>` |
|
||||
| Get player position | `data get entity <player> Pos` |
|
||||
| List online players | `list` |
|
||||
| Get world border size | `worldborder get` |
|
||||
| Set world border | `worldborder set <size>` |
|
||||
| Shrink world border | `worldborder add -<amount> <seconds>` |
|
||||
|
||||
## God Kit (1.21 syntax)
|
||||
```
|
||||
give <player> netherite_helmet[enchantments={protection:4,unbreaking:3,mending:1,respiration:3,aqua_affinity:1}]
|
||||
give <player> netherite_chestplate[enchantments={protection:4,unbreaking:3,mending:1}]
|
||||
give <player> netherite_leggings[enchantments={protection:4,unbreaking:3,mending:1}]
|
||||
give <player> netherite_boots[enchantments={protection:4,unbreaking:3,mending:1,feather_falling:4,depth_strider:3}]
|
||||
give <player> netherite_sword[enchantments={sharpness:5,unbreaking:3,mending:1,looting:3,fire_aspect:2,sweeping_edge:3}]
|
||||
give <player> bow[enchantments={power:5,unbreaking:3,infinity:1,flame:1,punch:2}]
|
||||
give <player> netherite_pickaxe[enchantments={efficiency:5,unbreaking:3,mending:1,fortune:3}]
|
||||
give <player> netherite_axe[enchantments={efficiency:5,unbreaking:3,mending:1,sharpness:5}]
|
||||
give <player> arrow 64
|
||||
give <player> golden_apple 64
|
||||
give <player> totem_of_undying 4
|
||||
give <player> ender_pearl 16
|
||||
give <player> cooked_beef 64
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shrinkborder Datapack (in-game, op required)
|
||||
|
||||
| Command | Effect |
|
||||
|---|---|
|
||||
| `/function shrinkborder:enable` | Start death-shrinking |
|
||||
| `/function shrinkborder:disable` | Stop death-shrinking |
|
||||
| `/function shrinkborder:reset` | Reset border to 1000x1000, disable shrinking |
|
||||
|
||||
---
|
||||
|
||||
## Services on CT 644
|
||||
|
||||
```bash
|
||||
# Access CT
|
||||
ssh pve112 "pct exec 644 -- bash"
|
||||
|
||||
# God mode watcher (mc1 - slingshooter08 auto creative)
|
||||
systemctl start/stop/status mc-godmode.service
|
||||
|
||||
# Shrink world kit watcher (shrink-world - auto kit on first join + stats)
|
||||
systemctl start/stop/status mc-shrink-kit.service
|
||||
|
||||
# MCSManager itself
|
||||
systemctl start/stop/status mcsm-web.service
|
||||
systemctl start/stop/status mcsm-daemon.service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Datapacks — Deploy to shrink-world server
|
||||
|
||||
```bash
|
||||
# From this machine (CT 629), copy a datapack folder to the server
|
||||
DEST=/opt/mcsmanager/daemon/data/InstanceData/shrinkborder1234567890abcdef12345/world/datapacks/
|
||||
|
||||
# Then reload in-game or via RCON:
|
||||
# datapack enable "file/<packname>"
|
||||
# reload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Kit Record — Reset a player's first-join kit
|
||||
|
||||
```bash
|
||||
ssh pve112 "pct exec 644 -- cat /opt/mcsmanager/daemon/data/InstanceData/shrinkborder1234567890abcdef12345/kit_given.json"
|
||||
|
||||
# To reset a specific player (edit the JSON):
|
||||
ssh pve112 "pct exec 644 -- nano /opt/mcsmanager/daemon/data/InstanceData/shrinkborder1234567890abcdef12345/kit_given.json"
|
||||
```
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
# Sethpc Minecraft Project Context
|
||||
|
||||
## Infrastructure
|
||||
|
||||
### MCSManager Panel
|
||||
- **CT:** 644 on node-112 (sethpc, 192.168.0.112)
|
||||
- **IP:** 192.168.0.244
|
||||
- **Web panel:** http://mc.sethpc.xyz (via Caddy on CT 600, node-241)
|
||||
- **Panel ports:** 23333 (web), 24444 (daemon)
|
||||
- **OS:** Debian 12, Java 21 (Temurin)
|
||||
- **Installed via:** https://script.mcsmanager.com/setup.sh
|
||||
- **Systemd services:** `mcsm-web.service`, `mcsm-daemon.service`
|
||||
|
||||
---
|
||||
|
||||
## Server 1 — mc1 (Vanilla survival)
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Instance ID | `d39f55861cb34204a92a18a9e1c78ca6` |
|
||||
| Game port | `25565` |
|
||||
| RCON port | `25575` |
|
||||
| RCON password | `REDACTED_RCON` |
|
||||
| Data dir | `/opt/mcsmanager/daemon/data/InstanceData/d39f55861cb34204a92a18a9e1c78ca6/` |
|
||||
| Version | Minecraft 1.21.x (vanilla) |
|
||||
| Connect | `192.168.0.244:25565` |
|
||||
|
||||
### Services on CT 644
|
||||
- `mc-godmode.service` — watches `latest.log`, sets `slingshooter08` to creative on every login
|
||||
- Script: `/usr/local/bin/mc_godmode_watch.sh`
|
||||
- Calls: `/usr/local/bin/mc_godmode_rcon.py`
|
||||
- Toggle: `systemctl start/stop mc-godmode.service`
|
||||
|
||||
---
|
||||
|
||||
## Server 2 — shrink-world
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Instance ID | `shrinkborder1234567890abcdef12345` |
|
||||
| Game port | `25566` |
|
||||
| RCON port | `25576` |
|
||||
| RCON password | `REDACTED_RCON` |
|
||||
| Data dir | `/opt/mcsmanager/daemon/data/InstanceData/shrinkborder1234567890abcdef12345/` |
|
||||
| Version | Minecraft 1.21.x (vanilla, same server.jar as mc1) |
|
||||
| Connect | `192.168.0.244:25566` |
|
||||
| World border | 500x500 centered at 0,0 (started at 1000x1000) |
|
||||
| Difficulty | Hard |
|
||||
|
||||
### Datapacks installed
|
||||
1. **shrinkborder** — detects player deaths, shrinks world border by 1 block per death, alternating N/S and E/W sides. Starts DISABLED.
|
||||
2. **morespawns** — increases creeper spawn weight from 100 to 500 (5x more creepers)
|
||||
|
||||
### Services on CT 644
|
||||
- `mc-shrink-kit.service` — main watcher script for shrink-world
|
||||
- Script: `/usr/local/bin/shrink_godkit.py`
|
||||
- On first login: gives full god kit (netherite armor + tools + consumables)
|
||||
- On every login: broadcasts world stats
|
||||
- On every death: broadcasts world stats + increments death counter
|
||||
- Every hour: broadcasts world stats
|
||||
- Kit record persisted at: `<data_dir>/kit_given.json`
|
||||
|
||||
---
|
||||
|
||||
## RCON Helper (Python)
|
||||
|
||||
All server communication uses raw RCON sockets (no external library needed):
|
||||
|
||||
```python
|
||||
import socket, struct, time
|
||||
|
||||
def rcon(cmd, host='127.0.0.1', port=25575, password='REDACTED_RCON'):
|
||||
s = socket.socket()
|
||||
s.settimeout(5)
|
||||
s.connect((host, 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, password))
|
||||
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()
|
||||
```
|
||||
|
||||
For shrink-world use port `25576` and password `REDACTED_RCON`.
|
||||
|
||||
---
|
||||
|
||||
## Minecraft 1.21 Notes
|
||||
|
||||
- **Enchantment syntax** (1.21+): `give player item[enchantments={sharpness:5,unbreaking:3}]`
|
||||
- NOT the old NBT `{Enchantments:[{id:...,lvl:...}]}` format
|
||||
- **Death detection regex:** `(\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)`
|
||||
- **RCON** requires `enable-rcon=true` and `rcon.password=` set in `server.properties`, then server restart
|
||||
|
||||
---
|
||||
|
||||
## Players
|
||||
|
||||
| Username | UUID |
|
||||
|---|---|
|
||||
| slingshooter08 | 45374469-5333-43e1-9881-39ad6b5f3301 |
|
||||
|
||||
---
|
||||
|
||||
## Networking
|
||||
|
||||
- Both servers are LAN-only (`192.168.0.244`)
|
||||
- External access requires port forwarding on router: `25565` and `25566` → `192.168.0.244`
|
||||
- Web panel accessible via Caddy at `mc.sethpc.xyz`
|
||||
- DNS: Pi-hole at `192.168.0.153`
|
||||
+1307
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,314 @@
|
||||
# Minecraft AI God
|
||||
|
||||
A Python-based "plugin" for vanilla Minecraft servers that integrates a locally-hosted LLM as an in-game God character. Players interact by typing `pray <message>` in chat. God responds with dramatic prose and optionally executes server commands via RCON.
|
||||
|
||||
No mods, no plugins, no server restarts required. Works with any vanilla Minecraft 1.21+ server.
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
Player types: pray <message>
|
||||
│
|
||||
▼
|
||||
latest.log ←─ tailed by mc_aigod.py
|
||||
│
|
||||
▼
|
||||
RCON: immediate acknowledgment to player ("The heavens stir...")
|
||||
│
|
||||
▼
|
||||
RCON: fetch live server context
|
||||
- Online players, time of day, weather, world border
|
||||
│
|
||||
▼
|
||||
RCON: fetch praying player's state
|
||||
- Full inventory (with rarity annotations), position, health, food, XP, deaths
|
||||
│
|
||||
▼
|
||||
Call 1 — command_model (qwen3-coder:30b or similar)
|
||||
- Decides what server commands to execute (JSON only, no prose)
|
||||
- Low temperature (0.3) for precise structured output
|
||||
│
|
||||
▼
|
||||
Call 2 — model (gemma3:12b or similar)
|
||||
- Writes God's spoken message knowing what was decided
|
||||
- No token competition with commands — full creative freedom
|
||||
│
|
||||
▼
|
||||
RCON: execute commands + broadcast message
|
||||
```
|
||||
|
||||
**Divine Intervention Timer** — a background thread fires at random intervals (Poisson process, user-defined average per day). If players are online, God acts unprompted. LLM can choose silence (`commands: []`) and nothing happens.
|
||||
|
||||
**Memory** — last 10 prayer exchanges stored as conversation history and passed to every LLM call. Persists across service restarts via JSON file. God remembers.
|
||||
|
||||
**Server log context** — last 20 minutes of meaningful server events (chat, deaths, joins, leaves) included with every prayer. God knows what's been happening.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.11+
|
||||
- `requests` library (`apt install python3-requests`)
|
||||
- Ollama instance with at least one model pulled
|
||||
- Minecraft vanilla server with RCON enabled
|
||||
- Server running on Linux (systemd for service management)
|
||||
|
||||
### Minecraft server.properties requirements
|
||||
|
||||
```properties
|
||||
enable-rcon=true
|
||||
rcon.port=25575
|
||||
rcon.password=yourpassword
|
||||
broadcast-rcon-to-ops=false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
mc_aigod.py # Main script — deploy to /usr/local/bin/
|
||||
mc_aigod_shrink.json # Example config — deploy to /etc/mc_aigod.json
|
||||
mc-aigod.service # Systemd unit — deploy to /etc/systemd/system/
|
||||
Minecraft_Ai_God.md # Full design document with architecture details
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
`/etc/mc_aigod.json`:
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `server_name` | string | required | Server name passed to God's persona |
|
||||
| `log_path` | string | required | Absolute path to `logs/latest.log` |
|
||||
| `rcon_host` | string | `"127.0.0.1"` | RCON host |
|
||||
| `rcon_port` | int | `25575` | RCON port |
|
||||
| `rcon_password` | string | required | RCON password |
|
||||
| `ollama_url` | string | required | Ollama base URL e.g. `http://192.168.0.1:11434` |
|
||||
| `model` | string | required | Message model — creative writing (e.g. `gemma3:12b`) |
|
||||
| `command_model` | string | falls back to `model` | Commands model — structured JSON (e.g. `qwen3-coder:30b`) |
|
||||
| `temperature` | float | `0.85` | Message model temperature |
|
||||
| `max_tokens` | int | `600` | Max tokens for message call |
|
||||
| `cooldown_seconds` | int | `20` | Per-player prayer cooldown |
|
||||
| `max_commands_per_response` | int | `6` | Max commands God can issue per prayer |
|
||||
| `interventions_per_day` | float | `4` | Avg unprompted interventions per 24h. `0` to disable |
|
||||
| `debug_commands` | bool | `false` | Show executed commands in-game via dark gray tellraw |
|
||||
| `memory_path` | string | see below | Path to persist prayer memory JSON |
|
||||
| `god_chat_prefix` | string | `"[GOD]"` | Chat prefix (supports Minecraft color codes) |
|
||||
|
||||
Default memory path: `<instance_data_dir>/aigod_memory.json`
|
||||
|
||||
### Example config
|
||||
|
||||
```json
|
||||
{
|
||||
"server_name": "my-server",
|
||||
"log_path": "/path/to/minecraft/logs/latest.log",
|
||||
"rcon_host": "127.0.0.1",
|
||||
"rcon_port": 25575,
|
||||
"rcon_password": "yourpassword",
|
||||
"ollama_url": "http://localhost:11434",
|
||||
"model": "gemma3:12b",
|
||||
"command_model": "qwen3-coder:30b",
|
||||
"temperature": 0.85,
|
||||
"max_tokens": 600,
|
||||
"cooldown_seconds": 20,
|
||||
"max_commands_per_response": 6,
|
||||
"interventions_per_day": 4,
|
||||
"debug_commands": false,
|
||||
"memory_path": "/path/to/minecraft/aigod_memory.json",
|
||||
"god_chat_prefix": "[§6§lGOD§r]"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Player Usage
|
||||
|
||||
Type in chat (no slash — vanilla 1.21 rejects unknown slash commands client-side):
|
||||
|
||||
```
|
||||
pray <message> — send a prayer to God
|
||||
bible — show help/guidance
|
||||
```
|
||||
|
||||
On login players see:
|
||||
```
|
||||
[GOD] GOD ENABLED — Type "bible" in chat for guidance. Type "pray <message>" to pray.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## God's Capabilities
|
||||
|
||||
### Commands God can issue
|
||||
|
||||
**Give any item:**
|
||||
```
|
||||
give <player> minecraft:<item_id> <count>
|
||||
give <player> minecraft:<item_id>[enchantments={sharpness:4,unbreaking:3}] 1
|
||||
xp add <player> <amount> levels
|
||||
```
|
||||
|
||||
**Effects (positive/negative):** regeneration, strength, speed, night_vision, fire_resistance, water_breathing, instant_health, blindness, slowness, weakness, hunger, nausea, levitation, effect clear
|
||||
|
||||
**Movement:** `tp <player> <x> <y> <z>`
|
||||
|
||||
**World:** `time set day/night`, `weather clear/thunder/rain <duration>`
|
||||
|
||||
**Punishment:** `execute at <player> run summon minecraft:lightning_bolt ~ ~ ~`, `kill <player>`
|
||||
|
||||
**Mobs:** `execute at <player> run summon minecraft:creeper ~ ~ ~3`
|
||||
|
||||
### Item naming rules (Minecraft 1.21)
|
||||
|
||||
- Always use `minecraft:` namespace prefix
|
||||
- Beds: `white_bed`, `red_bed` etc — there is no `minecraft:bed`
|
||||
- Logs: `oak_log`, `spruce_log` etc — there is no `minecraft:log`
|
||||
- Wool: `white_wool`, `red_wool` etc — there is no `minecraft:wool`
|
||||
- Enchantments use component syntax: `item[enchantments={sharpness:5,unbreaking:3}]`
|
||||
- Give syntax: `give <player> minecraft:<item> <count>` — count is LAST
|
||||
|
||||
### God's persona
|
||||
|
||||
- Benevolent but just, theatrical and dramatic (Old Testament style)
|
||||
- Aware of player inventory, health, food, position, deaths
|
||||
- Aware of server state: time, weather, world border, online players
|
||||
- Aware of recent server events: deaths, chat, joins, leaves (last 20 min)
|
||||
- Remembers last 10 prayer exchanges across all players
|
||||
- Acts on own accord via intervention timer — may choose silence
|
||||
- Not obligated to grant requests — may reward someone else, punish the requester, or do something unexpected
|
||||
|
||||
---
|
||||
|
||||
## Model Recommendations
|
||||
|
||||
**Command model** — needs reliable structured JSON output and Minecraft syntax knowledge:
|
||||
- `qwen3-coder:30b` (recommended, ~19GB Q4)
|
||||
- `qwen2.5:1.5b` (fast/small, acceptable for commands)
|
||||
|
||||
**Message model** — needs creative writing, roleplay, dramatic biblical prose:
|
||||
- `gemma3:12b` (recommended, ~8GB)
|
||||
- `llama3.1:8b` (good alternative)
|
||||
|
||||
Avoid coding models for the message role. Avoid general models for the command role.
|
||||
|
||||
Both calls go to the same Ollama instance. If both models fit in VRAM simultaneously there is no swap overhead. If not, Ollama swaps them — adds a few seconds per prayer.
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
apt install python3-requests
|
||||
|
||||
# Deploy files
|
||||
cp mc_aigod.py /usr/local/bin/mc_aigod.py
|
||||
chmod +x /usr/local/bin/mc_aigod.py
|
||||
cp mc_aigod_shrink.json /etc/mc_aigod.json # edit as needed
|
||||
cp mc-aigod.service /etc/systemd/system/mc-aigod.service
|
||||
|
||||
# Enable and start
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now mc-aigod.service
|
||||
|
||||
# Monitor
|
||||
journalctl -fu mc-aigod.service
|
||||
tail -f /var/log/mc_aigod.log
|
||||
tail -f /var/log/mc_aigod_responses.log # full untruncated LLM responses
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging
|
||||
|
||||
**`debug_commands: true` in config** — shows executed commands in-game as dark gray italic text:
|
||||
```
|
||||
[~] give slingshooter08 minecraft:spruce_log 64 | weather thunder 6000
|
||||
```
|
||||
Never appears in `latest.log`. Toggle off by setting `false` and restarting.
|
||||
|
||||
**Log files:**
|
||||
- `/var/log/mc_aigod.log` — startup, prayers received, RCON results, errors
|
||||
- `/var/log/mc_aigod_responses.log` — full untruncated LLM responses with commands and messages
|
||||
|
||||
**Common issues:**
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| `Unknown item 'minecraft:64'` | LLM put count before item | Auto-fixed by `fix_give_command()`, also update command model |
|
||||
| `Unknown item 'minecraft:bed'` | Missing colour prefix | Item library includes warning; auto-namespaced |
|
||||
| Message truncated | Token limit hit | Increase `max_tokens`; two-call split helps |
|
||||
| `commands: []` but message says it will give something | LLM treating message as action | CRITICAL rule in prompt: commands array is the only way things happen |
|
||||
| Prayer not detected | Typed as `/pray` (slash command) | Must type `pray` in chat without slash |
|
||||
|
||||
---
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
### Why no Minecraft plugin?
|
||||
|
||||
No Java plugin required. The script tails `latest.log` for chat lines matching `pray ` and `bible`, then acts via RCON. This means:
|
||||
- Works with any vanilla server version that has RCON
|
||||
- No server restart required to install or update
|
||||
- Script restarts independently of the server
|
||||
|
||||
### Log detection patterns
|
||||
|
||||
```python
|
||||
# Chat messages in vanilla 1.21:
|
||||
# [HH:MM:SS] [Server thread/INFO]: <playername> pray message here
|
||||
# [HH:MM:SS] [Server thread/INFO]: <playername> bible
|
||||
|
||||
PRAY_PATTERN = re.compile(r'\[.*?\]: <(\w+)> [Pp]ray (.+)')
|
||||
BIBLE_PATTERN = re.compile(r'\[.*?\]: <(\w+)> [Bb]ible\s*$')
|
||||
JOIN_PATTERN = re.compile(r'\[.*?\]: (\w+) joined the game')
|
||||
```
|
||||
|
||||
Note: `/pray` as a slash command does NOT work — vanilla 1.21 rejects unknown commands client-side before they reach the server log.
|
||||
|
||||
### Two-call LLM architecture
|
||||
|
||||
```
|
||||
Prayer received
|
||||
│
|
||||
├─► Command call (command_model, temp=0.3, max_tokens=200, format=json)
|
||||
│ System: terse spec, command palette, item rules
|
||||
│ Returns: {"commands": [...]}
|
||||
│
|
||||
└─► Message call (model, temp=0.85, max_tokens=600, no format constraint)
|
||||
System: God persona only
|
||||
User: prayer + context + "You decided to execute: [commands]"
|
||||
Returns: plain prose, any length
|
||||
```
|
||||
|
||||
Separating the calls means:
|
||||
- Commands are never truncated by a long message
|
||||
- Message has full token budget for dramatic prose
|
||||
- Each model does what it's best at
|
||||
|
||||
### Prayer memory format
|
||||
|
||||
Stored as a list of `[player, prayer, god_message]` tuples in JSON. Loaded at startup, appended after every successful prayer, capped at 10 entries. Injected into the message call as alternating `user`/`assistant` messages so the LLM sees genuine conversation history.
|
||||
|
||||
### Divine intervention timing
|
||||
|
||||
Uses exponential distribution (`random.expovariate`) — the correct model for Poisson arrivals. `interventions_per_day=4` means an average gap of 6 hours but intervals are random and memoryless. Could be 3 in one hour, then nothing for 18 hours.
|
||||
|
||||
---
|
||||
|
||||
## Sethpc Infrastructure Context
|
||||
|
||||
This was developed and deployed on:
|
||||
- MCSManager on CT 644 (Proxmox, Debian 12, node-112)
|
||||
- Minecraft shrink-world server: port 25566, RCON 25576
|
||||
- Ollama on steel141 (192.168.0.141:11434)
|
||||
- Models: `gemma3:12b` (messages), `qwen3-coder:30b` (commands)
|
||||
- Service: `mc-aigod.service` on CT 644
|
||||
- Config: `/etc/mc_aigod.json`
|
||||
- Script: `/usr/local/bin/mc_aigod.py`
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "minecraft:add_spawns",
|
||||
"biomes": "#minecraft:is_overworld",
|
||||
"spawners": [
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"weight": 400,
|
||||
"minCount": 1,
|
||||
"maxCount": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"pack": {
|
||||
"pack_format": 26,
|
||||
"description": "Increased creeper spawn rate"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"values": ["shrinkborder:load"]}
|
||||
@@ -0,0 +1 @@
|
||||
{"values": ["shrinkborder:tick"]}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Check if total deaths increased since last tick
|
||||
execute unless score $deaths_total deaths_total = $deaths_prev deaths_prev run function shrinkborder:on_death
|
||||
|
||||
# Update previous death count
|
||||
scoreboard players operation $deaths_prev deaths_prev = $deaths_total deaths_total
|
||||
@@ -0,0 +1,2 @@
|
||||
scoreboard players set $shrink_enabled shrink_enabled 0
|
||||
tellraw @a ["",{"text":"[ShrinkBorder] ","color":"gold"},{"text":"Border shrinking is now ","color":"white"},{"text":"DISABLED","color":"red"},{"text":".","color":"gray"}]
|
||||
@@ -0,0 +1,5 @@
|
||||
scoreboard players set $shrink_enabled shrink_enabled 1
|
||||
scoreboard players set $deaths_total deaths_total 0
|
||||
execute as @a run scoreboard players operation $deaths_total deaths_total += @s player_deaths
|
||||
scoreboard players operation $deaths_prev deaths_prev = $deaths_total deaths_total
|
||||
tellraw @a ["",{"text":"[ShrinkBorder] ","color":"gold"},{"text":"Border shrinking is now ","color":"white"},{"text":"ENABLED","color":"green"},{"text":"! Die and the walls close in.","color":"gray"}]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Initialise scoreboards on world load
|
||||
scoreboard objectives add deaths_total dummy "Total Deaths"
|
||||
scoreboard objectives add deaths_prev dummy "Previous Deaths"
|
||||
scoreboard objectives add border_parity dummy "Border Parity"
|
||||
scoreboard objectives add shrink_enabled dummy "Shrink Enabled"
|
||||
scoreboard objectives add player_deaths deathCount
|
||||
|
||||
# Set shrink feature to DISABLED by default
|
||||
scoreboard players set $shrink_enabled shrink_enabled 0
|
||||
|
||||
# Initialise parity tracker (0=shrink N/S, 1=shrink E/W)
|
||||
scoreboard players set $border_parity border_parity 0
|
||||
|
||||
# Set world border
|
||||
worldborder center 0 0
|
||||
worldborder set 1000
|
||||
|
||||
tellraw @a ["",{"text":"[ShrinkBorder] ","color":"gold"},{"text":"Loaded. Shrinking is ","color":"white"},{"text":"DISABLED","color":"red"},{"text":". Use /function shrinkborder:enable to start.","color":"gray"}]
|
||||
@@ -0,0 +1,12 @@
|
||||
# A death occurred - shrink the border by 1 on alternating axes
|
||||
tellraw @a ["",{"text":"[ShrinkBorder] ","color":"gold"},{"text":"A player died! Border shrinking...","color":"red"}]
|
||||
|
||||
# Even deaths: shrink N/S
|
||||
execute if score $border_parity border_parity matches 0 run function shrinkborder:shrink_ns
|
||||
|
||||
# Odd deaths: shrink E/W
|
||||
execute if score $border_parity border_parity matches 1 run function shrinkborder:shrink_ew
|
||||
|
||||
# Flip parity
|
||||
execute if score $border_parity border_parity matches 0 run scoreboard players set $border_parity border_parity 1
|
||||
execute if score $border_parity border_parity matches 1 run scoreboard players set $border_parity border_parity 0
|
||||
@@ -0,0 +1,5 @@
|
||||
scoreboard players set $shrink_enabled shrink_enabled 0
|
||||
scoreboard players set $border_parity border_parity 0
|
||||
worldborder center 0 0
|
||||
worldborder set 1000
|
||||
tellraw @a ["",{"text":"[ShrinkBorder] ","color":"gold"},{"text":"Border reset to 1000x1000 and shrinking DISABLED.","color":"green"}]
|
||||
@@ -0,0 +1,2 @@
|
||||
worldborder add -1 1
|
||||
tellraw @a ["",{"text":"[ShrinkBorder] ","color":"gold"},{"text":"East wall closed in. ","color":"yellow"},{"text":"Stay inside!","color":"red"}]
|
||||
@@ -0,0 +1,2 @@
|
||||
worldborder add -1 1
|
||||
tellraw @a ["",{"text":"[ShrinkBorder] ","color":"gold"},{"text":"North wall closed in. ","color":"yellow"},{"text":"Stay inside!","color":"red"}]
|
||||
@@ -0,0 +1,6 @@
|
||||
# Run every tick - count total deaths across all players
|
||||
scoreboard players set $deaths_total deaths_total 0
|
||||
execute as @a run scoreboard players operation $deaths_total deaths_total += @s player_deaths
|
||||
|
||||
# Only process if shrinking is enabled
|
||||
execute if score $shrink_enabled shrink_enabled matches 1 run function shrinkborder:check_deaths
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"pack": {
|
||||
"pack_format": 26,
|
||||
"description": "Shrinking border on death"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Minecraft AI God (/pray watcher)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/mc_aigod.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+1078
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"server_name": "shrink-world",
|
||||
"log_path": "/opt/mcsmanager/daemon/data/InstanceData/shrinkborder1234567890abcdef12345/logs/latest.log",
|
||||
"rcon_host": "127.0.0.1",
|
||||
"rcon_port": 25576,
|
||||
"rcon_password": "REDACTED_RCON",
|
||||
"ollama_url": "http://192.168.0.141:11434",
|
||||
"model": "gemma3:12b",
|
||||
"command_model": "qwen3-coder:30b",
|
||||
"temperature": 0.85,
|
||||
"max_tokens": 600,
|
||||
"cooldown_seconds": 20,
|
||||
"max_commands_per_response": 6,
|
||||
"interventions_per_day": 4,
|
||||
"god_chat_prefix": "[§6§lGOD§r]",
|
||||
"debug_commands": true,
|
||||
"memory_path": "/opt/mcsmanager/daemon/data/InstanceData/shrinkborder1234567890abcdef12345/aigod_memory.json"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Minecraft God Mode Watcher (mc1 - slingshooter08)
|
||||
After=mcsm-daemon.service
|
||||
Requires=mcsm-daemon.service
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/mc_godmode_watch.sh
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Minecraft Shrink World Auto Kit on Join
|
||||
After=mcsm-daemon.service
|
||||
Requires=mcsm-daemon.service
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/shrink_godkit.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Sets slingshooter08 to creative mode on server 1 (mc1).
|
||||
Called by mc_godmode_watch.sh whenever slingshooter08 joins.
|
||||
|
||||
Deployed to: /usr/local/bin/mc_godmode_rcon.py on CT 644
|
||||
"""
|
||||
import socket, struct, time, sys
|
||||
|
||||
PLAYER = 'slingshooter08'
|
||||
|
||||
def rcon(cmd, host='127.0.0.1', port=25575, password='REDACTED_RCON'):
|
||||
s = socket.socket()
|
||||
s.settimeout(5)
|
||||
s.connect((host, 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, password))
|
||||
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()
|
||||
|
||||
print(rcon(f'gamemode creative {PLAYER}'))
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Watches mc1 log for slingshooter08 joining and applies creative mode.
|
||||
# Deployed to: /usr/local/bin/mc_godmode_watch.sh on CT 644
|
||||
# Managed by: mc-godmode.service
|
||||
|
||||
LOG=/opt/mcsmanager/daemon/data/InstanceData/d39f55861cb34204a92a18a9e1c78ca6/logs/latest.log
|
||||
PLAYER=slingshooter08
|
||||
|
||||
tail -F "$LOG" | grep --line-buffered "joined the game" | while read line; do
|
||||
if echo "$line" | grep -qi "$PLAYER"; then
|
||||
sleep 1
|
||||
python3 /usr/local/bin/mc_godmode_rcon.py
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user