starblast.io player position tracker

can accurately find player postions in the console using reverse-engineering.

// ==UserScript==
// @name          starblast.io player position tracker
// @namespace     http://tampermonkey.net/
// @version       1.0
// @description   can accurately find player postions in the console using reverse-engineering.
// @author        plxyer-x
// @match         *://starblast.io/*
// @grant         none
// @license       hahahahahaha
// @run-at        document-start
// ==/UserScript==

(function() {
  'use strict';

  console.log("%c[Decoder Client] Initialized (v4.0: Protocol Confirmed).", "color: #E91E63; font-weight: bold;");

  // --- FINAL CONFIRMED PROTOCOL CONFIG ---
  const POSITION_TYPES = [200, 205];
  const ID_BYTE_SIZE = 2;
  const STATUS_BLOCK_SIZE = 6;
  const COORD_BYTE_SIZE = 2;
  const ROT_BYTE_SIZE = 2;
  const VELOCITY_BYTE_SIZE = 2;
  const TOTAL_RECORD_SIZE = 19;
  const SCALE_FACTOR = 100;
  const ROT_SCALE_FACTOR = 10000;
  const IS_LITTLE_ENDIAN = true;

  // calculate the final 1-byte skip
  const MIN_KNOWN_FIELDS_SIZE = ID_BYTE_SIZE + STATUS_BLOCK_SIZE + (COORD_BYTE_SIZE * 2) + ROT_BYTE_SIZE + (VELOCITY_BYTE_SIZE * 2);
  const TAIL_SKIP = TOTAL_RECORD_SIZE - MIN_KNOWN_FIELDS_SIZE; // 19 - 18 = 1 byte


  // --- state ---
  const players = new Map(); // id -> { name, x, y, rot, vx, vy, status[] }

  // --- utils ---
  function blobToArrayBuffer(blob) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(reader.result);
      reader.onerror = reject;
      reader.readAsArrayBuffer(blob);
    });
  }

  // --- decoder ---
  function decodeBinaryData(buffer) {
    const view = new DataView(buffer);
    let offset = 0;
    if (buffer.byteLength === 0) return;

    const messageType = view.getUint8(offset);
    offset += 1;

    if (!POSITION_TYPES.includes(messageType)) {
      if (messageType !== 0 && buffer.byteLength > 1) {
        console.log(`[DECODER] Binary Message (Type: ${messageType}, Size: ${buffer.byteLength} bytes) - Unhandled Type`);
      }
      return;
    }

    const expectedPayloadSize = buffer.byteLength - 1;
    const numRecords = Math.floor(expectedPayloadSize / TOTAL_RECORD_SIZE);

    // Check for packet trailer, which is normal
    if (expectedPayloadSize % TOTAL_RECORD_SIZE !== 0) {
         const trailer = expectedPayloadSize % TOTAL_RECORD_SIZE;
         console.log(`%c[DECODER INFO] Type ${messageType}: Decoded ${numRecords} position records. ${trailer} bytes of unknown trailer data remaining.`, "color: #2196F3;");
    }

    // Process all valid 19-byte records
    for (let i = 0; i < numRecords; i++) {
      if (offset + TOTAL_RECORD_SIZE > buffer.byteLength) break;

      // Ship ID (2 bytes)
      const id = view.getUint16(offset, IS_LITTLE_ENDIAN);
      offset += ID_BYTE_SIZE;

      // Status Block (6 bytes)
      const status = [];
      for (let j = 0; j < STATUS_BLOCK_SIZE; j++)
          //ignore the erro here lol
      status.push(view.getUint8(offset + j));
      offset += STATUS_BLOCK_SIZE;

      // Position X (2 bytes, Offset 8)
      const x = view.getInt16(offset, IS_LITTLE_ENDIAN) / SCALE_FACTOR;
      offset += COORD_BYTE_SIZE;

      // Position Y (2 bytes, Offset 10)
      const y = view.getInt16(offset, IS_LITTLE_ENDIAN) / SCALE_FACTOR;
      offset += COORD_BYTE_SIZE;

      // Rotation (2 bytes, Offset 12)
      const rot = view.getInt16(offset, IS_LITTLE_ENDIAN) / ROT_SCALE_FACTOR;
      offset += ROT_BYTE_SIZE;

      // Velocity X (2 bytes, Offset 14)
      const vx = view.getInt16(offset, IS_LITTLE_ENDIAN) / SCALE_FACTOR;
      offset += VELOCITY_BYTE_SIZE;

      // Velocity Y (2 bytes, Offset 16)
      const vy = view.getInt16(offset, IS_LITTLE_ENDIAN) / SCALE_FACTOR;
      offset += VELOCITY_BYTE_SIZE;

      // Skip the remaining 1 byte (Offset 18)
      offset += TAIL_SKIP;

      // Update player state
      const prev = players.get(id) || {};
      players.set(id, {
        id,
        name: prev.name || `(ID ${id})`,
        x: +x.toFixed(2),
        y: +y.toFixed(2),
        rot: +rot.toFixed(4),
        vx: +vx.toFixed(2),
        vy: +vy.toFixed(2),
        status: status
      });
    }
  }

  // --- intercept websocket ---
  const OriginalWebSocket = window.WebSocket;
  window.WebSocket = function(...args) {
    const ws = new OriginalWebSocket(...args);

    ws.addEventListener("message", async (event) => {
      const data = event.data;

      try {
        if (typeof data === "string") {
          const msg = JSON.parse(data);
          if (msg.name === "player_name" && msg.data?.id) {
            const existing = players.get(msg.data.id) || {};
            players.set(msg.data.id, { ...existing, id: msg.data.id, name: msg.data.player_name });
            console.log(`%c[Player Name] ID ${msg.data.id} is now ${msg.data.player_name}`, "color: #FF9800;");
          }
          return;
        }

        if (data instanceof Blob) {
          const buffer = await blobToArrayBuffer(data);
          decodeBinaryData(buffer);
        } else if (data instanceof ArrayBuffer) {
          decodeBinaryData(data);
        }
      } catch (err) {
        // Silently ignore common JSON parse errors on binary data
      }
    });

    return ws;
  };

  window.WebSocket.prototype = OriginalWebSocket.prototype;

  // --- write to the console on an interval (every 1s) ---
  setInterval(() => {
    const snapshot = Array.from(players.values()).filter(p => p.x !== undefined && p.y !== undefined);
    if (snapshot.length) {
      console.clear();
      console.log(`%c[Live Tracker] ${snapshot.length} Active Players (v4.0 - FINAL PROTOCOL)`, "color:#03A9F4; font-weight:bold;");
      console.table(snapshot, ['id', 'name', 'x', 'y', 'rot', 'vx', 'vy']);
    }
  }, 1000);
})();