PROJECT · NETWORKING / AUTOMATION
Goodgame Empire — WebSocket + SmartFox protocol
Multi-account bot built in Node.js that connects directly to the Goodgame Empire server over WebSocket. It speaks the game's SmartFoxServer protocol (XML messages for the handshake and %xt% frames with JSON payloads for gameplay), enabling automation of repetitive tasks. The architecture uses Worker Threads to run each account in an independent thread and a modular plugin system covering everything from automated attacks to Discord integration. It is a personal, educational and non-commercial project built to learn networking, protocols and concurrency.
Tech stack
JavaScriptNode.jsWebSocketSmartFoxServer ProtocolWorker ThreadsExpressSQLiteDiscord.js
How it works · 01
Overview
The project is an Express server with a web panel that manages multiple game accounts. Each account runs as an independent Worker Thread maintaining its own WebSocket connection to the game server. A plugin system lets users enable and configure features per account. Persistence is handled with Node.js native SQLite and the entire web frontend is served as static files.
- Express server with a web panel for managing accounts and plugins.
- Worker Threads: each game account runs in its own thread with an independent WebSocket connection.
- SQLite for persistence of users, accounts and plugin configuration.
How it works · 02
SmartFox protocol — handshake and communication
The game uses SmartFoxServer. The handshake is XML: first verChk, then login with the server zone, and finally autoJoin. Once inside, all communication uses frames with the format %xt%zone%cmd%room%jsonPayload%. The bot parses each frame, extracts the command and emits events through an EventEmitter so plugins can react.
- XML handshake: verChk → login (zone + credentials) → autoJoin.
- %xt% frames: each game message is a delimited string with command + JSON.
- Rate limiter of 5 messages/second to avoid detection by the server.
ggeBot.js — handshake + frame parsing
// ── Conexión WebSocket al servidor del juego ──
const webSocket = new WebSocket(
`wss://${botConfig.gameURL}/`
)
webSocket.onopen = () => {
// 1. Version check (XML)
webSocket.send(
'<msg t="sys"><body action="verChk" r="0">'
+ '<ver v="166"/></body></msg>'
)
}
webSocket.onmessage = ({ data: message }) => {
// ── SmartFox %xt% frames ──
if (message.charAt(0) === "%") {
let [,,cmd,, r, obj] = message.split("%")
const result = Number(r)
obj = JSON.parse(obj)
// "gbd" = game bulk data → emitir cada clave
if (cmd === "gbd")
for (const [key, value] of Object.entries(obj))
xtHandler.emit(key, value, 0)
else
xtHandler.emit(cmd, obj, result)
}
// ── XML handshake responses ──
else if (message.includes("apiOK")) {
webSocket.send(`<msg t="sys"><body action="login"
r="0"><login z="${botConfig.gameServer}">
<nick><![CDATA[]]></nick>
<pword><![CDATA[undefined%en%0]]></pword>
</login></body></msg>`)
}
}
How it works · 03
sendXT + waitForResult — sending and awaiting responses
sendXT builds a %xt% frame and sends it respecting the rate limiter. waitForResult registers a listener on the EventEmitter and returns a Promise that resolves when the server responds with the expected command, or rejects on timeout. This pattern lets each plugin await its actions sequentially.
ggeBot.js — sendXT + waitForResult
// ── Rate limiter: 5 mensajes/seg ──
const limiter = new RateLimiter({
tokensPerInterval: 5, interval: "sec"
})
async function sendXT(cmdName, paramObj) {
await limiter.removeTokens(1)
webSocket.send(
`%xt%${botConfig.gameServer}%${cmdName}%1%${paramObj}%`
)
}
// ── Espera la respuesta del servidor ──
const waitForResult = (key, timeout, func) =>
new Promise((resolve, reject) => {
func ??= _ => true
let timer = setTimeout(() => {
xtHandler.removeListener(key, handler)
reject("TIMED_OUT")
}, timeout * (ggeConfig.timeoutMultiplier ?? 1) * 5)
const handler = (data, result) => {
if (!func(Object(data), Number(result))) return
xtHandler.removeListener(key, handler)
clearInterval(timer)
resolve([Object(data), Number(result)])
}
xtHandler.addListener(key, handler)
})
How it works · 04
Worker Threads — one account, one thread
The main server (main.js) creates a Worker Thread for each active account by instantiating ggeBot.js with configuration data. Each worker is independent: it has its own WebSocket connection, loaded plugins and state. If a worker crashes, main.js detects it and restarts it automatically after a configurable delay. Communication between main and workers uses parentPort.postMessage with numeric ActionTypes.
- Each account = new Worker('./ggeBot.js', { workerData: config }).
- Auto-restart: if the worker exits and the account is still active, it restarts after N seconds.
- Bidirectional main↔worker communication via ActionType enums.
main.js — createBot (simplificado)
async function createBot(uuid, user) {
// Asignar plugins activos y servidor
plugins.forEach(plugin => {
data.plugins[plugin.key] ??= {}
if (plugin.force)
data.plugins[plugin.key].state = true
if (data.plugins[plugin.key]?.state)
data.plugins[plugin.key].filename = plugin.filename
})
const instance = instances.find(
e => Number(e.gameID) == data.server
)
data.gameURL ??= instance.gameURL
data.gameServer ??= instance.gameServer
// Crear worker con config completa
const worker = new Worker('./ggeBot.js', {
workerData: { ...data, discordData }
})
botMap.set(user.id, worker)
// Auto-restart on exit
worker.on('exit', () => {
if (getSpecificUser(uuid, user).state == true) {
setTimeout(() => createBot(uuid, user),
1000 * (ggeConfig.secondsTillRestartBot ?? 10))
}
})
}
How it works · 05
Modular plugin system
Plugins are auto-discovered by reading the plugins/ directory at startup. Each plugin exports metadata (description, configurable options, whether it is mandatory) and, when running as a worker, listens to xtHandler events to react to game messages. There are attack plugins (nomads, fortresses, barons, khan, samurai…), resource management, troop deployment, Discord integration and more.
- Auto-discovery: plugins/ is scanned and each .js found is loaded.
- Dual pattern: on the main thread it exports metadata, on the worker it runs the logic.
- Attack plugins with unit calculations, waves and siege tool computations.
plugins/index.js + attack/attack.js
// ── Auto-discovery de plugins ──
const dir = fs.readdirSync(__dirname, { recursive: true })
dir.forEach(file => {
if (path.extname(file) != ".js") return
plugins.push([
`plugins/${file.slice(0, -3)}`,
require(path.join(__dirname, file))
])
})
// ── Plugin de ataque: metadata + cálculos ──
// En el hilo principal → exportar opciones
if (require('worker_threads').isMainThread) {
return module.exports = {
force: true,
pluginOptions: [
{ type: "Text", key: "attackDelaySeconds",
default: "4.5" },
{ type: "Text", key: "attackDelayRandomizationSeconds",
default: "2.5" },
{ type: "Text", key: "attackLimit" }
]
}
}
// En el worker → lógica de cálculo
const getMaxAttackers = targetLevel =>
targetLevel <= 69
? Math.min(260, 5 * targetLevel + 8)
: 320
How it works · 06
Discord integration
The bot includes full Discord.js v14 integration: slash commands, incoming and outgoing attack notifications, fortress status and in-game chat forwarded to Discord channels. OAuth2 authentication links the web panel user with their Discord account. Available channels are auto-detected based on the bot's permissions in the guild.
- Full OAuth2 flow to link web account ↔ Discord.
- Real-time notifications: incoming attacks with preview, in-game chat, island status.
- Slash commands to check status from Discord without opening the panel.