├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── img
├── logo.png
└── ui.png
├── package.json
├── src
├── typings
│ └── Bitburner.d.ts
└── ui-example
│ ├── components
│ ├── Button.tsx
│ ├── Dashboard
│ │ ├── Dashboard.tsx
│ │ ├── MonitorInput.tsx
│ │ └── ToggleSection.tsx
│ └── Switch.tsx
│ ├── hooks
│ └── useOnHover.tsx
│ ├── ui.tsx
│ └── utils
│ ├── getAllServers.ts
│ └── monitor.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | public/
3 |
4 | notes.md
5 |
6 | package-lock.json
7 | # yarn v2
8 | yarn.lock
9 | .yarn/cache
10 | .yarn/unplugged
11 | .yarn/build-state.yml
12 | .yarn/install-state.gz
13 | .pnp.*
14 |
15 |
16 | ### Node template
17 | # Logs
18 | logs
19 | *.log
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 | lerna-debug.log*
24 |
25 | # Dependency directories
26 | node_modules/
27 | jspm_packages/
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bitburner.authToken": "YourAPIKeyHere",
3 | "bitburner.scriptRoot": "./build",
4 | "bitburner.fileWatcher.enable": true,
5 | "bitburner.showFileWatcherEnabledNotification": true,
6 | "bitburner.showPushSuccessNotification": true
7 | }
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Bitburner Custom UI with React Example
6 |
7 |
8 | Did you know you could use React to create custom UI components with access to netscript?
9 |
10 | Turns out it's pretty simple since React and ReactDOM are already present in `Window` object of Bitburner.
11 |
12 |
13 |
14 | ## Getting Started
15 |
16 | ### Prerequisites
17 |
18 | 1. Use an IDE with ability to sync files to Bitburner
19 | - There is a great extension available for VSCode which can be found [here](https://marketplace.visualstudio.com/items?itemName=bitburner.bitburner-vscode-integration)
20 |
21 | 2. Run the game
22 |
23 | 2. **Backup your save**
24 |
25 | ### Installation
26 |
27 | 1. Clone the repo and install packages
28 | ```sh
29 | git clone https://github.com/oddiz/bitburner-react-ui-example.git
30 | cd bitburner-react-ui-example
31 | npm i
32 | ```
33 | 2. Enter your Bitburner API key inside `.vscode/settings.json`
34 | ```json
35 | "bitburner.authToken": "YourAPIKeyHere"
36 | ```
37 | 3. Run build `npm run build`
38 | - If you want automatic build on file change use `npm run dev`
39 |
40 |
41 | If build step was sucessful and vscode bitburner extension is enabled you should now have `ui-example` folder in game.
42 |
43 | ## Usage
44 |
45 | Run `ui-example/ui.js` from Bitburner:
46 |
47 | ```sh
48 | run ui-example/ui.js
49 | ```
50 | Except for the input field titled Monitor, everything is placeholder and has no function in game.
51 |
52 | ## Things to consider
53 |
54 | - Imports must be done with full path with a `/` at start in order for Bitburner to find.
55 | ```js
56 | import { Button } from "/ui-example/components/Button";
57 | ```
58 |
59 | - UI doesn't disappear if you kill `ui.js`. However using it without running the script will very likely cause errors.
60 |
61 | ## Contact
62 | If you still have questions you can message me on discord
63 |
64 | oddiz#9659
65 |
66 |
--------------------------------------------------------------------------------
/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oddiz/bitburner-react-ui-example/b044c98900d525a28aaa09071ae09800310de121/img/logo.png
--------------------------------------------------------------------------------
/img/ui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oddiz/bitburner-react-ui-example/b044c98900d525a28aaa09071ae09800310de121/img/ui.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bitburner-react-ui-example",
3 | "version": "1.0.0",
4 | "description": "An example for making your own UI modules using React in Bitburner.",
5 | "main": "ui/ui.tsx",
6 | "author": "oddiz",
7 | "license": "MIT",
8 | "scripts": {
9 | "build": "rimraf build/* && tsc",
10 | "dev": "rimraf build/* && tsc -w"
11 | },
12 | "devDependencies": {
13 | "@types/react": "^18.0.15",
14 | "@types/react-dom": "^18.0.6",
15 | "rimraf": "^3.0.2"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/ui-example/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import { useOnHover } from "/ui-example/hooks/useOnHover";
2 | const cheatyWindow = eval("window") as Window & typeof globalThis;
3 | const React = cheatyWindow.React;
4 |
5 | export const Button = ({ bg, title, onButtonClick }: { bg: string; title: string; onButtonClick: () => void }) => {
6 | const buttonRef = React.useRef(null);
7 |
8 | const buttonHovered = useOnHover(buttonRef);
9 | return (
10 |
30 | {title}
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/src/ui-example/components/Dashboard/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | import { NS } from "typings/Bitburner";
2 | import { Button } from "/ui-example/components/Button";
3 | import { MonitorInput } from "/ui-example/components/Dashboard/MonitorInput";
4 | import { ToggleSection } from "/ui-example/components/Dashboard/ToggleSection";
5 |
6 | const cheatyWindow = eval("window") as Window & typeof globalThis;
7 | const React = cheatyWindow.React;
8 |
9 | export interface IDashboardProps {
10 | ns: NS;
11 | }
12 | export const Dashboard = ({ ns }: IDashboardProps) => {
13 | const killAllClicked = async () => {
14 | alert("Killing stuff");
15 | };
16 |
17 | const runClicked = async () => {
18 | alert("Running stuff");
19 | };
20 | return (
21 |
31 |
37 |
42 |
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/ui-example/components/Dashboard/MonitorInput.tsx:
--------------------------------------------------------------------------------
1 | import { NS } from "typings/Bitburner";
2 | import { getAllServers } from "/ui-example/utils/getAllServers";
3 |
4 | const cheatyWindow = eval("window") as Window & typeof globalThis;
5 | const cheatyDocument = eval("document") as Document & typeof globalThis;
6 |
7 | const React = cheatyWindow.React;
8 | const { useState, useMemo } = React;
9 |
10 | // This module lets you monitor a server's details (money, security, required threads for grow,weaken,hack etc).
11 | //It has a primitive auto - complete feature. Suggestions for server names will appear as you start typing.When there is 1 suggestion left pressing Enter will run a monitor for that server.
12 | export const MonitorInput = ({ ns }: { ns: NS }) => {
13 | const allServers = useMemo(() => getAllServers(ns), []);
14 | const [suggestions, setSuggestions] = useState([]);
15 |
16 | const onChangeHandler: React.ChangeEventHandler = (e) => {
17 | const query = e.target.value;
18 | const matchedServers: string[] = [];
19 | for (const server of allServers) {
20 | if (queryInString(query, server)) {
21 | matchedServers.push(server);
22 | }
23 | }
24 |
25 | setSuggestions(e.target.value === "" ? [] : matchedServers);
26 | };
27 |
28 | const onKeyDownHandler = async (e) => {
29 | if (e.key === "Enter") {
30 | if (suggestions.length === 1) {
31 | ns.run("/ui-example/utils/monitor.js", 1, suggestions[0]);
32 | setSuggestions([]);
33 | e.target.value = "";
34 | }
35 | }
36 | };
37 | const onFocusHandler = () => {
38 | // disable Bitburner terminal input so that we can write inside our custom widget instead of game's terminal
39 | const terminalInput = cheatyDocument.getElementById("terminal-input") as HTMLInputElement;
40 | if (terminalInput) terminalInput.disabled = true;
41 | };
42 |
43 | const onFocusOut = () => {
44 | // enable Bitburner terminal input again after focusing out of our widget input
45 | const terminalInput = cheatyDocument.getElementById("terminal-input") as HTMLInputElement;
46 | if (terminalInput) terminalInput.disabled = false;
47 | };
48 | const suggestionsSection = suggestions.map((server) => {
49 | return {server}
;
50 | });
51 | return (
52 |
58 |
74 |
86 | {suggestions.length > 0 ? suggestionsSection : null}
87 |
88 |
89 | );
90 | };
91 |
92 | function queryInString(query: string, string: string) {
93 | return string.toLowerCase().includes(query.toLowerCase());
94 | }
95 |
--------------------------------------------------------------------------------
/src/ui-example/components/Dashboard/ToggleSection.tsx:
--------------------------------------------------------------------------------
1 | import { Switch } from "/ui-example/components/Switch";
2 | import { NS } from "typings/Bitburner";
3 |
4 | const cheatyWindow = eval("window") as Window & typeof globalThis;
5 | const React = cheatyWindow.React;
6 | const { useState } = React;
7 |
8 | export const ToggleSection = ({ ns }: { ns: NS }) => {
9 | const [hackActive, setHackActive] = useState(false);
10 | const [workActive, setWorkActive] = useState(true);
11 | const [sleepActive, setSleepActive] = useState(false);
12 | const [repeatActive, setRepeatActive] = useState(true);
13 |
14 | return (
15 |
26 |
Switches
27 | {
30 | setHackActive(!hackActive);
31 | }}
32 | active={hackActive}
33 | />
34 | {
37 | setWorkActive(!workActive);
38 | }}
39 | active={workActive}
40 | />
41 | {
44 | setSleepActive(!sleepActive);
45 | }}
46 | active={sleepActive}
47 | />
48 | {
51 | setRepeatActive(!repeatActive);
52 | }}
53 | active={repeatActive}
54 | />
55 |
56 | );
57 | };
58 |
--------------------------------------------------------------------------------
/src/ui-example/components/Switch.tsx:
--------------------------------------------------------------------------------
1 | import { useOnHover } from "/ui-example/hooks/useOnHover";
2 | const cheatyWindow = eval("window") as Window & typeof globalThis;
3 | const React = cheatyWindow.React;
4 |
5 | export const Switch = ({
6 | title,
7 | onClickHandler,
8 | active,
9 | }: {
10 | title: string;
11 | onClickHandler: React.MouseEventHandler;
12 | active: boolean;
13 | }) => {
14 | const buttonRef = React.useRef(null);
15 |
16 | const buttonHovered = useOnHover(buttonRef);
17 |
18 | return (
19 |
40 | {title}
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/src/ui-example/hooks/useOnHover.tsx:
--------------------------------------------------------------------------------
1 | const cheatyWindow = eval("window") as Window & typeof globalThis;
2 | const React = cheatyWindow.React;
3 | const { useState, useEffect } = React;
4 |
5 | // Since I couldn't manage to work a css-in-js library all styles must be added via styles property. This hook allows us to change styles based on hover without css :hover.
6 | export const useOnHover = (ref: React.RefObject) => {
7 | const [hovered, setHovered] = useState(false);
8 |
9 | const mouseEntered = React.useCallback(() => {
10 | setHovered(true);
11 | }, [ref.current]);
12 |
13 | const mouseLeft = React.useCallback(() => {
14 | setHovered(false);
15 | }, [ref.current]);
16 |
17 | useEffect(() => {
18 | if (!ref.current) return;
19 |
20 | ref.current.addEventListener("mouseenter", mouseEntered);
21 | ref.current.addEventListener("mouseleave", mouseLeft);
22 |
23 | return () => {
24 | if (!ref.current) return;
25 |
26 | ref.current.removeEventListener("mouseenter", mouseEntered);
27 | ref.current.removeEventListener("mouseleave", mouseLeft);
28 | };
29 | }, [ref.current]);
30 |
31 | return hovered;
32 | };
33 |
--------------------------------------------------------------------------------
/src/ui-example/ui.tsx:
--------------------------------------------------------------------------------
1 | // always import as if you are using absolute path in Bitburner, relative paths don't work.
2 | // tsconfig.json file is modified to support this kind of import.
3 | import { Dashboard } from "/ui-example/components/Dashboard/Dashboard";
4 |
5 | // except for type files, which won't be in game
6 | import { NS } from "../typings/Bitburner";
7 |
8 | // accessing global window or document in bitburner costs 25GB each normally. this saves RAM for early UI convenience, sorry devs pls don't fix.
9 | const cheatyWindow = eval("window") as Window & typeof globalThis;
10 | const cheatyDocument = eval("document") as Document & typeof globalThis;
11 |
12 | // bitburner devs included React and ReactDOM in global window object!
13 | const React = cheatyWindow.React;
14 | const ReactDOM = cheatyWindow.ReactDOM;
15 |
16 | export async function main(ns: NS) {
17 | ns.disableLog("asleep");
18 | ReactDOM.render(
19 |
20 |
21 | ,
22 | cheatyDocument.getElementById("overview-extra-hook-0") // there are 3 empty elements provided for players to include their own ui under overview window named (.overview-extra-hook-0, ...-1 ,...-2).
23 | );
24 | while (ns.scriptRunning("/ui-example/ui.js", "home")) {
25 | await ns.asleep(1000); // script must be running in bitburner for ns methods to function inside our component
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/ui-example/utils/getAllServers.ts:
--------------------------------------------------------------------------------
1 | import { NS } from "typings/Bitburner";
2 |
3 | export function main(ns: NS) {
4 | ns.disableLog("ALL");
5 |
6 | const allServers = getAllServers(ns);
7 |
8 | ns.tprint("All servers: " + allServers.join(", "));
9 | }
10 |
11 | export function getAllServers(ns: NS) {
12 | try {
13 |
14 | const serversToCheck = ["home"];
15 | const serversChecked: string[] = [];
16 |
17 | while (serversToCheck.length > 0) {
18 | const serverToCheck = serversToCheck.pop();
19 | if (!serverToCheck) continue;
20 |
21 | if (arrayContains(serversChecked, serverToCheck)) continue;
22 |
23 | //ns.print("Scanning server: ", serverToCheck);
24 | const results = ns.scan(serverToCheck);
25 | serversChecked.push(serverToCheck);
26 |
27 | for (const result of results) {
28 | if (!arrayContains(serversChecked, result)) {
29 | serversToCheck.push(result);
30 | }
31 | }
32 | }
33 |
34 | return serversChecked;
35 | } catch (error) {
36 | console.log("Error in getAllServers: ", error);
37 |
38 | return [];
39 | }
40 | }
41 |
42 | //checks if an item already exists in an array
43 | const arrayContains = (array, item) => {
44 | for (const i of array) {
45 | if (i === item) {
46 | return true;
47 | }
48 | }
49 | return false;
50 | };
51 |
--------------------------------------------------------------------------------
/src/ui-example/utils/monitor.ts:
--------------------------------------------------------------------------------
1 | // slightly modified version of
2 | // https://github.com/bitburner-official/bitburner-scripts/blob/master/monitor.js
3 |
4 | import { NS } from "typings/Bitburner";
5 |
6 | const MONITORJS_REFRESH_INTERVAL = 1000 / 10; // 10 updates / s
7 |
8 | export async function main(ns: NS) {
9 | const flags = ns.flags([["help", false]]);
10 | if (flags._.length === 0 || flags.help) {
11 | ns.tprint("This script helps visualize the money and security of a server.");
12 | ns.tprint(`USAGE: run ${ns.getScriptName()} SERVER_NAME`);
13 | ns.tprint("Example:");
14 | ns.tprint(`> run ${ns.getScriptName()} n00dles`);
15 | return;
16 | }
17 |
18 | ns.tail();
19 | ns.disableLog("ALL");
20 |
21 | while (ns.scriptRunning("/ui-example/utils/monitor.js", "home")) {
22 | const server = flags._[0];
23 | ns.clearLog();
24 |
25 | logServerDetails(ns, server);
26 |
27 | await ns.sleep(MONITORJS_REFRESH_INTERVAL);
28 | }
29 | }
30 | export function autocomplete(data) {
31 | return data.servers;
32 | }
33 |
34 | export function logServerDetails(ns: NS, server: string) {
35 | let money = ns.getServerMoneyAvailable(server);
36 | if (money === 0) money = 1;
37 | const maxMoney = ns.getServerMaxMoney(server);
38 | const minSec = ns.getServerMinSecurityLevel(server);
39 | const sec = ns.getServerSecurityLevel(server);
40 |
41 | const hackTime = ns.getHackTime(server);
42 | const growTime = ns.getGrowTime(server);
43 | const weakenTime = ns.getWeakenTime(server);
44 |
45 | let printString = `${server}:\n`;
46 | printString += `Money: ${ns.nFormat(money, "$0.000a")} / ${ns.nFormat(maxMoney, "$0.000a")} (${(
47 | (money / maxMoney) *
48 | 100
49 | ).toFixed(2)}%)\n`;
50 |
51 | printString += `security: +${(sec - minSec).toFixed(2)}\n\n`;
52 |
53 | const curMoneyHackingThreadsAmount = Math.ceil(ns.hackAnalyzeThreads(server, money));
54 | const hackAnalyzeResult = ns.hackAnalyze(server);
55 | const moneyPerHack = Math.floor(money * hackAnalyzeResult);
56 |
57 | printString += `hack____: ${ns.tFormat(
58 | hackTime
59 | )} (t=${curMoneyHackingThreadsAmount}),\nSec increase: ${ns.hackAnalyzeSecurity(
60 | curMoneyHackingThreadsAmount,
61 | server
62 | )}\n\n`;
63 |
64 | const growthThreadsAmount = Math.ceil(ns.growthAnalyze(server, maxMoney / money));
65 | const maxGrowthSecIncrease = ns.growthAnalyzeSecurity(growthThreadsAmount, server, 1);
66 |
67 | printString += `grow____: ${ns.tFormat(
68 | growTime
69 | )} (t=${growthThreadsAmount}),\nSec increase: ${maxGrowthSecIncrease}\n\n`;
70 |
71 | printString += `weaken__: ${ns.tFormat(weakenTime)} (t=${Math.ceil(
72 | (sec - minSec) * 20
73 | )}) (tAfterGrowWeaken=${Math.ceil((sec + maxGrowthSecIncrease - minSec) * 20)})\n\n`;
74 |
75 | printString += `Analytics:\n$ per thread: ${moneyPerHack} $\n$ per sec(Hack only) per thread: ${(
76 | moneyPerHack /
77 | (ns.getHackTime(server) / 1000)
78 | ).toFixed(2)}$\n$ per sec per thread(full cycle): ${(
79 | moneyPerHack /
80 | (Math.max(weakenTime, hackTime, growTime) / 1000)
81 | ).toFixed(2)}$\n`;
82 |
83 | ns.print(printString);
84 | }
85 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "target": "ES2021",
5 | "module": "ESNext",
6 | "rootDir": "src/",
7 | "baseUrl": "src/",
8 |
9 | "paths": {
10 | "/ui-example/*": ["ui-example/*"],
11 | },
12 |
13 | "outDir": "build/",
14 | "moduleResolution": "node",
15 | "strictNullChecks": true,
16 | "strictPropertyInitialization": true,
17 | "strictBindCallApply": true,
18 | "allowJs": true,
19 | "allowSyntheticDefaultImports": true,
20 | "esModuleInterop": true
21 | }
22 | }
23 |
--------------------------------------------------------------------------------