The long-form answer to "so how's the WoW server going?" — what's running, how the mechanics actually work, and what broke along the way.
Nothing exotic — the whole thing runs on hand-me-down hardware across a small Proxmox setup. The trick isn't the gear, it's giving each box exactly one job it's actually good at:
An old laptop running Proxmox. One Linux container gets 13 of its 15GB of RAM — not because the server needs it to compute, but so the map data stays in page cache. When that cache starves, the game stutters. RAM as a moat.
The Windows gaming PC doubles as the AI box: Ollama serving a 12B model (unslopnemo-rp:12b, 9.9GB pinned in VRAM at 16k context). Every line of companion dialogue is a network call from the container to this GPU.
A NUC carrying the boring-but-vital tier: vector database for bot memories, Grafana for dashboards, and the Proxmox Backup Server the game node is being joined to.
Old Optiplex with a big slow disk. Cold archives and SQL dumps only — an earlier outage proved the hard way that hot state behind spinning NFS is how you take down fifteen containers at once.
The core problem: the game engine knows everything that happens — every kill, death, and wipe — but writes none of it anywhere a script can read. Seven planned features were stuck on exactly that. The fix was one pipe, built once: a Lua script inside the engine writes interesting events into a database table, and the Python sidecar polls that table and acts on them. Every mechanic below is just a handler plugged into that loop.
Why a plain table instead of a proper queue? Because it's inspectable with a
SELECT, replayable after an outage, and survives either side restarting. The
consumer claims events and marks them consumed in the same transaction, so a crashing
handler can't wedge the feed — one bad row gets logged and skipped, and the batch continues.
The dangerous part was volume. 650 bots fight constantly; logging every kill would mean thousands of rows a minute into the database whose responsiveness is the game — and disk thrash is the documented way this server dies. So the Lua side filters before writing: kills only count if the victim was rare/elite/boss or a human was nearby, deaths only near a real player, logins only for real players. Baseline result: 4 rows in 5 minutes with nobody logged in.
A rare, elite, or boss dies anywhere in the world → the realm posts a bounty on it. Whoever lands the next kill collects.
HOW · A handler on the event feed inserts a bounty row; a database unique constraint enforces one open bounty per creature, so a rare that dies on respawn forever can't flood the board. First live test: a bot killed a rare, the filter judged it notable, the bounty posted — the whole chain worked on the first real kill.
A bot that kills you starts a grudge and climbs a rank ladder with each repeat: Rival → Menace → Scourge → Nemesis → Archnemesis.
HOW · One database row per killer-victim pair, not per bot — so a bot can be your Archnemesis and a stranger to everyone else. That single schema choice is what makes it feel personal. PvP deaths always pass the volume filter, because they're the signal this feeds on.
A party wipe raises an "incident" recording who died, where, and to what — the plan is to mail it to you as a snarky root-cause analysis.
HOW · A wipe isn't a single event — it's "several deaths, same place, within 30 seconds." The Lua script stays dumb and cheap (one death = one row); the Python side correlates across events, because holding cross-event state inside the game process would blow the performance budget. Tested both ways: three quick deaths raise an incident, the same three spread over minutes correctly don't.
Every character on the realm has a fixed identity — archetype, voice, temperament, motivation, one oddly specific habit.
HOW · No LLM involved. Identity is a pure function of the bot's GUID, name, race, and class — a deterministic hash into trait tables. Same bot, same person, forever, at zero token cost; re-deriving 400 of them produced zero drift. An LLM would've been slower, cost money, and — the real killer — invented a different person on every run. All 168 archetype×voice combos get used. The registry has since grown from 1,154 to 2,785 identities, and the compiled personality cards — 2,798 of them — are now deployed to the whole population, the switch an earlier version of this page called "staged."
Party companions chat freely, but factual questions — "where should we quest?" — get answers grounded in the game's real data, or an honest "I don't know."
HOW · A knowledge service sits between the model and the answer: it returns actual rows from the world database (with quest IDs), checks level ranges and the race-restriction bitmask, and refuses to fabricate when it has nothing. The LLM only chooses the wording. Tested with real characters on both factions — no invented zones, no Tauren sent to Stormwind.
Bots hold beliefs about events — possibly second-hand, possibly wrong — and gossip spreads with a paper trail.
HOW · Facts and beliefs are separate tables: what happened vs. what each character thinks happened. Relationships are directional (A can like B while B loathes A). Rumor confidence multiplies by 0.6 per retelling, so gossip dies out after ~6 hops instead of circulating forever. Privacy is tested adversarially — the suite actively tries to extract one character's secrets through another and asserts it comes back empty.
Characters near you chat with each other, unprompted, about where they actually are — cities feel inhabited instead of staffed.
HOW · The previous attempt fired ambient chatter randomly across two continents, so with the client's 40-yard say-range, you never saw any of it. The director now only casts bots within those 40 yards of a player, on cooldowns (90s per player, 10min per bot, hourly cap), with 45-second quiet windows during combat and quest text. Cheaper and more visible. First unprompted exchange happened two minutes after arming — two bots discussing shelter from the live storm, by name.
Bots now form their own parties, remember who they've grouped with, and can found their own guilds. The 20 premade factory guilds were deleted to make room for real ones.
HOW · The best discovery of the project so far: a whole cluster of bot social behavior — grouping, guilds, PvP — ships inside the playerbots module but is commented out upstream, reachable through one config string. Re-enabling grouping took party formation from one group in thirteen hours to ten in twenty minutes. A sidecar job snapshots memberships every ten minutes into "bands," so a recurring lineup becomes a recognized group with history. The roadmap here borrows from Project Sid — the 1,000-agent Minecraft experiment where roles, laws, and culture emerged — as the measuring stick for what a 650-bot society can produce.
Cities can be snapshotted and restored — the prerequisite for ever letting AI events damage a town.
HOW · snapshot records every creature spawn in a hub while
the world is pristine (Orgrimmar: 1,243 · Stormwind: 1,288); restore re-adds
whatever's missing. It only ever adds — running it twice is harmless — and it refuses
to overwrite a baseline without --force, because re-snapshotting a damaged world
would destroy the thing you'd restore from. Proven by actually deleting spawns and getting
them back byte-identical, on a scratch copy of the table so the test itself couldn't hurt
the live game.
A few measurements ended up steering the whole architecture — measured, not guessed:
| NUMBER | WHAT IT MEANS |
|---|---|
| 2.4 lines/sec | The GPU's dialogue ceiling. Two concurrent LLM calls are free (same latency, double throughput); past two, latency climbs linearly for nothing. So ambient chatter gets one slot, and the second stays reserved for the player — that one benchmark dictated the entire conversation-scheduling design. Re-measured after the model swap: the 12B holds 2.46, no slower than the 8B it replaced. |
| 13h → 20min | Time for the bots to form one party before and after re-enabling the grouping strategy the module ships disabled. The feature existed all along — it was one config string away. |
| 70–140 bots | How many characters can plausibly hold conversations within that ceiling — out of 650. The rest stay silent by design, not neglect. |
| 40 yards | The client's say-range. Any social feature that fires outside it is invisible theater — this number is why the old city-chatter never appeared and the new one does. |
| 4 rows / 5 min | Event-feed baseline with 650 bots grinding and nobody logged in. The filters work. The remaining gate: under 500 rows in 10 minutes with a human playing. |
| 96.7% | Share of creature rows with zoneId = 0 — see
the scars below for why that number nearly sank the restore system. |
| 0.6× per hop | Rumor confidence decay. After six retellings a rumor is at 4.7% and effectively dead — gossip with a half-life. |
zoneId column seems natural — except 96.7% of creature
rows have it set to 0 (it's a lazily-populated cache column). A zone-based restore would have
quietly skipped almost every spawn while appearing to work. The restore keys on map +
coordinates instead. Lesson: verify what's actually in the column before building on
it.StartLimitIntervalSec placed under [Service] instead of
[Unit] is not an error; it's just ignored. The rate-limiting I thought I'd
configured wasn't active on two services, and nothing would have said so until one of them
started crash-looping against the game's database. Found it by reading back the effective
config with systemctl show, not the unit file.rank is a reserved word in MySQL 8. And the syntax
error it produces points at the wrong part of the statement, which makes it slow to
spot.That's the honest state of it: the spectacular stuff is still ahead, but the boring load-bearing layer underneath — the event feed, the consumer, the failsafes, the tests — is built, measured, and running. Which, as any homelabber knows, is the part nobody sees and the part everything depends on.
Questions welcome.
— moon
Evermind is an unofficial, private, non-commercial fan project and is not affiliated with or endorsed by Blizzard Entertainment. World of Warcraft is a trademark of Blizzard Entertainment, Inc.