Joseph Carothers
Bitburner Custom UI

Bitburner Custom UI

Thu Sep 26 2024

Visualizing Bitburner: A Custom GUI for Real-Time Game Data

Introduction

Bitburner is a complex game that combines hacking, scripting, and automation. To enhance my gameplay, I created a custom user interface (UI) that visualizes real-time game data. This UI helps track hacking progress, resources, and target nodes, making the gameplay smoother and more strategic.

In addition to this, I experimented with building a custom React component renderer and a diffing algorithm tailored for Bitburner. The unique challenge was that Bitburner doesn’t easily allow long-living scripts, so using traditional React libraries like react-dom wasn't feasible.

Project Overview

The UI functions as an interactive game HUD, tapping into Bitburner's API to display live stats, targets, and node performance in a responsive layout. The core features include:

Additionally, I built a custom React rendering engine from scratch to work within Bitburner's limitations. This allows for dynamic UI updates using familiar React-style components, without relying on traditional libraries like react-dom. I also implemented a diffing and patching algorithm to efficiently update the DOM without restarting the script, which is crucial in Bitburner's environment where scripts are usually short-lived.

Key Components

1. User Stats

The User component displays the player's hacking stats, including their hacking level, available funds, and money-per-second rate.

const User = ({ ns }) => {
  const hackLvl = ns.getHackingLevel();
  const money = ns.getServerMoneyAvailable('home');

  return React.createElement(
    CollapsibleSection,
    { id: 'user-section' },
    Row({ children: ["Hck Lvl:", hackLvl] }),
    Row({ children: ["Funds:", formatNumber(money)] }),
    Row({ children: ["Run $/s:", formatNumber(calculateMoneyPerSecond())] })
  );
};

2. Target Tracker

The Target component monitors attack targets, displaying server money available and security level with visual progress bars.

const Target = ({ ns, target }) => {
  const money = ns.getServerMoneyAvailable(target.name);
  const security = ns.getServerSecurityLevel(target.name);

  return React.createElement(
    'div',
    { style: {} },
    Row({ children: [target.name, "Money:", formatNumber(money)] }),
    Row({ children: ["Security:", formatNumber(security)] })
  );
};

3. Node Performance

The Node component tracks hacking operations on each active node, using ProgressBar components to visualize hack/grow/weaken progress.

const Node = ({ ns, node, stats }) => {
  const maxRam = ns.getServerMaxRam(node);

  return React.createElement(
    'div',
    { style: {} },
    Row({ children: [node, "Max RAM:", formatNumber(maxRam)] }),
    ProgressBar({ percent: stats.hack / maxRam, label: "Hack" }),
    ProgressBar({ percent: stats.grow / maxRam, label: "Grow" }),
    ProgressBar({ percent: stats.weaken / maxRam, label: "Weaken" })
  );
};

Experimenting with a Custom React Renderer

One of the main challenges I faced was working around Bitburner’s limitations, where long-living scripts are discouraged. This meant I couldn’t rely on React’s react-dom for efficient DOM updates. Instead, I decided to build a custom renderer that mimics React's component system while integrating directly with the game’s DOM.

The custom renderer takes in React components, processes them recursively, and creates HTML elements that are manually appended to the DOM. Here's an example of how the rendering works:

export function renderToHTMLElement(element) {
  if (typeof element === 'string' || typeof element === 'number') {
    return document.createTextNode(element);
  }

  if (typeof element.type === 'function') {
    const renderedElement = element.type(element.props);
    return renderToHTMLElement(renderedElement);
  }

  const domNode = document.createElement(element.type);
  Object.entries(element.props || {}).forEach(([key, value]) => {
    if (key === 'children') return;
    if (/^on[A-Z]/.test(key)) {
      domNode.addEventListener(key.toLowerCase().substring(2), value);
    } else if (key === 'style') {
      Object.assign(domNode.style, value);
    } else {
      domNode.setAttribute(key, value);
    }
  });

  const children = Array.isArray(element.props?.children)
    ? element.props.children
    : [element.props?.children || ''];

  children.forEach(child => domNode.appendChild(renderToHTMLElement(child)));

  return domNode;
}

Diffing Algorithm for Efficient Updates

To optimize performance, I implemented a diffing algorithm that compares the old and new virtual DOM trees and selectively applies changes to the real DOM. This keeps the UI responsive while minimizing unnecessary updates.

function diffAndUpdate(oldNode, newNode) {
  if (!newNode) {
    oldNode.remove();
    return;
  }

  if (oldNode.nodeType !== newNode.nodeType || oldNode.nodeName !== newNode.nodeName) {
    oldNode.replaceWith(newNode);
    return;
  }

  if (oldNode.nodeType === Node.TEXT_NODE && oldNode.textContent !== newNode.textContent) {
    oldNode.textContent = newNode.textContent;
    return;
  }

  const oldAttrs = Array.from(oldNode.attributes);
  const newAttrs = Array.from(newNode.attributes);

  oldAttrs.forEach(attr => {
    if (!newNode hasAttribute(attr.name)) {
      oldNode.removeAttribute(attr.name);
    }
  });

  newAttrs.forEach(attr => {
    if (oldNode.getAttribute(attr.name) !== attr.value) {
      oldNode.setAttribute(attr.name, attr.value);
    }
  });

  const oldChildren = Array.from(oldNode.childNodes);
  const newChildren = Array.from(newNode.childNodes);
  const maxLength = Math.max(oldChildren.length, newChildren.length);

  for (let i = 0;i < maxLength;i++) {
    diffAndUpdate(oldChildren[i], newChildren[i]);
  }
}

UI Design and Style

The UI features a minimalist, dark theme to match Bitburner’s aesthetic, with vibrant progress indicators. Styling is centralized in a theme.js file:

const theme = {
  color: {
    blue: '#4a90e2',
    yellow: '#f4d03f',
    green: '#27ae60',
    red: '#e74c3c',
    white: '#ecf0f1',
  },
  size: {
    font: {
      md: '14px',
    },
  },
  spacing: {
    sm: '8px',
    md: '16px',
    lg: '24px',
  },
};

Final Thoughts

This custom UI, along with the custom React renderer and diffing system, has significantly improved my gameplay by providing real-time data tracking and resource management. Building a custom rendering engine was an exciting challenge that opened up possibilities for more tailored UIs in Bitburner.

I'm continuing to refine the interface with more features like notifications and enhanced visuals. This project showcases how a well-designed UI and custom rendering logic can elevate both gameplay and strategy in Bitburner.