PROXECTO · REDES / AUTOMATIZACIÓN
Goodgame Empire — WebSocket + SmartFox protocol
Bot multi-conta construído en Node.js que se conecta directamente ao servidor de Goodgame Empire mediante WebSocket. Fala o protocolo SmartFoxServer do xogo (mensaxes XML para o handshake e frames %xt% con payload JSON para o gameplay), permitindo automatizar tarefas repetitivas. A arquitectura usa Worker Threads para executar cada conta nun fío independente e un sistema de plugins modulares que cobren dende ataques automáticos ata integración con Discord. É un proxecto persoal e educativo, sen fins comerciais, feito para aprender redes, protocolos e concorrencia.
Tecnoloxías
JavaScriptNode.jsWebSocketSmartFoxServer ProtocolWorker ThreadsExpressSQLiteDiscord.js
Como funciona · 01
Visión xeral
O proxecto é un servidor Express cun panel web que xestiona múltiples contas do xogo. Cada conta execútase como un Worker Thread independente que mantén a súa propia conexión WebSocket ao servidor do xogo. Un sistema de plugins permite activar e configurar funcionalidades por conta. A persistencia manéxase con SQLite nativo de Node.js e todo o frontend web sérvese como static files.
- Servidor Express con panel web para xestionar contas e plugins.
- Worker Threads: cada conta do xogo corre no seu propio fío con conexión WebSocket independente.
- SQLite para persistencia de usuarios, contas e configuración de plugins.
Como funciona · 02
Protocolo SmartFox — handshake e comunicación
O xogo usa SmartFoxServer. O handshake é XML: primeiro verChk, despois login coa zona do servidor, e finalmente autoJoin. Unha vez dentro, toda a comunicación usa frames con formato %xt%zone%cmd%room%jsonPayload%. O bot parsea cada frame, extrae o comando e emite eventos a través dun EventEmitter para que os plugins reaccionen.
- Handshake XML: verChk → login (zona + credenciais) → autoJoin.
- Frames %xt%: cada mensaxe do xogo é un string delimitado con comando + JSON.
- Rate limiter de 5 mensaxes/segundo para evitar detección polo 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>`)
}
}
Como funciona · 03
sendXT + waitForResult — envío e espera de respostas
sendXT constrúe un frame %xt% e envíao respetando o rate limiter. waitForResult rexistra un listener no EventEmitter e devolve unha Promise que se resolve cando o servidor responde co comando esperado, ou se rexeita por timeout. Este patrón permite a cada plugin facer await das súas accións 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)
})
Como funciona · 04
Worker Threads — unha conta, un fío
O servidor principal (main.js) crea un Worker Thread por cada conta activa instanciando ggeBot.js cos datos de configuración. Cada worker é independente: ten a súa propia conexión WebSocket, os seus plugins cargados e o seu estado. Se un worker crashea, main.js detéctao e reiníciao automaticamente tras un delay configurable. A comunicación entre main e workers usa parentPort.postMessage con ActionTypes numéricos.
- Cada conta = new Worker('./ggeBot.js', { workerData: config }).
- Auto-restart: se o worker termina e a conta segue activa, reiníciase 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))
}
})
}
Como funciona · 05
Sistema de plugins modulares
Os plugins auto-descóbrense lendo o directorio plugins/ ao arrancar. Cada plugin exporta metadata (descrición, opcións configurables, se é obrigatorio) e, cando se executa como worker, escoita eventos do xtHandler para reaccionar a mensaxes do xogo. Hai plugins de ataque (nómadas, fortalezas, baróns, khan, samurai…), xestión de recursos, envío de tropas, integración con Discord e máis.
- Auto-discovery: escanéase plugins/ e cárgase cada .js atopado.
- Patrón dual: no fío principal exporta metadata, no worker executa a lóxica.
- Plugins de ataque con cálculos de unidades, oleadas e ferramentas 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
Como funciona · 06
Integración con Discord
O bot inclúe integración completa con Discord.js v14: slash commands, notificacións de ataques entrantes e saíntes, estado de fortalezas e chat do xogo reenviado a canais de Discord. A autenticación OAuth2 vincula o usuario do panel web coa súa conta de Discord. Os canais dispoñibles detéctanse automaticamente segundo os permisos do bot no servidor.
- OAuth2 flow completo para vincular conta web ↔ Discord.
- Notificacións en tempo real: ataques entrantes con preview, chat do xogo, estado de illas.
- Slash commands para consultar estado dende Discord sen abrir o panel.