bonktools 3.2.0 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/bot.js DELETED
@@ -1,1470 +0,0 @@
1
- /**
2
- * Main Bot class for BonkBot
3
- */
4
- const EventEmitter = require('events');
5
- const io = require('socket.io-client');
6
- const axios = require('axios');
7
- const https = require('https');
8
- const {
9
- createLogger
10
- } = require('./utils/logger');
11
- const {
12
- DEFAULT_SERVER,
13
- DEFAULT_AVATAR,
14
- GAMEMODE_NAMES,
15
- ENGINE_NAMES,
16
- TEAM_NAMES,
17
- CLIENT_MESSAGE_TYPES,
18
- API,
19
- SERVER_MESSAGE_TYPES
20
- } = require('./utils/constants');
21
- const {
22
- parsePacket
23
- } = require('./packet');
24
- const { LOG_LEVELS } = require("./utils/logger");
25
-
26
- // Create logger
27
- const logger = createLogger('BonkBot');
28
-
29
- const httpsAgent = new https.Agent({
30
- rejectUnauthorized: false
31
- });
32
-
33
- /**
34
- * Main BonkBot class
35
- */
36
- class BonkBot {
37
- /**
38
- * Create a new BonkBot instance
39
- * @param {Object} options - Bot options
40
- * @param {Object} options.account - Account information
41
- * @param {string} [options.account.username] - Username
42
- * @param {string} [options.account.password] - Password
43
- * @param {boolean} [options.account.guest=true] - Whether the account is a guest
44
- * @param {string} [options.avatar] - Bot avatar
45
- * @param {string} [options.server=b2ny1] - Server to connect to
46
- * @param {string} [options.bypass] - Pass bypass
47
- * @param {string} [options.token] - Authentication token
48
- * @param {string} [options.peerID] - Peer ID
49
- * @param {number} [options.logLevel=LOG_LEVELS.INFO] - Log level
50
- */
51
- constructor(options = {}) {
52
- // Set log level
53
- if (options.logLevel !== undefined) {
54
- logger.setLevel(options.logLevel);
55
- }
56
-
57
- logger.info('Creating new BonkBot instance');
58
-
59
- // Initialize properties
60
- this.PROTOCOL_VERSION = options.PROTOCOL_VERSION
61
- this.HARDCODED_PROTOCOL_VERSION = 49; // dont change this, pass the property to the function
62
-
63
- if(this.PROTOCOL_VERSION == undefined){
64
- logger.warn("You should really set the PROTOCOL_VERSION to the correct one in the createBot() options object, without it the bot may fail to start")
65
- logger.warn("Defaulting PROTOCOL_VERSION to: " + this.HARDCODED_PROTOCOL_VERSION)
66
- this.PROTOCOL_VERSION = this.HARDCODED_PROTOCOL_VERSION;
67
- }
68
-
69
- this.account = this.validateAccount(options.account || {});
70
- this.avatar = options.avatar || DEFAULT_AVATAR;
71
- this.server = options.server || DEFAULT_SERVER;
72
- this.bypass = options.bypass;
73
- this.token = options.token;
74
- this.peerID = options.peerID || this.generatePeerId();
75
-
76
-
77
- // Create event emitter
78
- this.events = new EventEmitter();
79
-
80
- // Socket connection
81
- this.socket = null;
82
- this.connected = false;
83
- this.keepAliveTimer = null;
84
- this.timeSyncCount = 1;
85
-
86
- this.location = {
87
- lat: 0,
88
- long: 0,
89
- country: "US",
90
- server: "b2ny1"
91
- }
92
-
93
- this.timeSync = {
94
- count: 0,
95
- last_sync: 0,
96
- last_sync_id: 0,
97
- latency: 0
98
- }
99
-
100
- // Room information
101
- this.room = {
102
- id: null,
103
- name: null,
104
- address: null,
105
- server: this.server,
106
- bypass: this.bypass,
107
- teamsLocked: false,
108
- password: null,
109
- countdown: false,
110
-
111
- state: false,
112
- map: false,
113
- inGame: false,
114
- roundStartTime: 0,
115
- rounds: 3,
116
-
117
- gt: false, // ?
118
-
119
- quickplay: false,
120
- teams: false,
121
- mode: false,
122
- balance: false,
123
- inputs: false,
124
- framecount: false,
125
- stateID: false,
126
- admin: false,
127
- random: false,
128
- };
129
-
130
- // Game state
131
- this.game = {
132
- id: null,
133
- host: null,
134
- banned: false
135
- };
136
-
137
- // Player tracking
138
- this.players = new Map();
139
- }
140
-
141
- /**
142
- * Initialize the bot
143
- * @returns {Promise<BonkBot>} This bot instance
144
- */
145
- async init() {
146
- logger.info('Initializing BonkBot');
147
-
148
- try {
149
- // Get authentication token if needed
150
- if (!this.account.guest && !this.token) {
151
- this.token = await this.getToken(this.account.username, this.account.password);
152
- }
153
-
154
- // Get server information if using default
155
- if (!this.server || this.server === DEFAULT_SERVER) {
156
- logger.info('Getting server information');
157
- const serverInfo = await this.getServerInfo();
158
- this.server = serverInfo.server;
159
-
160
- this.location = serverInfo;
161
-
162
- this.room.server = this.server;
163
- logger.info(`Using server: ${this.server}`);
164
- }
165
-
166
- logger.info('BonkBot initialized');
167
- this.events.emit('ready');
168
-
169
- return this;
170
- } catch (error) {
171
- logger.error('Failed to initialize BonkBot', error);
172
- throw error;
173
- }
174
- }
175
-
176
- /**
177
- * Start the keep alive timer
178
- * @private
179
- */
180
- startKeepAlive() {
181
- this.keepAliveTimer = setInterval(() => {
182
- if (this.connected) {
183
- // Verify the socket is still connected before sending keep-alive
184
- if (this.socket && this.socket.connected) {
185
- this.sendTimesync();
186
- } else {
187
- logger.warn('Keep-alive detected disconnected socket');
188
-
189
- this.stopBot();
190
- this.events.emit('disconnect');
191
- }
192
- }
193
- }, 5000);
194
- }
195
-
196
- /**
197
- * Set the room address
198
- * @param {Object} addressInfo - Room address information
199
- */
200
- setAddress(addressInfo) {
201
- logger.info('Setting room address');
202
-
203
- if (!addressInfo.address || !addressInfo.roomname || !addressInfo.server) {
204
- throw new Error('Invalid room address information');
205
- }
206
-
207
- this.room.address = addressInfo.address;
208
- this.room.name = addressInfo.roomname;
209
- this.room.server = addressInfo.server;
210
- this.room.bypass = addressInfo.bypass || '';
211
-
212
- // Update server if different
213
- if (this.server !== addressInfo.server) {
214
- this.server = addressInfo.server;
215
- }
216
-
217
- logger.info(`Set room address: ${this.room.name} (${this.room.address})`);
218
- }
219
-
220
- /**
221
- * Connect to the server
222
- * @returns {Promise<BonkBot>} This bot instance
223
- */
224
- async connect() {
225
- if (this.connected) {
226
- logger.warn('Already connected, disconnecting first');
227
- this.disconnect();
228
- }
229
-
230
- // Disable TLS certificate verification for bonk.io servers
231
- // NOTE: This is required because bonk.io uses Sectigo certificates with incomplete chains
232
- // and socket.io-client v2.x with engine.io-client v3.x doesn't properly pass
233
- // rejectUnauthorized option to the underlying ws library
234
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
235
-
236
- logger.info(`Connecting to server: ${this.server}`);
237
-
238
- const socketAddr = `https://${this.server}.bonk.io`;
239
-
240
- return new Promise((resolve, reject) => {
241
- try {
242
- // Configure socket.io options
243
- const socketOptions = {
244
- transports: ['websocket'],
245
- reconnection: false,
246
- timeout: 10000,
247
- forceNew: true,
248
- path: '/socket.io',
249
- // NOTE: socket.io-client v2.x with engine.io-client v3.x has a bug where
250
- // custom CA certificates cannot be properly passed to the websocket transport.
251
- // As a workaround, we disable certificate verification for bonk.io servers
252
- // which use Sectigo certificates with incomplete chains.
253
- rejectUnauthorized: false
254
- };
255
-
256
- logger.info('Creating Socket.IO connection with certificate verification disabled');
257
- this.socket = io(socketAddr, socketOptions);
258
-
259
- // Set up connection timeout
260
- const timeout = setTimeout(() => {
261
- if (!this.connected) {
262
- reject(new Error(`Connection timeout to server: ${this.server}`));
263
- this.stopBot();
264
- }
265
- }, 10000);
266
-
267
- // Connection opened
268
- this.socket.on('connect', () => {
269
- logger.info('Socket.IO connection established');
270
-
271
- clearTimeout(timeout);
272
- this.connected = true;
273
-
274
- // Set up event handlers
275
- this.setupSocketEvents();
276
-
277
- // Start keep alive timer
278
- this.startKeepAlive();
279
-
280
- // Emit connect event
281
- this.events.emit('connect');
282
-
283
- resolve(this);
284
- });
285
-
286
- // Connection error
287
- this.socket.on('connect_error', (error) => {
288
- logger.error('Socket.IO connection error:', error.message || error);
289
-
290
- if (!this.connected) {
291
- clearTimeout(timeout);
292
- const errorMsg = error.message || error.description?.message || 'Unknown error';
293
- reject(new Error(`Failed to connect to server: ${errorMsg}`));
294
- }
295
-
296
- this.events.emit('error', error);
297
- });
298
-
299
- // Connection error (fallback handler)
300
- this.socket.on('error', (error) => {
301
- logger.error('Socket.IO error:', error.message || error);
302
-
303
- if (!this.connected) {
304
- clearTimeout(timeout);
305
- const errorMsg = error.message || error.description?.message || 'Unknown error';
306
- reject(new Error(`Failed to connect to server: ${errorMsg}`));
307
- }
308
-
309
- this.events.emit('error', error);
310
- });
311
-
312
- // Connection closed
313
- this.socket.on('disconnect', (reason) => {
314
- logger.info(`Socket.IO connection closed: ${reason}`);
315
-
316
- if (!this.connected) {
317
- clearTimeout(timeout);
318
- reject(new Error(`Connection closed before fully established: ${reason}`));
319
- }
320
-
321
- this.stopBot();
322
- this.events.emit('disconnect');
323
- });
324
- } catch (error) {
325
- reject(new Error(`Failed to create Socket.IO connection: ${error.message}`));
326
- }
327
- });
328
- }
329
-
330
- /**
331
- * Disconnect from the server
332
- */
333
- disconnect() {
334
- if (!this.connected) {
335
- logger.warn('Not connected, nothing to disconnect');
336
- return;
337
- }
338
-
339
- logger.info('Disconnecting from server');
340
-
341
- this.stopBot();
342
-
343
- if (this.socket) {
344
- this.socket.disconnect();
345
- this.socket = null;
346
- }
347
- return true;
348
- }
349
-
350
-
351
- async getAddressFromUrl(url) {
352
- const regex = /\/(\d{6})([a-zA-Z0-9]{5})?$/;
353
- const match = url.match(regex);
354
-
355
- if (!match) {
356
- return null;
357
- }
358
-
359
- const id = match[1];
360
- const bypass = match[2] || "";
361
-
362
- const data = new URLSearchParams();
363
- data.append('joinID', id);
364
-
365
- try {
366
- const response = await axios.post(API.AUTOJOIN, data.toString(), {
367
- headers: {
368
- 'Content-Type': 'application/x-www-form-urlencoded'
369
- },
370
- httpsAgent: httpsAgent
371
- });
372
-
373
- const result = response.data;
374
-
375
- if (result.r == "success") {
376
- result.bypass = bypass;
377
- }
378
-
379
- return result;
380
- } catch (error) {
381
- console.error('Error getting join link:', error);
382
- throw error; // Re-throw to allow caller to handle the error
383
- }
384
- }
385
-
386
- /**
387
- * Join a room
388
- * @param {Object} [options] - Join options
389
- * @returns {Promise<void>} Resolves when joined
390
- */
391
- async joinRoom(options = {}) {
392
- if (!this.connected) {
393
- throw new Error('Not connected to server');
394
- }
395
-
396
- if (!this.room.address) {
397
- throw new Error('Room address not set');
398
- }
399
-
400
- logger.info(`Joining room: ${this.room.name} (${this.room.address})`);
401
-
402
- // Prepare join data
403
- const joinData = {
404
- joinID: this.room.address,
405
- roomPassword: options.password ? options.password.toString() : '',
406
- guest: this.account.guest,
407
- dbid: 2,
408
- version: this.PROTOCOL_VERSION,
409
- peerID: options.peerID || this.peerID,
410
- bypass: this.room.bypass || '',
411
- avatar: this.avatar
412
- };
413
-
414
- // Add account-specific data
415
- if (this.account.guest) {
416
- joinData.guestName = this.account.username;
417
- } else {
418
- joinData.token = this.token;
419
- }
420
-
421
- // Send join message
422
- await this.sendMessage(CLIENT_MESSAGE_TYPES.JOIN_ROOM, joinData);
423
-
424
- logger.info(`Join request sent for room: ${this.room.name}`);
425
- }
426
-
427
- /**
428
- * Create a new room
429
- * @param {Object} [options] - Room options
430
- * @returns {Promise<Object>} Room address information
431
- */
432
- async createRoom(options = {}) {
433
- if (!this.connected) {
434
- throw new Error('Not connected to server');
435
- }
436
-
437
- logger.info('Creating new room');
438
-
439
- // Set room info
440
- this.room.name = options.roomname || `BonkBot Room ${Math.floor(Math.random() * 1000)}`;
441
- this.room.maxPlayers = options.maxplayers || 8;
442
- this.room.password = options.roompassword || '';
443
-
444
- // Prepare create data
445
- const createData = {
446
- peerID: options.peerID ?? this.peerID,
447
- roomName: options.roomName ?? this.room.name,
448
- maxPlayers: options.maxPlayers ?? this.room.maxPlayers,
449
- password: options.password ?? this.room.password,
450
- dbid: options.dbid ?? 11822936,
451
- guest: options.guest ?? this.account.guest,
452
- minLevel: options.minLevel ?? 0,
453
- maxLevel: options.maxLevel ?? 999,
454
- latitude: options.latitude ?? this.location.lat,
455
- longitude: options.longitude ?? this.location.long,
456
- country: options.country ?? this.location.country,
457
- version: this.PROTOCOL_VERSION,
458
- hidden: options.hidden ? 1 : 0,
459
- quick: options.quick ?? false,
460
- mode: options.mode ?? 'custom',
461
- token: options.token ?? (this.token || ''),
462
- avatar: options.avatar ?? this.avatar,
463
- };
464
- if(createData.guest){
465
- createData.guestName = this.account.username;
466
- }
467
-
468
- // Send create message
469
- await this.sendMessage(CLIENT_MESSAGE_TYPES.CREATE_ROOM, createData);
470
-
471
- // set the only player to yorself
472
- this.players.set(0, {
473
- peerID: createData.peerID,
474
- guest: createData.guest,
475
- team: 1,
476
- teamName: TEAM_NAMES[1],
477
- level: 0,
478
- ready: false,
479
- tabbed: false,
480
- avatar: createData.avatar,
481
- id: 0,
482
- username: createData.guestName,
483
- xp: 0,
484
- ping: 0,
485
- balance: 0,
486
- host: true,
487
- movement: {
488
- input: 0,
489
- frame: 0,
490
- sequence: 0
491
- }
492
- });
493
-
494
- this.game.id = 0
495
- this.game.host = 0
496
-
497
- logger.info(`Room creation request sent: ${this.room.name}`);
498
-
499
- this.events.emit('CREATE_ROOM');
500
-
501
- // Return room info
502
- return {
503
- address: this.room.address,
504
- roomname: this.room.name,
505
- server: this.room.server,
506
- bypass: this.room.bypass
507
- };
508
- }
509
-
510
- /**
511
- * Get a player by ID
512
- * @param {string|number} id - Player ID
513
- * @returns {Object|null} Player object or null if not found
514
- */
515
- getPlayerByID(id) {
516
- return this.players.get(id) || null;
517
- }
518
-
519
- /**
520
- * Get a players id by username
521
- * @param {string} username - Player username
522
- * @param {boolean} guest - Whether the player is a guest
523
- * @returns {number} Player ID
524
- */
525
- getPlayerIDByUsername(username, guest = false) {
526
- for (const player of this.players.values()) {
527
- console.log(player)
528
- if (player.username == username && player.guest == guest) {
529
- return player.id;
530
- }
531
- }
532
-
533
- return -1;
534
- }
535
-
536
- /**
537
- * Get all players
538
- * @returns {Array<Object>} Array of player objects
539
- */
540
- getAllPlayers() {
541
- const players = [];
542
-
543
- for (const player of this.players.values()) {
544
- players.push(player);
545
- }
546
-
547
- return players;
548
- }
549
-
550
-
551
-
552
- /**
553
- * Automatically handle a packet
554
- * @param {Object} packet - Packet to handle
555
- */
556
- async autoHandlePacket(packet) {
557
- switch (packet.type) {
558
-
559
- case 'ROOM_SHARE_LINK':
560
- this.room.dbid = packet.roomId;
561
- this.room.bypass = packet.roomBypass;
562
-
563
- this.events.emit('ROOM_SHARE_LINK', { url: this.getShareLink() });
564
- break;
565
-
566
- case 'MAP_SWITCH':
567
- this.room.map = packet.mapdata;
568
- this.events.emit('MAP_SWITCH', { map: this.room.map });
569
- break;
570
-
571
- case 'MAP_SUGGEST':
572
- const mapSuggestion = {
573
- title: packet.maptitle,
574
- author: packet.mapauthor,
575
- player: this.players.get(packet.id)
576
- }
577
- this.events.emit('MAP_SUGGEST', mapSuggestion);
578
- break;
579
-
580
- case 'CHANGE_ROUNDS':
581
- this.room.rounds = packet.rounds;
582
- this.events.emit('CHANGE_ROUNDS', { rounds: packet.rounds });
583
- break;
584
-
585
- case 'COUNTDOWN':
586
- this.room.countdown = packet.countdown;
587
- this.events.emit('COUNTDOWN', { countdown: packet.countdown });
588
- break;
589
-
590
- case 'GAME_START':
591
- // THIS NEEDS WORK
592
- // COULD CAUSE DESYNC IF PLAYERS ARE FKING WITH THE STATE OBJ WITHOUT TELLING OTHERS
593
-
594
- this.room.inGame = true;
595
- this.room.roundStartTime = packet.timestamp;
596
- this.room.state = packet.state;
597
-
598
- this.room.map = packet.state.map;
599
- this.room.gt = packet.state.gt;
600
- this.room.rounds = packet.state.wl;
601
- this.room.quickplay = packet.state.q;
602
- this.room.teamsLocked = packet.state.tl;
603
- this.room.teams = packet.state.tea;
604
- this.room.engine = ENGINE_NAMES[packet.state.ga];
605
- this.room.mode = GAMEMODE_NAMES[packet.state.mo];
606
-
607
- // apply balances to all the players
608
- // bal[playerid] = num
609
- this.players.forEach((player) => {
610
- player.balance = packet.state.bal[player.id] || 0;
611
- this.players.set(player.id, player);
612
- });
613
-
614
- this.events.emit('GAME_START');
615
- break;
616
-
617
- case 'GAME_END':
618
- this.room.inGame = false;
619
- this.events.emit('GAME_END');
620
- break;
621
-
622
- case 'GAMEMODE_CHANGE':
623
- this.room.mode = GAMEMODE_NAMES[packet.mode];
624
- this.room.engine = ENGINE_NAMES[packet.engine];
625
-
626
- this.events.emit('GAMEMODE_CHANGE', { mode: this.room.mode, engine: this.room.engine });
627
- break;
628
-
629
- case 'BALANCE_SET':
630
- const playerBalance = this.players.get(packet.id);
631
-
632
- if (playerBalance) {
633
- playerBalance.balance = packet.balance;
634
- this.players.set(packet.id, playerBalance);
635
- }
636
-
637
- this.events.emit('BALANCE_SET', { player: playerBalance, balance: packet.balance });
638
- break;
639
-
640
- case 'TIMESYNC':
641
- this.timeSync.last_sync = packet.time;
642
- this.timeSync.last_sync_id = packet.id;
643
- this.timeSync.latency = Date.now() - this.timeSync.last_sync;
644
- break;
645
-
646
- case 'TEAM_CHANGE':
647
- // Update player team
648
- const playerTeam = this.players.get(packet.id);
649
-
650
- if (playerTeam) {
651
- playerTeam.team = packet.team;
652
- playerTeam.teamName = TEAM_NAMES[packet.team];
653
- this.players.set(packet.id, playerTeam);
654
- }
655
-
656
- // Emit team change event
657
- this.events.emit('TEAM_CHANGE', { player: playerTeam, team: packet.team });
658
- break;
659
-
660
- case 'PLAYER_PINGS':
661
- // Update player pings
662
- // pings: { '0': 14 }
663
- for (let [id, ping] of Object.entries(packet.pings)) {
664
- id = parseInt(id)
665
- const playerPing = this.players.get(id);
666
- playerPing.ping = ping;
667
- this.players.set(id, playerPing);
668
- }
669
-
670
- await this.sendMessage(CLIENT_MESSAGE_TYPES.PING_RESPONSE, { id: packet.pingId })
671
- break;
672
-
673
- case 'CHAT_MESSAGE':
674
- // Handle chat message
675
- const player = this.players.get(packet.id);
676
-
677
- this.events.emit('CHAT_MESSAGE', { player: player, message: packet.message });
678
- break;
679
-
680
- case 'JOIN_ROOM':
681
- // Set game info
682
- this.game.id = packet.myid;
683
- this.game.host = packet.hostid;
684
- // Set room info
685
- this.room.id = packet.roomid;
686
- this.room.bypass = packet.roombypass;
687
- this.room.teamsLocked = packet.teamsLocked;
688
-
689
- // Add players
690
- if (packet.playerdata && Array.isArray(packet.playerdata)) {
691
- for (let i = 0; i < packet.playerdata.length; i++) {
692
- const playerData = packet.playerdata[i];
693
- if (playerData) {
694
- playerData.id = i;
695
-
696
- playerData.username = playerData.userName;
697
- delete playerData.userName;
698
-
699
- playerData.xp = this.levelToXP(playerData.level)
700
- playerData.ping = 0;
701
- playerData.balance = 0;
702
- playerData.movement = {
703
- input: 0,
704
- frame: 0,
705
- sequence: 0
706
- }
707
-
708
- this.players.set(i, playerData);
709
- }
710
- }
711
- }
712
-
713
- packet.players = this.players;
714
- delete packet.playerdata;
715
-
716
- // Emit join event
717
- this.events.emit('JOIN', { game: this.game, room: this.room, players: this.players });
718
- break;
719
-
720
- case 'INITIAL_DATA':
721
-
722
- this.room.engine = ENGINE_NAMES[packet.ga];
723
- this.room.mode = GAMEMODE_NAMES[packet.mo];
724
-
725
- this.room.gt = packet.gt;
726
- this.room.rounds = packet.wl;
727
- this.room.quickplay = packet.q;
728
- this.room.teamsLocked = packet.tl;
729
- this.room.teams = packet.tea;
730
- this.room.framecount = packet.fc;
731
- this.room.stateID = packet.stateID;
732
- this.room.admin = packet.admin;
733
- this.room.map = packet.gs ? packet.gs.map : null;
734
- this.room.state = packet.state;
735
- this.room.random = packet.random;
736
-
737
- // apply balances to all the players
738
- // bal[playerid] = num
739
- this.players.forEach((player) => {
740
- player.balance = packet.bal[player.id] || 0;
741
- this.players.set(player.id, player);
742
- });
743
- break;
744
-
745
- case 'PLAYER_JOIN':
746
- // Add player
747
- this.players.set(packet.id, {
748
- peerID: packet.peerID,
749
- guest: packet.guest,
750
- team: 1,
751
- teamName: TEAM_NAMES[1],
752
- level: parseInt(packet.level),
753
- ready: false,
754
- tabbed: false,
755
- avatar: packet.avatar,
756
- id: packet.id,
757
- username: packet.username,
758
- xp: this.levelToXP(packet.level),
759
- ping: 0,
760
- balance: 0,
761
- host: false,
762
- movement: {
763
- input: 0,
764
- frame: 0,
765
- sequence: 0
766
- }
767
- });
768
-
769
- // check if the bot is host
770
- const botPlayer = this.players.get(this.game.id);
771
- if (botPlayer.host) {
772
- await this.sendMessage(CLIENT_MESSAGE_TYPES.INFORM_IN_LOBBY, {
773
- sid: packet.id,
774
- gs: {
775
- map: this.room.map || {
776
- v: 13,
777
- s: {
778
- re: false,
779
- nc: false,
780
- pq: 1,
781
- gd: 25,
782
- fl: false
783
- },
784
- physics: {
785
- shapes: [],
786
- fixtures: [],
787
- bodies: [],
788
- bro: [],
789
- joints: [],
790
- ppm: 12
791
- },
792
- spawns: [],
793
- capZones: [],
794
- m: {
795
- a: "BonkBot",
796
- n: "Empty Map",
797
- dbv: 2,
798
- dbid: 767645,
799
- authid: -1,
800
- date: "",
801
- rxid: 0,
802
- rxn: "",
803
- rxa: "",
804
- rxdb: 1,
805
- cr: ["uint32"],
806
- pub: true,
807
- mo: ""
808
- }
809
- },
810
- gt: this.room.gt || 2,
811
- wl: this.room.rounds || 3,
812
- q: this.room.quickplay || false,
813
- tl: this.room.teamsLocked || false,
814
- tea: this.room.teams || false,
815
- ga: "b",
816
- mo: "b",
817
- bal: this.getAllPlayers().reduce((balances, player) => {
818
- if (player.balance) {
819
- balances[player.id] = player.balance;
820
- }
821
- return balances;
822
- }, {})
823
- }
824
- });
825
- }
826
-
827
- // Emit join event
828
- this.events.emit('PLAYER_JOIN', { player: this.players.get(packet.id), id: packet.id });
829
- break;
830
-
831
- case 'PLAYER_LEAVE':
832
- const deletedPlayer = this.players.get(packet.id);
833
-
834
- // delete player
835
- this.players.delete(packet.id);
836
-
837
- // Emit leave event
838
- this.events.emit('PLAYER_LEAVE', { player: deletedPlayer, id: packet.id });
839
- break;
840
-
841
- case 'HOST_TRANSFER':
842
- // Update host
843
- this.game.host = packet.newHost;
844
-
845
- // find the player that is now host
846
- this.players.forEach((player) => {
847
- if (player.id === packet.newHost) {
848
- player.host = true;
849
- } else {
850
- player.host = false;
851
- }
852
- this.players.set(player.id, player);
853
- });
854
-
855
- // Emit host transfer event
856
- this.events.emit('HOST_TRANSFER', { oldHost: packet.oldHost, newHost: packet.newHost });
857
- break;
858
-
859
- case 'READY_CHANGE':
860
- // Update player ready status
861
- const playerReady = this.players.get(packet.id);
862
-
863
- if (playerReady) {
864
- playerReady.ready = packet.ready;
865
-
866
- this.players.set(packet.id, playerReady);
867
- }
868
-
869
- // Emit ready change event
870
- this.events.emit('READY_CHANGE', { player: playerReady, ready: packet.ready });
871
- break;
872
-
873
- case 'TEAMLOCK_TOGGLE':
874
- // Update teams lock
875
- this.room.teamsLocked = packet.teamsLocked;
876
-
877
- // Emit teams lock event
878
- this.events.emit('TEAMLOCK_TOGGLE', { teamsLocked: packet.teamsLocked });
879
- break;
880
-
881
- case 'PLAYER_TABBED':
882
- // Update player tabbed status
883
- const playerTabbed = this.players.get(packet.id);
884
-
885
- if (playerTabbed) {
886
- playerTabbed.tabbed = packet.tabbed;
887
-
888
- this.players.set(packet.id, playerTabbed);
889
- }
890
-
891
- // Emit tabbed event
892
- this.events.emit('PLAYER_TABBED', { player: playerTabbed, tabbed: packet.tabbed });
893
- break;
894
-
895
- case 'ROOM_NAME_UPDATE':
896
- // Update room name
897
- this.room.name = packet.name;
898
-
899
- // Emit room name change event
900
- this.events.emit('ROOM_NAME_UPDATE', { name: packet.name });
901
- break;
902
-
903
- case 'ROOM_ADDRESS':
904
- // Update room address
905
- this.room.address = packet.address;
906
-
907
- // Emit room address event
908
- this.events.emit('ROOM_ADDRESS', { address: packet.address });
909
- break;
910
-
911
- case 'PLAYER_KICK':
912
- // Check if we were kicked
913
- if (packet.id === this.game.id) {
914
- this.game.banned = true; // might not act be banned
915
- }
916
-
917
- // Remove player
918
- const kickedPlayer = this.players.get(packet.id);
919
- this.players.delete(packet.id);
920
-
921
- // Emit kick event
922
- this.events.emit('PLAYER_KICK', kickedPlayer);
923
- break;
924
-
925
- case 'PLAYER_INPUT':
926
- // Handle player input
927
- const playerInput = this.players.get(packet.id);
928
-
929
- if (playerInput) {
930
- playerInput.movement = {
931
- input: packet.input,
932
- frame: packet.frame,
933
- sequence: packet.sequence
934
- }
935
-
936
- this.players.set(packet.id, playerInput);
937
- }
938
-
939
- // Emit player input event
940
- this.events.emit('PLAYER_INPUT', { player: playerInput, movement: playerInput.movement });
941
- break;
942
-
943
-
944
- default:
945
- logger.debug(`No handler for packet type: ${packet.type}. Tell the developer about this for a fix!\nhttps://github.com/PixelMelt/BonkBot`, packet);
946
- break;
947
- }
948
- }
949
-
950
- /**
951
- * Validate account information
952
- * @private
953
- * @param {Object} account - Account information
954
- * @returns {Object} Validated account information
955
- */
956
- validateAccount(account) {
957
- // Default to guest account
958
- const validatedAccount = {
959
- guest: true,
960
- username: `BonkBot${Math.floor(Math.random() * 10000)}`
961
- };
962
-
963
- // Override with provided values
964
- if (account.guest !== undefined) {
965
- validatedAccount.guest = !!account.guest;
966
- }
967
-
968
- if (account.username) {
969
- validatedAccount.username = account.username;
970
- }
971
-
972
- if (account.password) {
973
- validatedAccount.password = account.password;
974
- }
975
-
976
- // Non-guest accounts must have a password
977
- if (!validatedAccount.guest && !validatedAccount.password) {
978
- logger.warn('Non-guest account must have a password, defaulting to guest');
979
- validatedAccount.guest = true;
980
- }
981
-
982
- return validatedAccount;
983
- }
984
-
985
- /**
986
- * Generate a random peer ID
987
- * @private
988
- * @returns {string} Random peer ID
989
- */
990
- generatePeerId() {
991
- return Math.random().toString(36).substr(2, 10) + 'v00000';
992
- }
993
-
994
- /**
995
- * Get authentication token
996
- * @private
997
- * @param {string} username - Username
998
- * @param {string} password - Password
999
- * @returns {Promise<string>} Authentication token
1000
- */
1001
- async getToken(username, password) {
1002
- try {
1003
- logger.info(`Getting token for user: ${username}`);
1004
-
1005
- const response = await axios.post(API.LOGIN,
1006
- `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}&remember=true`, {
1007
- headers: {
1008
- 'Content-Type': 'application/x-www-form-urlencoded'
1009
- },
1010
- httpsAgent: httpsAgent
1011
- }
1012
- );
1013
-
1014
- if (!response.data || !response.data.token) {
1015
- throw new Error('Failed to get authentication token');
1016
- }
1017
-
1018
- logger.info('Successfully obtained authentication token');
1019
- return response.data.token;
1020
- } catch (error) {
1021
- logger.error('Failed to authenticate', error);
1022
- throw new Error(`Failed to authenticate: ${error.message}`);
1023
- }
1024
- }
1025
-
1026
- /**
1027
- * Get a room address from a room name
1028
- * @param {string} roomName - Room name
1029
- * @returns {Promise<Object>} Room address information
1030
- */
1031
- async getAddressFromRoomName(roomName) {
1032
- logger.info(`Getting address for room: ${roomName}`);
1033
-
1034
- try {
1035
- // Find room by name
1036
- const room = await this.getRoomByName(roomName, this.token);
1037
-
1038
- if (!room) {
1039
- throw new Error(`Room not found: ${roomName}`);
1040
- }
1041
-
1042
- // Get room address
1043
- const address = await this.getRoomAddress(room.id);
1044
-
1045
- const result = {
1046
- roomname: room.roomname,
1047
- address: address.address,
1048
- server: address.server,
1049
- bypass: ''
1050
- };
1051
-
1052
- logger.info(`Got address for room: ${roomName}`);
1053
-
1054
- return result;
1055
- } catch (error) {
1056
- logger.error(`Failed to get address for room: ${roomName}`, error);
1057
- throw error;
1058
- }
1059
- }
1060
-
1061
- /**
1062
- * Get server information
1063
- * @private
1064
- * @param {string} [token] - Authentication token
1065
- * @returns {Promise<Object>} Server information
1066
- */
1067
- async getServerInfo() {
1068
- try {
1069
- const response = await axios.post(API.GET_ROOMS,
1070
- `version=${this.PROTOCOL_VERSION}&gl=y&token=${this.token ?? ""}`, {
1071
- headers: {
1072
- 'Content-Type': 'application/x-www-form-urlencoded'
1073
- },
1074
- httpsAgent: httpsAgent
1075
- }
1076
- );
1077
-
1078
- if (!response.data || !response.data.createserver) {
1079
- throw new Error('Failed to get server information');
1080
- }
1081
-
1082
- return {
1083
- server: response.data.createserver,
1084
- lat: response.data.lat,
1085
- long: response.data.long,
1086
- country: response.data.country
1087
- };
1088
- } catch (error) {
1089
- logger.error(error);
1090
- throw new Error(`Failed to get server information: ${error.message}`);
1091
- }
1092
- }
1093
-
1094
- /**
1095
- * Get list of rooms
1096
- * @private
1097
- * @returns {Promise<Array>} List of rooms
1098
- */
1099
- async getRooms() {
1100
- try {
1101
- logger.info('Getting list of rooms');
1102
-
1103
- const response = await axios.post(API.GET_ROOMS,
1104
- `version=${this.PROTOCOL_VERSION}&gl=y&token=${this.token || ""}`, {
1105
- headers: {
1106
- 'Content-Type': 'application/x-www-form-urlencoded'
1107
- },
1108
- httpsAgent: httpsAgent
1109
- }
1110
- );
1111
-
1112
- if (!response.data || !response.data.rooms) {
1113
- throw new Error('Failed to get rooms');
1114
- }
1115
-
1116
- logger.info(`Retrieved ${response.data.rooms.length} rooms`);
1117
- return response.data.rooms;
1118
- } catch (error) {
1119
- logger.error('Failed to get rooms', error);
1120
- throw new Error(`Failed to get rooms: ${error.message}`);
1121
- }
1122
- }
1123
-
1124
- /**
1125
- * Change game mode
1126
- * @param {string} mode - Game mode ('b' = classic, 'ar' = arrows, 'ard' = death arrows, 'sp' = grapple, 'v' = vtol, 'f' = football)
1127
- * @param {string} engine - Game engine ('b' = bonk, 'f' = football)
1128
- */
1129
- async setGameMode(mode, engine = 'b') {
1130
- this.checkConnection();
1131
-
1132
- // Validar modo
1133
- const validModes = ['b', 'ar', 'ard', 'sp', 'v', 'f'];
1134
- const validEngines = ['b', 'f'];
1135
-
1136
- if (!validModes.includes(mode)) {
1137
- throw new Error(`Invalid mode. Valid modes: ${validModes.join(', ')}`);
1138
- }
1139
-
1140
- if (!validEngines.includes(engine)) {
1141
- throw new Error(`Invalid engine. Valid engines: ${validEngines.join(', ')}`);
1142
- }
1143
-
1144
- await this.sendMessage(CLIENT_MESSAGE_TYPES.SEND_MODE, {
1145
- ga: engine,
1146
- mo: mode
1147
- });
1148
-
1149
- logger.info(`Game mode changed to: ${GAMEMODE_NAMES[mode]} (${ENGINE_NAMES[engine]})`);
1150
- }
1151
-
1152
- /**
1153
- * Set Football mode
1154
- */
1155
- async setFootballMode() {
1156
- await this.setGameMode('f', 'f');
1157
- }
1158
-
1159
- /**
1160
- * Set Classic mode
1161
- */
1162
- async setClassicMode() {
1163
- await this.setGameMode('b', 'b');
1164
- }
1165
-
1166
- /**
1167
- * Set Arrows mode
1168
- */
1169
- async setArrowsMode() {
1170
- await this.setGameMode('ar', 'b');
1171
- }
1172
-
1173
- /**
1174
- * Set Death Arrows mode
1175
- */
1176
- async setDeathArrowsMode() {
1177
- await this.setGameMode('ard', 'b');
1178
- }
1179
-
1180
- /**
1181
- * Set Grapple mode
1182
- */
1183
- async setGrappleMode() {
1184
- await this.setGameMode('sp', 'b');
1185
- }
1186
-
1187
- /**
1188
- * Set VTOL mode
1189
- */
1190
- async setVTOLMode() {
1191
- await this.setGameMode('v', 'b');
1192
- }
1193
-
1194
- /**
1195
- * Get room by name
1196
- * @private
1197
- * @param {string} roomName - Room name
1198
- * @returns {Promise<Object|null>} Room object or null if not found
1199
- */
1200
- async getRoomByName(roomName) {
1201
- try {
1202
- logger.info(`Searching for room: ${roomName}`);
1203
-
1204
- const rooms = await this.getRooms(this.token);
1205
-
1206
- for (const room of rooms) {
1207
- if (room.roomname === roomName) {
1208
- logger.info(`Found room: ${roomName}`);
1209
- return room;
1210
- }
1211
- }
1212
-
1213
- logger.info(`Room not found: ${roomName}`);
1214
- return null;
1215
- } catch (error) {
1216
- logger.error(`Failed to find room: ${roomName}`, error);
1217
- throw new Error(`Failed to find room: ${error.message}`);
1218
- }
1219
- }
1220
-
1221
- /**
1222
- * Get room address
1223
- * @private
1224
- * @param {string} id - Room ID
1225
- * @returns {Promise<Object>} Room address
1226
- */
1227
- async getRoomAddress(id) {
1228
- try {
1229
- logger.info(`Getting address for room ID: ${id}`);
1230
-
1231
- const response = await axios.post(API.GET_ROOM_ADDRESS,
1232
- `id=${id}`, {
1233
- headers: {
1234
- 'Content-Type': 'application/x-www-form-urlencoded'
1235
- },
1236
- httpsAgent: httpsAgent
1237
- }
1238
- );
1239
-
1240
- if (!response.data || !response.data.address) {
1241
- throw new Error('Failed to get room address');
1242
- }
1243
-
1244
- logger.info(`Retrieved address for room ID: ${id}`);
1245
- return response.data;
1246
- } catch (error) {
1247
- logger.error(`Failed to get address for room ID: ${id}`, error);
1248
- throw new Error(`Failed to get room address: ${error.message}`);
1249
- }
1250
- }
1251
-
1252
- /**
1253
- * Set up socket event handlers
1254
- * @private
1255
- */
1256
- setupSocketEvents() {
1257
- // Register listeners for all message types we care about
1258
- Object.values(SERVER_MESSAGE_TYPES).forEach(eventId => {
1259
- this.socket.on(eventId, (...args) => {
1260
- // Create a packet array with event ID as first element and args as the rest
1261
- const packet = [eventId, ...args];
1262
-
1263
- // Process the packet
1264
- const parsedPacket = parsePacket(packet);
1265
-
1266
-
1267
- // Emit events
1268
- this.events.emit('PACKET', parsedPacket);
1269
-
1270
- // logger.debug(`Received event ${eventId}`, parsedPacket);
1271
- });
1272
- });
1273
-
1274
- // Handle standard Socket.IO events
1275
- this.socket.on('connect', () => {
1276
- this.events.emit('connect');
1277
- });
1278
-
1279
- this.socket.on('disconnect', (reason) => {
1280
- this.events.emit('disconnect', reason);
1281
- });
1282
-
1283
- this.socket.on('error', (error) => {
1284
- this.events.emit('error', error);
1285
- });
1286
- }
1287
-
1288
-
1289
- /**
1290
- * Send a message to the server using Socket.IO v2 protocol
1291
- * @private
1292
- * @param {number} eventId - Event ID from CLIENT_MESSAGE_TYPES
1293
- * @param {any} data - Message data
1294
- */
1295
- async sendMessage(eventId, data) {
1296
- this.checkConnection()
1297
-
1298
- // if(eventId != 18 && eventId != 1){ // just for pix to not get annoyed by things
1299
- // }
1300
- logger.debug(`Sending message type ${eventId}`, data);
1301
-
1302
- // For Socket.IO v2, we need to emit to the 'message' event with the event ID and data
1303
- // The server expects a message in the format: [eventId, data]
1304
- await this.socket.emit(eventId, data);
1305
- return true
1306
- }
1307
-
1308
- stopBot() {
1309
- this.connected = false;
1310
-
1311
- if (this.keepAliveTimer) {
1312
- clearInterval(this.keepAliveTimer);
1313
- this.keepAliveTimer = null;
1314
- }
1315
- return true;
1316
- }
1317
-
1318
- getShareLink(){
1319
- return "https://bonk.io/" + this.room.dbid + this.room.bypass;
1320
- }
1321
-
1322
-
1323
- async chat(message) {
1324
- this.checkConnection();
1325
- await this.sendMessage(CLIENT_MESSAGE_TYPES.CHAT_MESSAGE, {
1326
- message
1327
- });
1328
- }
1329
-
1330
- /**
1331
- * Set player ready status
1332
- * @param {boolean} ready - Ready status
1333
- */
1334
- async ready(ready) {
1335
- this.checkConnection();
1336
- await this.sendMessage(CLIENT_MESSAGE_TYPES.SET_READY, {
1337
- ready
1338
- });
1339
- }
1340
-
1341
- /**
1342
- * Join a team
1343
- * @param {number} team - Team to join
1344
- */
1345
- async joinTeam(team) {
1346
- this.checkConnection();
1347
- await this.sendMessage(CLIENT_MESSAGE_TYPES.CHANGE_OWN_TEAM, {
1348
- targetTeam: team
1349
- });
1350
- }
1351
-
1352
- /**
1353
- * Toggle teams lock
1354
- * @param {boolean} locked - Whether teams are locked
1355
- */
1356
- async toggleTeams(locked) {
1357
- this.checkConnection();
1358
- await this.sendMessage(CLIENT_MESSAGE_TYPES.TEAM_LOCK, {
1359
- teamLock: locked
1360
- });
1361
- }
1362
-
1363
- /**
1364
- * Ban a player
1365
- * @param {string|number} playerId - Player ID to ban
1366
- */
1367
- async banPlayer(playerId) {
1368
- this.checkConnection();
1369
- await this.sendMessage(CLIENT_MESSAGE_TYPES.KICK_BAN_PLAYER, {
1370
- banshortid: playerId
1371
- });
1372
- }
1373
-
1374
- /**
1375
- * Leave the game
1376
- */
1377
- async leaveGame() {
1378
- this.checkConnection();
1379
- await this.sendMessage(CLIENT_MESSAGE_TYPES.RETURN_TO_LOBBY);
1380
- }
1381
-
1382
- /**
1383
- * Give host to another player
1384
- * @param {string|number} playerId - Player ID to give host to
1385
- */
1386
- async giveHost(playerId) {
1387
- this.checkConnection();
1388
- await this.sendMessage(CLIENT_MESSAGE_TYPES.SEND_HOST_CHANGE, {
1389
- id: playerId
1390
- });
1391
- }
1392
-
1393
- /**
1394
- * Get the host player
1395
- * @returns {Object} Host player object
1396
- */
1397
- getHost(){
1398
- return this.players.get(this.game.host);
1399
- }
1400
-
1401
- /**
1402
- * Set number of rounds
1403
- * @param {number} rounds - Number of rounds
1404
- */
1405
- async setRounds(rounds) {
1406
- this.checkConnection();
1407
- await this.sendMessage(CLIENT_MESSAGE_TYPES.SEND_ROUNDS, {
1408
- w: rounds
1409
- });
1410
- }
1411
-
1412
- /**
1413
- * Send an input to the game
1414
- * @param {Object} input - Input data
1415
- */
1416
- async sendInput(input) {
1417
- this.checkConnection();
1418
- await this.sendMessage(CLIENT_MESSAGE_TYPES.SEND_INPUTS, {
1419
- i: input.input,
1420
- f: input.frame,
1421
- c: input.sequence
1422
- });
1423
- }
1424
-
1425
- /**
1426
- * Send a timesync message to the server
1427
- * @private
1428
- */
1429
- async sendTimesync() {
1430
- await this.sendMessage(CLIENT_MESSAGE_TYPES.TIMESYNC, {
1431
- jsonrpc: "2.0",
1432
- id: this.timeSync.count++,
1433
- method: "timesync"
1434
- });
1435
- }
1436
-
1437
- /**
1438
- * Check if connected to server
1439
- * @private
1440
- * @throws {Error} If not connected
1441
- */
1442
- checkConnection() {
1443
- if (!this.connected || !this.socket) {
1444
- throw new Error('Not connected to server');
1445
- }
1446
- return true;
1447
- }
1448
-
1449
- /**
1450
- * Converts level to XP total
1451
- * @param {Number} level - The level to convert
1452
- * @returns {Number} The total XP required for this level
1453
- */
1454
- levelToXP(level) {
1455
- if (level < 1) return 0;
1456
- return 100 * Math.pow(level - 1, 2);
1457
- }
1458
-
1459
- /**
1460
- * Converts XP total to level
1461
- * @param {Number} xp - The XP amount to convert
1462
- * @returns {Number} The level for this XP amount
1463
- */
1464
- xpToLevel(xp) {
1465
- if (xp < 0) return 1;
1466
- return Math.floor(Math.sqrt(xp / 100) + 1);
1467
- }
1468
- }
1469
-
1470
- module.exports = BonkBot;