PROYECTO · REDES / AUTOMATIZACIÓN
Goodgame Empire — WebSocket + SmartFox protocol
Bot multi-cuenta construido en Node.js que se conecta directamente al servidor de Goodgame Empire mediante WebSocket. Habla el protocolo SmartFoxServer del juego (mensajes XML para el handshake y frames %xt% con payload JSON para el gameplay), permitiendo automatizar tareas repetitivas. La arquitectura usa Worker Threads para ejecutar cada cuenta en un hilo independiente y un sistema de plugins modulares que cubren desde ataques automáticos hasta integración con Discord. Es un proyecto personal y educativo, sin fines comerciales, hecho para aprender redes, protocolos y concurrencia.
Tecnologías
JavaScriptNode.jsWebSocketSmartFoxServer ProtocolWorker ThreadsExpressSQLiteDiscord.js
Cómo funciona · 01
Visión general
El proyecto es un servidor Express con un panel web que gestiona múltiples cuentas del juego. Cada cuenta se ejecuta como un Worker Thread independiente que mantiene su propia conexión WebSocket al servidor del juego. Un sistema de plugins permite activar y configurar funcionalidades por cuenta. La persistencia se maneja con SQLite nativo de Node.js y todo el frontend web se sirve como static files.
- Servidor Express con panel web para gestionar cuentas y plugins.
- Worker Threads: cada cuenta del juego corre en su propio hilo con conexión WebSocket independiente.
- SQLite para persistencia de usuarios, cuentas y configuración de plugins.
Cómo funciona · 02
Protocolo SmartFox — handshake y comunicación
El juego usa SmartFoxServer. El handshake es XML: primero verChk, luego login con zona del servidor, y finalmente autoJoin. Una vez dentro, toda la comunicación usa frames con formato %xt%zone%cmd%room%jsonPayload%. El bot parsea cada frame, extrae el comando y emite eventos a través de un EventEmitter para que los plugins reaccionen.
- Handshake XML: verChk → login (zona + credenciales) → autoJoin.
- Frames %xt%: cada mensaje del juego es un string delimitado con comando + JSON.
- Rate limiter de 5 mensajes/segundo para evitar detección por el servidor.
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>`)
}
}
Cómo funciona · 03
sendXT + waitForResult — envío y espera de respuestas
sendXT construye un frame %xt% y lo envía respetando el rate limiter. waitForResult registra un listener en el EventEmitter y devuelve una Promise que se resuelve cuando el servidor responde con el comando esperado, o se rechaza por timeout. Este patrón permite a cada plugin hacer await de sus acciones de forma secuencial.
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)
})
Cómo funciona · 04
Worker Threads — una cuenta, un hilo
El servidor principal (main.js) crea un Worker Thread por cada cuenta activa instanciando ggeBot.js con los datos de configuración. Cada worker es independiente: tiene su propia conexión WebSocket, sus plugins cargados y su estado. Si un worker crashea, main.js lo detecta y lo reinicia automáticamente tras un delay configurable. La comunicación entre main y workers usa parentPort.postMessage con ActionTypes numéricos.
- Cada cuenta = new Worker('./ggeBot.js', { workerData: config }).
- Auto-restart: si el worker termina y la cuenta sigue activa, se reinicia tras N segundos.
- Comunicación bidireccional main↔worker mediante 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))
}
})
}
Cómo funciona · 05
Sistema de plugins modulares
Los plugins se auto-descubren leyendo el directorio plugins/ al arrancar. Cada plugin exporta metadata (descripción, opciones configurables, si es obligatorio) y, cuando se ejecuta como worker, escucha eventos del xtHandler para reaccionar a mensajes del juego. Hay plugins de ataque (nómadas, fortalezas, barones, khan, samurai…), gestión de recursos, envío de tropas, integración con Discord y más.
- Auto-discovery: se escanea plugins/ y se carga cada .js encontrado.
- Patrón dual: en el hilo principal exporta metadata, en el worker ejecuta la lógica.
- Plugins de ataque con cálculos de unidades, oleadas y herramientas de asedio.
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
Cómo funciona · 06
Integración con Discord
El bot incluye integración completa con Discord.js v14: slash commands, notificaciones de ataques entrantes y salientes, estado de fortalezas y chat del juego reenviado a canales de Discord. La autenticación OAuth2 vincula el usuario del panel web con su cuenta de Discord. Los canales disponibles se detectan automáticamente según los permisos del bot en el servidor.
- OAuth2 flow completo para vincular cuenta web ↔ Discord.
- Notificaciones en tiempo real: ataques entrantes con preview, chat del juego, estado de islas.
- Slash commands para consultar estado desde Discord sin abrir el panel.