├── .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 |
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 | --------------------------------------------------------------------------------