Tuesday, October 4
Shadow

List all LAN Minecraft servers dynamically

const dgram = require("node:dgram");

// UDP multicast packet is sent every second and a half from this address/port
const MINECRAFT_BROADCAST_ADDR = "224.0.2.60";
const MINECRAFT_BROADCAST_PORT = "4445";

// Message has a simple format "[MOTD]{server description}[/MOTD][AD]{port number}[/AD]"
const MINECRAFT_SERVER_REGEX = /^\[MOTD\](.*)\[\/MOTD\]\[AD\](.*)\[\/AD\]$/;

const STALE_THRESHOLD = 5000;
const mcServerList = [];

const printServers = () => {
  console.clear();
  if (mcServerList.length > 0) {
    console.table(mcServerList);
  } else {
    console.log("Listening for servers...");
  }
};

const addServer = (serverInfo) => {
  const entry = mcServerList.find((entry) => {
    return entry.address == serverInfo.address && entry.port == serverInfo.port;
  });

  if (entry) {
    // update timestamp to avoid culling as stale
    entry.timestamp = serverInfo.timestamp;
    return;
  } else {
    // add new server, and reprint list
    mcServerList.push(serverInfo);
    printServers();
  }
};

const removeServer = (serverInfo) => {
  const index = mcServerList.findIndex((entry) => {
    return entry.address == serverInfo.address && entry.port == serverInfo.port;
  });

  mcServerList.splice(index, 1);
};

const checkForStale = () => {
  const toRemove = mcServerList.filter((entry) => {
    return Date.now() - entry.timestamp >= STALE_THRESHOLD;
  });

  if (toRemove.length == 0) {
    return;
  }

  toRemove.forEach((entry) => {
    removeServer(entry);
  });

  printServers();
};

const server = dgram.createSocket("udp4");

server.on("error", (err) => {
  console.log(`server error:\n${err.stack}`);
  server.close();
});

server.on("message", (msg, rinfo) => {
  if (MINECRAFT_SERVER_REGEX.test(msg)) {
    const [_, motd, _port] = msg.toString().match(MINECRAFT_SERVER_REGEX);

    addServer({
      address: rinfo.address,
      port: rinfo.port,
      motd: motd,
      timestamp: Date.now(),
    });
  }
});

server.on("listening", () => {
  console.clear();
  console.log("Listening for servers...");
});

server.bind(MINECRAFT_BROADCAST_PORT, () => {
  server.addMembership(MINECRAFT_BROADCAST_ADDR);
});

setInterval(checkForStale, STALE_THRESHOLD);

Leave a Reply