← Volver ao portfolio

PROXECTO · REDES / AUTOMATIZACIÓN

Bot de 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.