Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Headless TypeScript client for bonk.io — connects via Socket.IO, no browser required. Create/join rooms, manage rosters, run 24/7 bots with automatic reconnection.
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
Cliente TypeScript headless para bonk.io — conecta direto via Socket.IO, sem browser. Crie e gerencie salas, entre em salas existentes, escute o roster e os eventos de partida em tempo real, e rode bots 24h com reconexão automática. Por que headless? Automatizar o bonk.io com Puppeteer/Playwright exige um browser completo (~300 MB de RAM, headless instável, tela virtual no Linux). bonktools fala o protocolo Socket.IO do jogo diretamente — menos de 15 MB de RAM por sala, sem Chromium, sem Xvfb, sem depender de seletores de UI que quebram a cada atualização do client.檢視完整 README ↗
Cliente TypeScript headless para bonk.io — conecta direto via Socket.IO, sem browser. Crie e gerencie salas, entre em salas existentes, escute o roster e os eventos de partida em tempo real, e rode bots 24h com reconexão automática.
Por que headless? Automatizar o bonk.io com Puppeteer/Playwright exige um browser completo (~300 MB de RAM, headless instável, tela virtual no Linux). bonktools fala o protocolo Socket.IO do jogo diretamente — menos de 15 MB de RAM por sala, sem Chromium, sem Xvfb, sem depender de seletores de UI que quebram a cada atualização do client.
createRoom()joinRoom()BonkRoom — eventos e métodosBonkSession — pool de salasBonkSession
└── BonkRoom (1 por sala)
└── BonkTransport (socket.io-client v2)
└── bonk.io WS
| Camada | Responsabilidade |
|---|---|
BonkTransport | Conexão Socket.IO v2 (EIO=3), TLS Sectigo, timesync |
BonkRoom | Ciclo de vida da sala, roster, eventos tipados, reconexão |
BonkSession | Pool de salas, auth compartilhado, throttle, reconcile 60s |
npm install bonktools
# ou
pnpm add bonktools
# ou
yarn add bonktools
Requisitos: Node.js >= 20.18.1. O pacote usa ESM ("type": "module"), com build CJS disponível para require().
A lib suporta dois modos:
import type { AuthOptions } from 'bonktools';
// Conta registrada (recomendado para bots de longa duração)
const auth: AuthOptions = {
type: 'registered',
username: 'meu_usuario',
password: 'minha_senha',
};
// Convidado (sem HTTP de auth)
const auth: AuthOptions = {
type: 'guest',
guestName: 'BonkBot',
};
Com type: 'registered', a lib faz uma chamada HTTP para o endpoint de login do bonk.io e obtém um token de sessão. O token é reusado em todas as salas da mesma BonkSession.
createRoom()A função mais direta. Retorna um BonkRoom já conectado e ativo (aguarda o packet SHARE_LINK do servidor antes de resolver).
import { createRoom } from 'bonktools';
const room = await createRoom({
auth: { type: 'registered', username: '...', password: '...' },
desiredState: {
roomName: 'Minha Sala',
password: '', // string vazia = sem senha
maxPlayers: 6,
mode: 'b', // 'b'=classic, 'ar'=arrows, 'ard'=arrows death, 'sp'=grapple, 'v'=vtol, 'f'=football
rounds: 3,
},
hidden: false, // aparece na lista pública
timeoutMs: 10_000, // rejeita com RoomCreationTimeoutError se demorar mais
});
console.log('Link da sala:', room.shareLink);
// => https://bonk.io/123456abcde
createRoom()| Campo | Tipo | Default | Descrição |
|---|---|---|---|
auth | AuthOptions | — | Estratégia de autenticação (obrigatório) |
desiredState | DesiredRoomState | — | Configuração da sala (obrigatório) |
hidden | boolean | false | Sala oculta na lista pública |
minLevel | number | 0 | Nível mínimo para entrar |
maxLevel | number | 999 | Nível máximo para entrar |
timeoutMs | number | 10000 | Timeout em ms para receber SHARE_LINK |
protocolVersion | number | 49 | Versão do protocolo bonk.io |
reconnectPolicy | ReconnectPolicyOptions | defaults | Política de backoff |
DesiredRoomStateinterface DesiredRoomState {
roomName: string;
password: string; // '' = sem senha
maxPlayers: number; // 1–8
mode: string | number; // 'b', 'ar', 'ard', 'sp', 'v', 'f'...
engine?: string; // 'b', 'f'...
rounds: number;
map?: string | null; // blob LZ-String do mapa
}
joinRoom()import { joinRoom } from 'bonktools';
// Via URL pública
const room = await joinRoom('https://bonk.io/123456abcde', {
auth: { type: 'registered', username: '...', password: '...' },
role: 'host', // 'host' (time=1) ou 'spectator' (time=0, apenas o time inicial requisitado)
password: '', // senha da sala, se houver
});
A lib parseia a URL, resolve o servidor e depois conecta. Resolve após o packet ROOM_JOIN, ou rejeita com RoomJoinTimeoutError.
Também aceita um ResolvedRoomAddress já pronto (sem chamada HTTP):
const room = await joinRoom(
{ server: 'b2seattle1', joinId: '...', bypass: 'abcde' },
{ auth, role: 'spectator' },
);
Nota:
role: 'spectator'define apenas o time inicial requisitado noJOIN_ROOM(mesmo comportamento do botão "Spectate" do client oficial) — o servidor ainda trata a conexão como um jogador normal daí em diante. Qualquer lógica de sala que decide quem joga (pick systems, etc.) precisa lidar com isso explicitamente.
BonkRoom — eventos e métodosBonkRoom estende EventEmitter3<BonkRoomEvents>. Todos os eventos são tipados.
const state = room.state;
// state.myId — ID numérico do bot nesta sala (null antes de entrar)
// state.hostId — ID numérico do host atual
// state.players — Map<id, PlayerData>
// state.inGame — boolean (partida em andamento)
// state.teamsLocked — boolean
room.currentMap; // blob LZ-String do mapa ativo, ou null (mapa padrão)
// Jogador entrou
room.on('player-join', (packet) => {
console.log(packet.userName, packet.id, packet.level);
});
// Jogador saiu
room.on('player-leave', (packet) => {
console.log('saiu id:', packet.id);
});
// Mensagem de chat (filtra echo do próprio bot automaticamente)
room.on('chat-message', (packet) => {
const nome = room.state.players.get(packet.id)?.userName;
console.log(`[${nome}]: ${packet.message}`);
});
// Link da sala disponível
room.on('share-link', (packet) => {
console.log(`https://bonk.io/${packet.roomId}${packet.bypass}`);
});
// Sala morreu (socket caiu, ban, sala cheia, retries esgotados)
room.on('room-dead', (reason) => {
// reason.kind: 'socket-disconnect' | 'status-banned' | 'status-room_full' | 'max-retries-exceeded'
});
// Sala foi recriada após reconexão
room.on('room-rebuilt', (shareLink) => {
console.log('nova URL:', shareLink);
});
// Todos os packets brutos (antes dos reducers)
room.on('raw-packet', (packet) => {
if (packet.type === 'UNKNOWN') { /* packet não mapeado */ }
});
| Evento | Payload | Descrição |
|---|---|---|
room-join | RoomJoinPacket | Bot entrou na sala |
room-created | RoomCreatedPacket | Bot criou a sala |
player-join | PlayerJoinPacket | Jogador entrou |
player-leave | PlayerLeavePacket | Jogador saiu |
host-leave | HostLeavePacket | Host saiu (newHostId=-1 = sala fechada) |
team-change | TeamChangePacket | Jogador trocou de time |
ready-change | ReadyChangePacket | Jogador marcou/desmarcou pronto |
tabbed | TabbedPacket | Jogador tabou/voltou |
username-change | UsernameChangePacket | Jogador mudou de nome |
player-pings | PlayerPingsPacket | Ping de todos os jogadores |
game-start | GameStartPacket | Partida iniciada |
game-end | GameEndPacket | Partida encerrada |
all-ready-reset | AllReadyResetPacket | Reset de estado pronto |
chat-message | ChatMessagePacket | Mensagem de chat |
player-kick | PlayerKickPacket | Jogador foi kickado |
countdown | CountdownPacket | Countdown iniciado |
abort-countdown | AbortCountdownPacket | Countdown abortado |
teamlock-toggle | TeamlockTogglePacket | Times bloqueados/desbloqueados |
room.setRoomName('Novo Nome');
room.setRoomPassword('nova_senha'); // '' = remover senha
room.startGame();
room.stopGame(); // volta ao lobby
room.startCountdown(3); // countdown de 3s
room.abortCountdown();
room.setMode('b', 'b'); // engine, mode
room.setRounds(5);
room.setMap(lzStringBlob); // troca o mapa ativo
room.chat('Olá!');
room.kickPlayer(id); // kick sem ban
room.banPlayer(id); // ban permanente
// time: 0=spec 1=ffa 2=red 3=blue 4=green 5=yellow
room.setTeam(id, 2);
room.setTeamLock(true);
room.setTeamsEnabled(true);
room.giveHost(id);
room.setNoHostSwap(true); // desativa troca automática de host
room.disconnect(); // idempotente, limpa timers e estado
BonkRoom reconecta automaticamente após desconexões transitórias (socket caiu, servidor reiniciou). A política de backoff é configurável:
const room = await createRoom({
auth,
desiredState: { /* ... */ },
reconnectPolicy: {
maxAttempts: 10, // default
initialDelayMs: 1000, // default: 1s
maxDelayMs: 30_000, // default: 30s
multiplier: 1.5, // default
jitter: true, // full jitter (recomendado)
},
});
Causas terminais (sem retry): status-banned, status-room_full, max-retries-exceeded.
Causas transitórias (com retry): socket-disconnect.
BonkSession — pool de salasPara rodar múltiplas salas com a mesma conta, use BonkSession. Ela compartilha o AuthClient, o token e aplica throttle de token-bucket entre criações.
import { BonkSession } from 'bonktools';
const session = new BonkSession({
auth: { type: 'registered', username: '...', password: '...' },
throttle: {
capacity: 3, // burst máximo de criações simultâneas
refillPerSec: 0.5, // 1 slot reabastecido a cada 2s
},
});
// Pré-autentica uma vez; token reusado em todas as salas
await session.getToken();
// Ouve eventos do pool
session.on('room-added', (localId) => {
const { room } = session.rooms.get(localId)!;
console.log('sala ativa:', room.shareLink);
});
session.on('room-dead-terminal', ({ localId, reason }) => {
console.error('sala terminal:', localId, reason);
});
startFromConfig() — modo declarativoCria todas as salas a partir de um array de configs, com stagger + jitter entre criações. Registra as configs no reconcile loop de 60s (rede de segurança para falhas silenciosas).
await session.startFromConfig({
rooms: [
{ id: 'sala-1', name: 'ATLAS', maxPlayers: 6, mode: 'b', rounds: 3 },
{ id: 'sala-2', name: 'ZEUS', maxPlayers: 8, mode: 'ar', rounds: 5 },
],
throttle: {
maxConcurrentRooms: 10,
roomCreationDelayMs: 3000, // espera mínima entre criações
roomCreationJitterMs: 2000, // + aleatório de até 2s
},
});
addRoom() / removeRoom() — modo imperativo// Adicionar sala avulsa
const localId = await session.addRoom({
id: 'sala-3',
name: 'HERMES',
password: 'segredo',
maxPlayers: 4,
mode: 'sp',
rounds: 3,
});
// Acessar a BonkRoom diretamente
const { room, status } = session.rooms.get(localId)!;
room.chat('Olá!');
// Remover sala
await session.removeRoom(localId);
// Encerrar toda a sessão (idempotente)
await session.destroy();
RoomConfiginterface RoomConfig {
id: string; // identificador único (usado no reconcile)
name: string;
password?: string; // default: ''
maxPlayers?: number; // default: 6
mode?: string; // default: 'b'
rounds?: number; // default: 3
hidden?: boolean; // default: false
map?: string; // blob LZ-String
}
| Status | Significado |
|---|---|
starting | createRoom() ainda não resolveu |
active | sala criada e viva |
dead-transient | morta — será recriada com throttle |
dead-terminal | morta permanentemente (ban, sala cheia, retries esgotados) |
room.lockTeams(); // jogadores não conseguem mais trocar de time nem sair para o spec
room.unlockTeams(); // libera de novo
room.state.teamsLocked; // estado atual
Só o host pode chamar (de outro cliente é ignorado com um aviso). Com os times travados só o host move jogadores (room.setTeam(id, team) continua funcionando). Quem entra depois do lock já vê a sala travada, e o lock é reaplicado sozinho se a sala for reconstruída (room-rebuilt). setTeamLock(boolean) continua disponível; lockTeams/unlockTeams são os atalhos.
O bonk.io não informa placar nem vencedor ao host por pacote nenhum. O ScoreTracker roda a física do football do próprio client do jogo (baixada de bonk.io na primeira execução, não faz parte do pacote) num worker, alimentada com o estado inicial da partida e com os inputs dos jogadores. O resultado é idêntico ao dos clientes, sem navegador e sem ocupar vaga na sala.
import { ScoreTracker } from 'bonktools';
const score = new ScoreTracker(room, { cacheDir: '.cache/bonk-client' });
score.on('score', ({ team, scores }) => room.chat(`ponto do time ${team}: ${scores[3]} x ${scores[2]}`));
score.on('match-winner', ({ team }) => console.log('venceu o time', team)); // 2 vermelho, 3 azul
score.on('error', (e) => console.error('rastreamento desativado', e));
await score.start();
// Estado inicial pelo próprio jogo (qualquer nº de jogadores e ids, sem blobs capturados):
const is = await score.buildInitialState([null, { id: 1, team: 3 }, { id: 2, team: 2 }]);
room.startGame({ is: is ?? undefined });
gs.wl (ou maxScore).error e para.decodeInitialState, encodeInitialState e remapInitialStatePlayers (formato do IS blob documentado em BONK_PROTOCOL.md).room.enableAntiAfk(); // 12 s sem se mexer nem falar no chat (padrão)
room.on('player-afk', (id) => room.kickPlayer(id));
room.on('player-back', (id) => room.chat(`jogador ${id} voltou`));
room.isAfk(playerId); // consulta pontual
Só vigia jogadores em time durante a partida (espectadores e o bot são ignorados). Opções: enableAntiAfk({ thresholdMs, checkIntervalMs }). O movimento é detectado pelos frames de input via WebRTC (evento peer-input); o chat, pelo Socket.IO.
import { RoomCreationTimeoutError, RoomJoinTimeoutError } from 'bonktools';
try {
const room = await createRoom({ auth, desiredState: { /* ... */ } });
} catch (err) {
if (err instanceof RoomCreationTimeoutError) {
// SHARE_LINK não chegou dentro do timeout
}
}
try {
const room = await joinRoom('https://bonk.io/...', { auth });
} catch (err) {
if (err instanceof RoomJoinTimeoutError) {
// ROOM_JOIN não chegou dentro do timeout (ex: sala cheia)
}
}
O IS blob (estado inicial da física) depende do mapa ativo na sala — capturar um novo exige um client real do bonk.io iniciando a partida (veja Decisões técnicas). Pra evitar que todo mundo precise capturar os mesmos blobs dos mapas padrão de cada gamemode, registry/blobs.json espelha publicamente os que este projeto já capturou:
const res = await fetch('https://cdn.jsdelivr.net/gh/brenoluizdev/bonktools@main/registry/blobs.json');
const registry = await res.json();
const blob = registry.gamemodeDefaults.vtol.blobs['2']; // vtol, mapa padrão, 1v1
Os mesmos dados já vêm embutidos na lib via GAMEMODE_DEFAULT_BLOBS/FOOTBALL_DEFAULT_BLOBS — o registro é útil pra quem não usa TypeScript/o pacote npm diretamente, ou quer os dados mais recentes sem esperar um novo release. Detalhes do formato e como contribuir: registry/README.md.
Por que socket.io-client@2 e não a versão mais recente?
O servidor bonk.io fala o protocolo Engine.IO 3 (EIO=3). O cliente v4 negocia EIO=4 e não tem opção de downgrade — a conexão falha imediatamente no handshake. A versão 2.5.0 é a última da linha v2 e a única compatível.
Por que undici e não fetch nativo?
O bonk.io serve uma cadeia TLS Sectigo incompleta. O fetch nativo do Node.js não permite injetar uma CA customizada por requisição sem monkeypatch global. O undici.Agent permite configurar a CA Sectigo por cliente, sem afetar outras requisições HTTPS do processo.
Por que o CA Sectigo está bundlado?
Usar o TLS store padrão do Node.js rejeitaria a cadeia incompleta do bonk.io. Ao invés de desativar toda a verificação TLS globalmente (NODE_TLS_REJECT_UNAUTHORIZED=0), o projeto bundla a cadeia completa Sectigo e injeta apenas onde necessário.
Por que EventEmitter3 e não o EventEmitter nativo?
EventEmitter3 tem tipagem genérica por evento (EventEmitter<Events>), o que permite room.on('player-join', handler) com o tipo do handler inferido corretamente pelo TypeScript.
username, password) nunca são logadas — apenas eventos de sucesso/falha.undici.Agent — sem NODE_TLS_REJECT_UNAUTHORIZED=0 global.process.env — nunca hardcode credenciais no código.bonktools é um cliente não-oficial, resultado de engenharia reversa do protocolo Socket.IO público do bonk.io. Não é afiliado, endossado ou mantido pela equipe do bonk.io. Use por sua conta e risco, respeitando os termos de serviço do jogo — o projeto existe para automação, bots e ferramentas legítimas (salas 24h, moderação, integração com outras plataformas), não para lag/DDoS, spam ou qualquer forma de abuso.
Veja CONTRIBUTING.md.
gamemode-change | GamemodeChangePacket | Modo de jogo alterado |
change-rounds | ChangeRoundsPacket | Número de rounds alterado |
map-switch | MapSwitchPacket | Mapa trocado |
balance-set | BalanceSetPacket | Balance de jogador alterado |
player-level-up | PlayerLevelUpPacket | Jogador subiu de nível |
room-name-update | RoomNameUpdatePacket | Nome da sala alterado |
room-password-update | RoomPasswordUpdatePacket | Senha da sala alterada |
status-message | StatusMessagePacket | Mensagem de status do servidor |
share-link | ShareLinkPacket | Link da sala disponível |
room-dead | RoomDeadReason | Sala morreu |
room-rebuilt | string (shareLink) | Sala reconectada |
raw-packet | IncomingPacket | UnknownPacket | Todo packet bruto |