├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── index.html ├── manifest.json └── robots.txt ├── src ├── App.tsx ├── bitburner.types.ts ├── components │ ├── editor │ │ ├── index.tsx │ │ └── section │ │ │ ├── factions-section.tsx │ │ │ ├── index.tsx │ │ │ ├── player-section.tsx │ │ │ └── properties │ │ │ ├── editable.tsx │ │ │ └── stat.tsx │ ├── file-loader.tsx │ └── inputs │ │ ├── checkbox.tsx │ │ └── input.tsx ├── icons │ ├── check.svg │ ├── close.svg │ ├── download.svg │ ├── filter.svg │ ├── search.svg │ └── upload.svg ├── index.css ├── index.tsx ├── react-app-env.d.ts ├── reportWebVitals.ts ├── setupTests.ts ├── store │ └── file.store.ts └── util │ ├── format.ts │ ├── game.ts │ └── hooks.tsx ├── tailwind.config.js └── tsconfig.json /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | build-and-deploy: 8 | concurrency: ci-${{ github.ref }} # Recommended if you intend to make multiple deployments in quick succession. 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 🛎️ 12 | uses: actions/checkout@v3 13 | 14 | - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. 15 | run: | 16 | npm ci 17 | npm run test 18 | npm run build 19 | 20 | - name: Deploy 🚀 21 | uses: JamesIves/github-pages-deploy-action@v4.3.3 22 | with: 23 | branch: gh-pages # The branch the action should deploy to. 24 | folder: build # The folder the action should deploy. 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Butburner Save Editor 2 | 3 | ## Usage 4 | 5 | https://redmega.github.io/bitburner-save-editor/ 6 | 7 | Open the save editor and upload your exported Bitburner save file. When you're done editing, you can click the download icon in the header to get a new version of your save that you can import to the game. 8 | 9 | ## TODO 10 | 11 | - [ ] Aliases 12 | - [ ] Global Aliases 13 | - [ ] Gangs 14 | - [ ] Servers 15 | - [ ] Companies 16 | - [x] Factions 17 | - [ ] Player 18 | - [x] Money 19 | - [ ] Exploits 20 | - [x] Edit Save File 21 | - [ ] ...Everything Else 22 | - [ ] Settings 23 | - [ ] Staneks 24 | - [ ] Stock Market 25 | 26 | ## Contributing 27 | 28 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "save-editor", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://redmega.github.io/bitburner-save-editor", 6 | "dependencies": { 7 | "@heroicons/react": "^1.0.5", 8 | "@testing-library/jest-dom": "^5.16.1", 9 | "@testing-library/react": "^12.1.2", 10 | "@testing-library/user-event": "^13.5.0", 11 | "buffer": "^6.0.3", 12 | "clsx": "^1.1.1", 13 | "mobx": "^6.3.9", 14 | "mobx-react-lite": "^3.2.2", 15 | "ramda": "^0.27.1", 16 | "react": "^17.0.2", 17 | "react-dom": "^17.0.2", 18 | "react-scripts": "5.0.0", 19 | "typescript": "^4.5.4", 20 | "web-vitals": "^2.1.2" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test --passWithNoTests", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "@types/jest": "^27.0.3", 48 | "@types/node": "^16.11.14", 49 | "@types/ramda": "^0.27.62", 50 | "@types/react": "^17.0.37", 51 | "@types/react-dom": "^17.0.11", 52 | "autoprefixer": "^10.0.2", 53 | "postcss": "^8.4.5", 54 | "prettier": "^2.5.1", 55 | "tailwindcss": "^3.0.5" 56 | }, 57 | "resolutions": { 58 | "nth-check": "^2.0.1", 59 | "node-forge": "^1.0.0", 60 | "postcss": "^8.4.5" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Redmega/bitburner-save-editor/2abea22e28d81bec52b5131e1d151a9721065711/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 15 | 16 | 20 | 21 | 30 | Bitburner Save Editor 31 | 32 | 33 | 34 |
35 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Bitburner Save Editor", 3 | "name": "Bitburner Save Editor", 4 | "icons": [ 5 | { 6 | "src": "https://danielyxie.github.io/bitburner/favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | import { observer } from "mobx-react-lite"; 3 | 4 | import FileLoader from "components/file-loader"; 5 | import Editor from "components/editor"; 6 | import fileStore from "store/file.store"; 7 | import type { FileStore } from "store/file.store"; 8 | 9 | import { ReactComponent as DownloadIcon } from "icons/download.svg"; 10 | 11 | export const FileContext = createContext(fileStore); 12 | 13 | function App() { 14 | const fileStore = useContext(FileContext); 15 | 16 | return ( 17 | 18 |
19 |
20 |

21 | Bitburner Save Editor 22 | {fileStore.ready && ( 23 | 26 | )} 27 |

28 | 29 |
30 | 31 |
32 |
33 | ); 34 | } 35 | 36 | export default observer(App); 37 | -------------------------------------------------------------------------------- /src/bitburner.types.ts: -------------------------------------------------------------------------------- 1 | export namespace Bitburner { 2 | export enum SaveDataKey { 3 | PlayerSave = "PlayerSave", 4 | FactionsSave = "FactionsSave", 5 | AllServersSave = "AllServersSave", 6 | CompaniesSave = "CompaniesSave", 7 | AliasesSave = "AliasesSave", 8 | GlobalAliasesSave = "GlobalAliasesSave", 9 | MessagesSave = "MessagesSave", 10 | StockMarketSave = "StockMarketSave", 11 | SettingsSave = "SettingsSave", 12 | VersionSave = "VersionSave", 13 | AllGangsSave = "AllGangsSave", 14 | LastExportBonus = "LastExportBonus", 15 | StaneksGiftSave = "StaneksGiftSave", 16 | } 17 | 18 | export const enum Ctor { 19 | BitburnerSaveObject = "BitburnerSaveObject", 20 | CodingContract = "CodingContract", 21 | Company = "Company", 22 | Faction = "Faction", 23 | HacknetNode = "HacknetNode", 24 | HashManager = "HashManager", 25 | Message = "Message", 26 | MoneySourceTracker = "MoneySourceTracker", 27 | PlayerObject = "PlayerObject", 28 | Script = "Script", 29 | Server = "Server", 30 | StaneksGift = "StaneksGift", 31 | } 32 | 33 | export interface RawSaveData extends SaveObject { 34 | ctor: Ctor.BitburnerSaveObject; 35 | data: Record; 36 | } 37 | 38 | export interface SaveObject { 39 | ctor: T; 40 | data: {}; 41 | } 42 | export interface SaveData extends SaveObject { 43 | ctor: Ctor.BitburnerSaveObject; 44 | data: { 45 | [SaveDataKey.AliasesSave]: Record; 46 | [SaveDataKey.AllGangsSave]: unknown; // @TODO: Gangs 47 | [SaveDataKey.AllServersSave]: Record; 48 | [SaveDataKey.CompaniesSave]: Record; 49 | [SaveDataKey.FactionsSave]: Record; 50 | [SaveDataKey.GlobalAliasesSave]: Record; 51 | [SaveDataKey.LastExportBonus]: number; // Date Number 52 | [SaveDataKey.MessagesSave]: Record; 53 | [SaveDataKey.PlayerSave]: PlayerSaveObject; 54 | [SaveDataKey.SettingsSave]: SettingsSaveData; 55 | [SaveDataKey.StaneksGiftSave]: StaneksGiftSaveObject; 56 | [SaveDataKey.StockMarketSave]: StockMarketSaveData; 57 | [SaveDataKey.VersionSave]: number; 58 | }; 59 | } 60 | 61 | interface ServerSaveObject extends SaveObject { 62 | data: { 63 | backdoorInstalled: boolean; 64 | baseDifficulty: number; 65 | contracts: CodingContractSaveObject[]; 66 | cpuCores: number; 67 | ftpPortOpen: boolean; 68 | hackDifficulty: number; 69 | hasAdminRights: boolean; 70 | hostname: string; 71 | httpPortOpen: boolean; 72 | ip: string; 73 | isConnectedTo: boolean; 74 | maxRam: number; 75 | messages: string[]; 76 | minDifficulty: number; 77 | moneyAvailable: number; 78 | moneyMax: number; 79 | numOpenPortsRequired: number; 80 | openPortCount: number; 81 | organizationName: string; 82 | programs: string[]; 83 | purchasedByPlayer: boolean; 84 | ramUsed: number; 85 | requiredHackingSkill: number; 86 | runningScripts: unknown[]; // @TODO: RunningScript 87 | scripts: ScriptSaveObject[]; 88 | serverGrowth: number; 89 | serversOnNetwork: string[]; 90 | smtpPortOpen: boolean; 91 | sqlPortOpen: boolean; 92 | sshPortOpen: boolean; 93 | textFiles: string[]; 94 | }; 95 | } 96 | 97 | interface CodingContractSaveObject extends SaveObject { 98 | data: { 99 | data: any; // Data can be anything, arrays of numbers, 2d arrays, etc. 100 | fn: string; 101 | reward: { name: string; type: number } | null; 102 | tries: number; 103 | type: string; 104 | }; 105 | } 106 | 107 | interface CompanySaveObject extends SaveObject { 108 | data: { 109 | name: string; 110 | info: string; 111 | companyPositions: Record; 112 | expMultiplier: number; 113 | isMegacorp?: boolean; 114 | jobStatReqOffset: number; 115 | salaryMultiplier: number; 116 | }; 117 | } 118 | 119 | export interface FactionsSaveObject extends SaveObject { 120 | data: { 121 | alreadyInvited: boolean; 122 | augmentations: string[]; 123 | favor: number; 124 | isBanned: boolean; 125 | isMember: boolean; 126 | name: string; 127 | playerReputation: number; 128 | }; 129 | } 130 | 131 | interface HacknetNodeSaveObject extends SaveObject { 132 | data: { 133 | cores: number; 134 | level: number; 135 | moneyGainRatePerSecond: number; 136 | name: string; 137 | onlineTimeSeconds: number; 138 | ram: number; 139 | totalMoneyGenerated: number; 140 | }; 141 | } 142 | 143 | interface HashManagerSaveObject extends SaveObject { 144 | data: { 145 | capacity: number; 146 | hashes: number; 147 | upgrades: Record; 148 | }; 149 | } 150 | 151 | interface MessageSaveObject extends SaveObject { 152 | data: { 153 | filename: string; 154 | msg: string; 155 | recvd: boolean; 156 | }; 157 | } 158 | 159 | interface MoneySourceSaveObject extends SaveObject { 160 | data: { 161 | [key: string]: number; 162 | bladeburner: number; 163 | casino: number; 164 | class: number; 165 | codingcontract: number; 166 | corporation: number; 167 | crime: number; 168 | gang: number; 169 | hacking: number; 170 | hacknet: number; 171 | hacknet_expenses: number; 172 | hospitalization: number; 173 | infiltration: number; 174 | sleeves: number; 175 | stock: number; 176 | total: number; 177 | work: number; 178 | servers: number; 179 | other: number; 180 | augmentations: number; 181 | }; 182 | } 183 | 184 | interface ScriptSaveObject extends SaveObject { 185 | data: { 186 | code: string; 187 | dependencies: { filename: string; url: string }[]; 188 | filename: string; 189 | module: object | string; 190 | moduleSequenceNumber: number; 191 | ramUsage: number; 192 | server: string; 193 | url: string; 194 | }; 195 | } 196 | 197 | interface StaneksGiftSaveObject extends SaveObject { 198 | data: { 199 | fragments: { 200 | id: number; 201 | avgCharge: number; 202 | numCharge: number; 203 | rotation: number; 204 | x: number; 205 | y: number; 206 | }[]; 207 | storedCycles: number; 208 | }; 209 | } 210 | 211 | export interface PlayerSaveObject extends SaveObject { 212 | data: { 213 | augmentations: { level: number; name: string }[]; 214 | bitNodeN: number; 215 | city: CityName; 216 | companyName: string; 217 | corporation: unknown | null; // @TODO ICorporation 218 | gang: unknown | null; // @TODO IGang 219 | bladeburner: unknown | null; // @TODO IBladeburner 220 | currentServer: string; 221 | factions: string[]; 222 | factionInvitations: string[]; 223 | hacknetNodes: (HacknetNodeSaveObject | string)[]; 224 | has4SData: boolean; 225 | has4SDataTixApi: boolean; 226 | hashManager: HashManagerSaveObject; 227 | hasTixApiAccess: boolean; 228 | hasWseAccount: boolean; 229 | hp: number; 230 | jobs: Record; 231 | isWorking: boolean; 232 | karma: number; 233 | numPeopleKilled: number; 234 | location: LocationName; 235 | max_hp: number; 236 | money: number; 237 | moneySourceA: MoneySourceSaveObject; 238 | moneySourceB: MoneySourceSaveObject; 239 | playtimeSinceLastAug: number; 240 | playtimeSinceLastBitnode: number; 241 | purchasedServers: string[]; 242 | queuedAugmentations: { level: number; name: string }[]; 243 | resleeves: unknown[]; // @TODO Resleeve[]; 244 | scriptProdSinceLastAug: number; 245 | sleeves: unknown[]; // @TODO Sleeve[]; 246 | sleevesFromCovenant: number; 247 | sourceFiles: { 248 | lvl: number; 249 | n: number; 250 | }[]; 251 | exploits: Exploit[]; 252 | lastUpdate: number; 253 | totalPlaytime: number; 254 | 255 | // Stats 256 | hacking: number; 257 | strength: number; 258 | defense: number; 259 | dexterity: number; 260 | agility: number; 261 | charisma: number; 262 | intelligence: number; 263 | 264 | // Experience 265 | hacking_exp: number; 266 | strength_exp: number; 267 | defense_exp: number; 268 | dexterity_exp: number; 269 | agility_exp: number; 270 | charisma_exp: number; 271 | intelligence_exp: number; 272 | 273 | // Multipliers 274 | entropy: number; 275 | hacking_chance_mult: number; 276 | hacking_speed_mult: number; 277 | hacking_money_mult: number; 278 | hacking_grow_mult: number; 279 | hacking_mult: number; 280 | hacking_exp_mult: number; 281 | strength_mult: number; 282 | strength_exp_mult: number; 283 | defense_mult: number; 284 | defense_exp_mult: number; 285 | dexterity_mult: number; 286 | dexterity_exp_mult: number; 287 | agility_mult: number; 288 | agility_exp_mult: number; 289 | charisma_mult: number; 290 | charisma_exp_mult: number; 291 | hacknet_node_money_mult: number; 292 | hacknet_node_purchase_cost_mult: number; 293 | hacknet_node_ram_cost_mult: number; 294 | hacknet_node_core_cost_mult: number; 295 | hacknet_node_level_cost_mult: number; 296 | company_rep_mult: number; 297 | faction_rep_mult: number; 298 | work_money_mult: number; 299 | crime_success_mult: number; 300 | crime_money_mult: number; 301 | bladeburner_max_stamina_mult: number; 302 | bladeburner_stamina_gain_mult: number; 303 | bladeburner_analysis_mult: number; 304 | bladeburner_success_chance_mult: number; 305 | 306 | createProgramReqLvl: number; 307 | factionWorkType: string; 308 | createProgramName: string; 309 | timeWorkedCreateProgram: number; 310 | crimeType: string; 311 | committingCrimeThruSingFn: boolean; 312 | singFnCrimeWorkerScript: unknown | null; // @TODO: WorkerScript 313 | timeNeededToCompleteWork: number; 314 | focus: boolean; 315 | className: string; 316 | currentWorkFactionName: string; 317 | workType: string; 318 | currentWorkFactionDescription: string; 319 | timeWorked: number; 320 | workMoneyGained: number; 321 | workMoneyGainRate: number; 322 | workRepGained: number; 323 | workRepGainRate: number; 324 | workHackExpGained: number; 325 | workHackExpGainRate: number; 326 | workStrExpGained: number; 327 | workStrExpGainRate: number; 328 | workDefExpGained: number; 329 | workDefExpGainRate: number; 330 | workDexExpGained: number; 331 | workDexExpGainRate: number; 332 | workAgiExpGained: number; 333 | workAgiExpGainRate: number; 334 | workChaExpGained: number; 335 | workChaExpGainRate: number; 336 | workMoneyLossRate: number; 337 | }; 338 | } 339 | 340 | export type PlayerStat = "hacking" | "strength" | "defense" | "dexterity" | "agility" | "charisma" | "intelligence"; 341 | 342 | export const PLAYER_STATS: PlayerStat[] = [ 343 | "hacking", 344 | "strength", 345 | "defense", 346 | "dexterity", 347 | "agility", 348 | "charisma", 349 | "intelligence", 350 | ]; 351 | 352 | // Not sure why these don't follow the SaveObject model 353 | interface SettingsSaveData { 354 | // Basically ripped from the bitburner git repo 355 | /** 356 | * How many servers per page 357 | */ 358 | ActiveScriptsServerPageSize: number; 359 | /** 360 | * How many scripts per page 361 | */ 362 | ActiveScriptsScriptPageSize: number; 363 | /** 364 | * How often the game should autosave the player's progress, in seconds. 365 | */ 366 | AutosaveInterval: number; 367 | 368 | /** 369 | * How many milliseconds between execution points for Netscript 1 statements. 370 | */ 371 | CodeInstructionRunTime: number; 372 | 373 | /** 374 | * Render city as list of buttons. 375 | */ 376 | DisableASCIIArt: boolean; 377 | 378 | /** 379 | * Whether global keyboard shortcuts should be recognized throughout the game. 380 | */ 381 | DisableHotkeys: boolean; 382 | 383 | /** 384 | * Whether text effects such as corruption should be visible. 385 | */ 386 | DisableTextEffects: boolean; 387 | 388 | /** 389 | * Enable bash hotkeys 390 | */ 391 | EnableBashHotkeys: boolean; 392 | 393 | /** 394 | * Enable timestamps 395 | */ 396 | EnableTimestamps: boolean; 397 | 398 | /** 399 | * Locale used for display numbers 400 | */ 401 | Locale: string; 402 | 403 | /** 404 | * Limit the number of log entries for each script being executed on each server. 405 | */ 406 | MaxLogCapacity: number; 407 | 408 | /** 409 | * Limit how many entries can be written to a Netscript Port before entries start to get pushed out. 410 | */ 411 | MaxPortCapacity: number; 412 | 413 | /** 414 | * Limit the number of entries in the terminal. 415 | */ 416 | MaxTerminalCapacity: number; 417 | 418 | /** 419 | * Save the game when you save any file. 420 | */ 421 | SaveGameOnFileSave: boolean; 422 | 423 | /** 424 | * Whether the player should be asked to confirm purchasing each and every augmentation. 425 | */ 426 | SuppressBuyAugmentationConfirmation: boolean; 427 | 428 | /** 429 | * Whether the user should be prompted to join each faction via a dialog box. 430 | */ 431 | SuppressFactionInvites: boolean; 432 | 433 | /** 434 | * Whether to show a popup message when player is hospitalized from taking too much damage 435 | */ 436 | SuppressHospitalizationPopup: boolean; 437 | 438 | /** 439 | * Whether the user should be shown a dialog box whenever they receive a new message file. 440 | */ 441 | SuppressMessages: boolean; 442 | 443 | /** 444 | * Whether the user should be asked to confirm travelling between cities. 445 | */ 446 | SuppressTravelConfirmation: boolean; 447 | 448 | /** 449 | * Whether the user should be displayed a popup message when his Bladeburner actions are cancelled. 450 | */ 451 | SuppressBladeburnerPopup: boolean; 452 | 453 | /* 454 | * Theme colors 455 | */ 456 | theme: { 457 | [key: string]: string | undefined; 458 | primarylight: string; 459 | primary: string; 460 | primarydark: string; 461 | errorlight: string; 462 | error: string; 463 | errordark: string; 464 | secondarylight: string; 465 | secondary: string; 466 | secondarydark: string; 467 | warninglight: string; 468 | warning: string; 469 | warningdark: string; 470 | infolight: string; 471 | info: string; 472 | infodark: string; 473 | welllight: string; 474 | well: string; 475 | white: string; 476 | black: string; 477 | hp: string; 478 | money: string; 479 | hack: string; 480 | combat: string; 481 | cha: string; 482 | int: string; 483 | rep: string; 484 | disabled: string; 485 | }; 486 | } 487 | 488 | enum LocationName { 489 | // Cities 490 | Aevum = "Aevum", 491 | Chongqing = "Chongqing", 492 | Ishima = "Ishima", 493 | NewTokyo = "New Tokyo", 494 | Sector12 = "Sector-12", 495 | Volhaven = "Volhaven", 496 | 497 | // Aevum Locations 498 | AevumAeroCorp = "AeroCorp", 499 | AevumBachmanAndAssociates = "Bachman & Associates", 500 | AevumClarkeIncorporated = "Clarke Incorporated", 501 | AevumCrushFitnessGym = "Crush Fitness Gym", 502 | AevumECorp = "ECorp", 503 | AevumFulcrumTechnologies = "Fulcrum Technologies", 504 | AevumGalacticCybersystems = "Galactic Cybersystems", 505 | AevumNetLinkTechnologies = "NetLink Technologies", 506 | AevumPolice = "Aevum Police Headquarters", 507 | AevumRhoConstruction = "Rho Construction", 508 | AevumSnapFitnessGym = "Snap Fitness Gym", 509 | AevumSummitUniversity = "Summit University", 510 | AevumWatchdogSecurity = "Watchdog Security", 511 | AevumCasino = "Iker Molina Casino", 512 | 513 | // Chongqing locations 514 | ChongqingKuaiGongInternational = "KuaiGong International", 515 | ChongqingSolarisSpaceSystems = "Solaris Space Systems", 516 | ChongqingChurchOfTheMachineGod = "Church of the Machine God", 517 | 518 | // Sector 12 519 | Sector12AlphaEnterprises = "Alpha Enterprises", 520 | Sector12BladeIndustries = "Blade Industries", 521 | Sector12CIA = "Central Intelligence Agency", 522 | Sector12CarmichaelSecurity = "Carmichael Security", 523 | Sector12CityHall = "Sector-12 City Hall", 524 | Sector12DeltaOne = "DeltaOne", 525 | Sector12FoodNStuff = "FoodNStuff", 526 | Sector12FourSigma = "Four Sigma", 527 | Sector12IcarusMicrosystems = "Icarus Microsystems", 528 | Sector12IronGym = "Iron Gym", 529 | Sector12JoesGuns = "Joe's Guns", 530 | Sector12MegaCorp = "MegaCorp", 531 | Sector12NSA = "National Security Agency", 532 | Sector12PowerhouseGym = "Powerhouse Gym", 533 | Sector12RothmanUniversity = "Rothman University", 534 | Sector12UniversalEnergy = "Universal Energy", 535 | 536 | // New Tokyo 537 | NewTokyoDefComm = "DefComm", 538 | NewTokyoGlobalPharmaceuticals = "Global Pharmaceuticals", 539 | NewTokyoNoodleBar = "Noodle Bar", 540 | NewTokyoVitaLife = "VitaLife", 541 | 542 | // Ishima 543 | IshimaNovaMedical = "Nova Medical", 544 | IshimaOmegaSoftware = "Omega Software", 545 | IshimaStormTechnologies = "Storm Technologies", 546 | IshimaGlitch = "0x6C1", 547 | 548 | // Volhaven 549 | VolhavenCompuTek = "CompuTek", 550 | VolhavenHeliosLabs = "Helios Labs", 551 | VolhavenLexoCorp = "LexoCorp", 552 | VolhavenMilleniumFitnessGym = "Millenium Fitness Gym", 553 | VolhavenNWO = "NWO", 554 | VolhavenOmniTekIncorporated = "OmniTek Incorporated", 555 | VolhavenOmniaCybersystems = "Omnia Cybersystems", 556 | VolhavenSysCoreSecurities = "SysCore Securities", 557 | VolhavenZBInstituteOfTechnology = "ZB Institute of Technology", 558 | 559 | // Generic locations 560 | Hospital = "Hospital", 561 | Slums = "The Slums", 562 | TravelAgency = "Travel Agency", 563 | WorldStockExchange = "World Stock Exchange", 564 | 565 | // Default name for Location objects 566 | Void = "The Void", 567 | } 568 | 569 | // Stocks for companies at which you can work 570 | const StockSymbols: Record = { 571 | [LocationName.AevumECorp]: "ECP", 572 | [LocationName.Sector12MegaCorp]: "MGCP", 573 | [LocationName.Sector12BladeIndustries]: "BLD", 574 | [LocationName.AevumClarkeIncorporated]: "CLRK", 575 | [LocationName.VolhavenOmniTekIncorporated]: "OMTK", 576 | [LocationName.Sector12FourSigma]: "FSIG", 577 | [LocationName.ChongqingKuaiGongInternational]: "KGI", 578 | [LocationName.AevumFulcrumTechnologies]: "FLCM", 579 | [LocationName.IshimaStormTechnologies]: "STM", 580 | [LocationName.NewTokyoDefComm]: "DCOMM", 581 | [LocationName.VolhavenHeliosLabs]: "HLS", 582 | [LocationName.NewTokyoVitaLife]: "VITA", 583 | [LocationName.Sector12IcarusMicrosystems]: "ICRS", 584 | [LocationName.Sector12UniversalEnergy]: "UNV", 585 | [LocationName.AevumAeroCorp]: "AERO", 586 | [LocationName.VolhavenOmniaCybersystems]: "OMN", 587 | [LocationName.ChongqingSolarisSpaceSystems]: "SLRS", 588 | [LocationName.NewTokyoGlobalPharmaceuticals]: "GPH", 589 | [LocationName.IshimaNovaMedical]: "NVMD", 590 | [LocationName.AevumWatchdogSecurity]: "WDS", 591 | [LocationName.VolhavenLexoCorp]: "LXO", 592 | [LocationName.AevumRhoConstruction]: "RHOC", 593 | [LocationName.Sector12AlphaEnterprises]: "APHE", 594 | [LocationName.VolhavenSysCoreSecurities]: "SYSC", 595 | [LocationName.VolhavenCompuTek]: "CTK", 596 | [LocationName.AevumNetLinkTechnologies]: "NTLK", 597 | [LocationName.IshimaOmegaSoftware]: "OMGA", 598 | [LocationName.Sector12FoodNStuff]: "FNS", 599 | 600 | // Stocks for other companies 601 | "Sigma Cosmetics": "SGC", 602 | "Joes Guns": "JGN", 603 | "Catalyst Ventures": "CTYS", 604 | "Microdyne Technologies": "MDYN", 605 | "Titan Laboratories": "TITN", 606 | }; 607 | 608 | interface StockMarketSaveData { 609 | Orders: Record< 610 | string, 611 | { 612 | pos: PositionTypes; 613 | price: number; 614 | shares: number; 615 | stockSymbol: keyof typeof StockSymbols; 616 | type: OrderTypes; 617 | }[] 618 | >; 619 | lastUpdate: number; 620 | storedCycles: number; 621 | ticksUntilCycle: number; 622 | } 623 | 624 | /** 625 | * Enums from Bitburner 626 | */ 627 | export enum CityName { 628 | Aevum = "Aevum", 629 | Chongqing = "Chongqing", 630 | Ishima = "Ishima", 631 | NewTokyo = "New Tokyo", 632 | Sector12 = "Sector-12", 633 | Volhaven = "Volhaven", 634 | } 635 | 636 | export enum Exploit { 637 | Bypass = "Bypass", 638 | PrototypeTampering = "PrototypeTampering", 639 | Unclickable = "Unclickable", 640 | UndocumentedFunctionCall = "UndocumentedFunctionCall", 641 | TimeCompression = "TimeCompression", 642 | RealityAlteration = "RealityAlteration", 643 | N00dles = "N00dles", 644 | // To the players reading this. Yes you're supposed to add EditSaveFile by 645 | // editing your save file, yes you could add them all, no we don't care 646 | // that's not the point. 647 | EditSaveFile = "EditSaveFile", 648 | } 649 | 650 | export enum OrderTypes { 651 | LimitBuy = "Limit Buy Order", 652 | LimitSell = "Limit Sell Order", 653 | StopBuy = "Stop Buy Order", 654 | StopSell = "Stop Sell Order", 655 | } 656 | 657 | export enum PositionTypes { 658 | Long = "L", 659 | Short = "S", 660 | } 661 | } 662 | -------------------------------------------------------------------------------- /src/components/editor/index.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEventHandler, useCallback, useContext, useRef, useState } from "react"; 2 | import clsx from "clsx"; 3 | import { observer } from "mobx-react-lite"; 4 | 5 | import { Bitburner } from "bitburner.types"; 6 | import { FileContext } from "App"; 7 | import EditorSection from "components/editor/section"; 8 | 9 | import { ReactComponent as IconFilter } from "icons/filter.svg"; 10 | 11 | export default observer(function EditorContainer() { 12 | const fileContext = useContext(FileContext); 13 | const navRef = useRef(); 14 | 15 | const [isFiltering, setIsFiltering] = useState(false); 16 | const toggleFiltering = useCallback(() => { 17 | setIsFiltering((f) => !f); 18 | }, []); 19 | 20 | const [activeTab, setActiveTab] = useState(Bitburner.SaveDataKey.PlayerSave); 21 | const onClickTab = useCallback>((event) => { 22 | setActiveTab(event.currentTarget.value as Bitburner.SaveDataKey); 23 | setIsFiltering(false); 24 | navRef.current.scrollTo({ 25 | left: event.currentTarget.offsetLeft - 32, 26 | }); 27 | }, []); 28 | 29 | const scrollRight = useCallback(() => { 30 | navRef.current.scrollBy({ left: navRef.current.scrollWidth * 0.2 }); 31 | }, []); 32 | const scrollLeft = useCallback(() => { 33 | navRef.current.scrollBy({ left: navRef.current.scrollWidth * -0.2 }); 34 | }, []); 35 | 36 | return ( 37 |
38 |
39 | 45 | 66 | 72 |
73 |
74 | {!fileContext.ready && Upload a file to begin...} 75 | {fileContext.ready && } 76 |
77 |
78 | ); 79 | }); 80 | -------------------------------------------------------------------------------- /src/components/editor/section/factions-section.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeEvent, 3 | ChangeEventHandler, 4 | FormEventHandler, 5 | MouseEvent, 6 | MouseEventHandler, 7 | PropsWithChildren, 8 | useCallback, 9 | useContext, 10 | useMemo, 11 | useState, 12 | } from "react"; 13 | import { observer } from "mobx-react-lite"; 14 | import clsx from "clsx"; 15 | import { ascend, descend, path, pick, sortWith } from "ramda"; 16 | 17 | import { FileContext } from "App"; 18 | import { Bitburner } from "bitburner.types"; 19 | import { Checkbox } from "components/inputs/checkbox"; 20 | import { Input } from "components/inputs/input"; 21 | import { formatNumber } from "util/format"; 22 | import { useDebounce } from "util/hooks"; 23 | 24 | import { SortAscendingIcon, SortDescendingIcon } from "@heroicons/react/solid"; 25 | import { ReactComponent as SearchIcon } from "icons/search.svg"; 26 | 27 | export type FactionDataKey = keyof Bitburner.FactionsSaveObject["data"]; 28 | 29 | interface Props extends PropsWithChildren<{}> { 30 | isFiltering?: boolean; 31 | } 32 | export default observer(function FactionSection({ isFiltering }: Props) { 33 | const { factions } = useContext(FileContext); 34 | const [query, setQuery] = useState(""); 35 | const debouncedQuery = useDebounce(query, 500); 36 | const [filters, setFilters] = useState>({ 37 | playerReputation: -1, 38 | }); 39 | 40 | const filteredFactions = useMemo(() => { 41 | const filteredFactions = factions.data.filter(([, faction]) => { 42 | return ( 43 | (!filters.alreadyInvited || faction.data.alreadyInvited) && 44 | (!filters.isMember || faction.data.isMember) && 45 | (!filters.isBanned || faction.data.isBanned) && 46 | (debouncedQuery.length === 0 || faction.data.name.indexOf(debouncedQuery) >= 0) 47 | ); 48 | }); 49 | 50 | // sort 51 | let sortProperty: keyof typeof filters = filters.playerReputation 52 | ? "playerReputation" 53 | : filters.favor 54 | ? "favor" 55 | : undefined; 56 | 57 | if (!sortProperty) return filteredFactions; 58 | 59 | return sortWith( 60 | [filters[sortProperty] > 0 ? ascend(path([1, "data", sortProperty])) : descend(path([1, "data", sortProperty]))], 61 | filteredFactions 62 | ); 63 | }, [factions.data, filters, debouncedQuery]); 64 | 65 | const onSubmit = useCallback( 66 | (faction: string, updates: Partial) => { 67 | factions.updateFaction(faction, updates); 68 | }, 69 | [factions] 70 | ); 71 | 72 | const onEditFilters = useCallback((event: ChangeEvent | MouseEvent) => { 73 | if (event.currentTarget.type === "checkbox") { 74 | const element = event.currentTarget as HTMLInputElement; 75 | const property = element.dataset.key; 76 | const checked = element.checked; 77 | 78 | setFilters((f) => ({ 79 | ...f, 80 | [property]: checked, 81 | })); 82 | } else { 83 | const element = event.currentTarget as HTMLButtonElement; 84 | const property = element.dataset.key as keyof typeof filters; 85 | const otherProperty = property === "favor" ? "playerReputation" : "favor"; 86 | 87 | setFilters((f) => ({ 88 | ...f, 89 | [property]: !f[property] ? -1 : f[property] > 0 ? -1 : 1, 90 | [otherProperty]: undefined, 91 | })); 92 | } 93 | }, []); 94 | 95 | // @TODO: Add sorting 96 | return ( 97 |
98 | {isFiltering && ( 99 | <> 100 |
101 | 111 | 115 | 119 | 123 |
124 |
125 | 137 | 149 |
150 | 151 | )} 152 |
153 | {filteredFactions.map(([faction, factionData]) => ( 154 | 155 | ))} 156 |
157 |
158 | ); 159 | }); 160 | 161 | interface FactionProps extends PropsWithChildren<{}> { 162 | id: string; 163 | faction: Bitburner.FactionsSaveObject; 164 | onSubmit(key: string, value: Partial): void; 165 | } 166 | 167 | const Faction = function Faction({ id, faction, onSubmit }: FactionProps) { 168 | const [editing, setEditing] = useState(false); 169 | const [state, setState] = useState(Object.assign({}, faction.data)); 170 | 171 | const onClickEnter = useCallback>((event) => { 172 | setEditing(true); 173 | // So clicking into the box does not trigger checkboxes 174 | event.preventDefault(); 175 | }, []); 176 | 177 | const onChange = useCallback>((event) => { 178 | const { checked, dataset, type, value } = event.currentTarget; 179 | setState((s) => ({ ...s, [dataset.key]: type === "checkbox" ? checked : value })); 180 | }, []); 181 | 182 | const onClose = useCallback( 183 | (event) => { 184 | // Process rep and favor 185 | const playerReputation = Math.min(Number.MAX_SAFE_INTEGER, Number(state.playerReputation)); 186 | const favor = Math.min(Number.MAX_SAFE_INTEGER, Number(state.favor)); 187 | 188 | onSubmit(id, { 189 | ...pick(["alreadyInvited", "isMember", "isBanned"], state), 190 | playerReputation, 191 | favor, 192 | }); 193 | 194 | setEditing(false); 195 | event.preventDefault(); 196 | }, 197 | [id, state, onSubmit] 198 | ); 199 | 200 | // @TODO: Display Augmentations 201 | return ( 202 | <> 203 |
210 |
211 |
212 |

{faction.data.name}

213 |
214 | 223 | 236 | 240 | 247 | 251 |
254 |
261 | 262 | ); 263 | }; 264 | -------------------------------------------------------------------------------- /src/components/editor/section/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | import { Bitburner } from "bitburner.types"; 4 | import PlayerSection from "./player-section"; 5 | import FactionSection from "./factions-section"; 6 | 7 | interface Props { 8 | tab: Bitburner.SaveDataKey; 9 | isFiltering?: boolean; 10 | } 11 | 12 | export default class EditorSection extends Component { 13 | get component() { 14 | const { tab, ...restProps } = this.props; 15 | switch (tab) { 16 | case Bitburner.SaveDataKey.PlayerSave: 17 | return ; 18 | case Bitburner.SaveDataKey.FactionsSave: 19 | return ; 20 | default: 21 | return
Not Implemented
; 22 | } 23 | } 24 | 25 | render() { 26 | return this.component; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/editor/section/player-section.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext } from "react"; 2 | import { observer } from "mobx-react-lite"; 3 | 4 | import { Bitburner } from "bitburner.types"; 5 | import EditableSection from "./properties/editable"; 6 | import StatSection from "./properties/stat"; 7 | import { FileContext } from "App"; 8 | import { formatMoney, formatNumber } from "util/format"; 9 | 10 | export type PlayerDataKey = keyof Bitburner.PlayerSaveObject["data"]; 11 | 12 | export default observer(function PlayerSection() { 13 | const { player } = useContext(FileContext); 14 | 15 | const onSubmit = useCallback( 16 | (key: PlayerDataKey, value: any) => { 17 | player.updatePlayer({ 18 | [key]: value, 19 | }); 20 | }, 21 | [player] 22 | ); 23 | 24 | return ( 25 |
26 | 34 | {Bitburner.PLAYER_STATS.map((stat) => ( 35 | 41 | ))} 42 | 50 | 57 |
58 | ); 59 | }); 60 | -------------------------------------------------------------------------------- /src/components/editor/section/properties/editable.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeEventHandler, 3 | FormEventHandler, 4 | MouseEventHandler, 5 | PropsWithChildren, 6 | useCallback, 7 | useState, 8 | } from "react"; 9 | import clsx from "clsx"; 10 | 11 | interface Props extends PropsWithChildren<{}> { 12 | formatter?: (...args: any) => string; 13 | label: string; 14 | onSubmit(key: string, value: any): void; 15 | property: string; 16 | type: string; 17 | value: string | number; 18 | } 19 | 20 | export default function EditableSection({ formatter, label, property, onSubmit, type, value: initialValue }: Props) { 21 | const [value, setValue] = useState(initialValue); 22 | const [editing, setEditing] = useState(false); 23 | 24 | const onChange = useCallback>((event) => { 25 | setValue(event.currentTarget.value); 26 | }, []); 27 | 28 | const onClose = useCallback & FormEventHandler>( 29 | (event) => { 30 | let parsedValue: string | number = value; 31 | 32 | if (type === "number") { 33 | parsedValue = Math.min(Number.MAX_VALUE, Number(value)); 34 | } 35 | 36 | onSubmit(property, parsedValue); 37 | setEditing(false); 38 | event.preventDefault(); 39 | }, 40 | [property, onSubmit, type, value] 41 | ); 42 | 43 | return ( 44 | <> 45 |
51 | 70 |
71 |
78 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /src/components/editor/section/properties/stat.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeEventHandler, 3 | FormEventHandler, 4 | MouseEventHandler, 5 | PropsWithChildren, 6 | useCallback, 7 | useContext, 8 | useState, 9 | } from "react"; 10 | import clsx from "clsx"; 11 | import { observer } from "mobx-react-lite"; 12 | import { FileContext } from "App"; 13 | import { calculateExp } from "util/game"; 14 | import { Bitburner } from "bitburner.types"; 15 | 16 | interface Props extends PropsWithChildren<{}> { 17 | property: Bitburner.PlayerStat; 18 | onSubmit(key: string, value: any): void; 19 | } 20 | 21 | export default observer(function StatSection({ property, onSubmit }: Props) { 22 | const { player } = useContext(FileContext); 23 | const [value, setValue] = useState(`${player.data[property]}`); 24 | 25 | const [editing, setEditing] = useState(false); 26 | 27 | const onChange = useCallback>((event) => { 28 | setValue(event.currentTarget.value); 29 | }, []); 30 | 31 | const onClose = useCallback & FormEventHandler>( 32 | (event) => { 33 | const desiredLevel = Math.min(Number.MAX_SAFE_INTEGER, Number(value)); 34 | let mult = property === "intelligence" ? 1 : player.data[`${property}_exp_mult`]; 35 | 36 | // @TODO: Handle augmentations 37 | 38 | onSubmit(`${property}_exp`, calculateExp(desiredLevel, mult)); 39 | onSubmit(`${property}`, desiredLevel); 40 | setEditing(false); 41 | event.preventDefault(); 42 | }, 43 | [property, onSubmit, value, player.data] 44 | ); 45 | 46 | return ( 47 | <> 48 |
54 | 80 |
81 |
88 | 89 | ); 90 | }); 91 | -------------------------------------------------------------------------------- /src/components/file-loader.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEventHandler, useCallback, useContext } from "react"; 2 | import { FileContext } from "../App"; 3 | 4 | import { ReactComponent as UploadIcon } from "icons/upload.svg"; 5 | import { observer } from "mobx-react-lite"; 6 | 7 | export default observer(function FileLoader() { 8 | const fileContext = useContext(FileContext); 9 | 10 | const onSelectFile = useCallback>( 11 | async (event) => { 12 | const { files } = event.currentTarget; 13 | await fileContext.uploadFile(files[0]); 14 | }, 15 | [fileContext] 16 | ); 17 | 18 | return ( 19 | 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /src/components/inputs/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEventHandler, PropsWithChildren } from "react"; 2 | 3 | import { ReactComponent as CheckIcon } from "icons/check.svg"; 4 | import clsx from "clsx"; 5 | 6 | interface Props extends PropsWithChildren<{}> { 7 | checked?: boolean; 8 | className?: string; 9 | "data-key"?: string; 10 | disabled?: boolean; 11 | onChange: ChangeEventHandler; 12 | value?: string; 13 | } 14 | 15 | export function Checkbox({ checked, className, "data-key": dataKey, disabled, onChange, value }: Props) { 16 | return ( 17 | <> 18 |
19 | 25 |
26 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/inputs/input.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { InputHTMLAttributes, PropsWithChildren } from "react"; 3 | 4 | interface Props extends PropsWithChildren> { 5 | "data-key"?: string; 6 | } 7 | 8 | export function Input({ "data-key": dataKey, className, ...props }: Props) { 9 | return ( 10 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body, 7 | #root { 8 | @apply h-full w-full; 9 | } 10 | 11 | #root { 12 | @apply p-8 bg-gray-900 text-white font-mono; 13 | } 14 | 15 | ::-webkit-scrollbar { 16 | @apply w-1 h-1 p-1; 17 | } 18 | 19 | ::-webkit-scrollbar-track { 20 | @apply bg-slate-900; 21 | } 22 | 23 | ::-webkit-scrollbar-thumb { 24 | @apply bg-gray-800; 25 | } 26 | 27 | ::-webkit-scrollbar-thumb:hover { 28 | @apply bg-slate-700; 29 | } 30 | 31 | input::-webkit-inner-spin-button { 32 | appearance: none; 33 | } 34 | 35 | .scroll-hidden::-webkit-scrollbar { 36 | @apply hidden; 37 | } 38 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import reportWebVitals from "./reportWebVitals"; 5 | 6 | import "./index.css"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById("root") 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from "web-vitals"; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /src/store/file.store.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from "buffer"; 2 | import { Bitburner } from "bitburner.types"; 3 | import { makeAutoObservable } from "mobx"; 4 | 5 | export class FileStore { 6 | _file: File; 7 | save: Bitburner.SaveData; 8 | 9 | constructor() { 10 | makeAutoObservable(this); 11 | 12 | // @ts-ignore 13 | window.store = this; 14 | } 15 | 16 | get file() { 17 | return this._file; 18 | } 19 | 20 | get ready() { 21 | return !!this.save; 22 | } 23 | 24 | get player() { 25 | return { 26 | data: this.save.data.PlayerSave.data, 27 | updatePlayer: this.updatePlayer, 28 | }; 29 | } 30 | 31 | updatePlayer = (updates: Partial) => { 32 | Object.assign(this.save.data.PlayerSave.data, updates); 33 | }; 34 | 35 | get factions() { 36 | return { 37 | data: Object.entries(this.save.data.FactionsSave).sort( 38 | (a, b) => b[1].data.playerReputation - a[1].data.playerReputation 39 | ), 40 | updateFaction: this.updateFaction, 41 | }; 42 | } 43 | 44 | updateFaction = (faction: string, updates: Partial) => { 45 | Object.assign(this.save.data.FactionsSave[faction].data, updates); 46 | 47 | if (updates.isMember) { 48 | this.updatePlayer({ factions: Array.from(new Set(this.player.data.factions.concat(faction))) }); 49 | } else { 50 | this.updatePlayer({ factions: this.player.data.factions.filter((f) => f !== faction) }); 51 | } 52 | if (updates.alreadyInvited && !updates.isMember) { 53 | this.updatePlayer({ 54 | factionInvitations: Array.from(new Set(this.player.data.factionInvitations.concat(faction))), 55 | }); 56 | } else { 57 | this.updatePlayer({ factionInvitations: this.player.data.factionInvitations.filter((f) => f !== faction) }); 58 | } 59 | }; 60 | 61 | clearFile = () => { 62 | this._file = undefined; 63 | this.save = undefined; 64 | }; 65 | 66 | uploadFile = async (file: File) => { 67 | this.clearFile(); 68 | this._file = file; 69 | await this.processFile(); 70 | }; 71 | 72 | processFile = async () => { 73 | const buffer = Buffer.from(await this.file.text(), "base64"); 74 | 75 | const rawData: Bitburner.RawSaveData = JSON.parse(buffer.toString()); 76 | 77 | if (rawData.ctor !== "BitburnerSaveObject") { 78 | throw new Error("Invalid save file"); 79 | } 80 | 81 | const data: any = {}; 82 | 83 | for (const key of Object.values(Bitburner.SaveDataKey)) { 84 | if (!rawData.data[key]) { 85 | data[key] = null; 86 | } else { 87 | data[key] = JSON.parse(rawData.data[key]); 88 | } 89 | } 90 | 91 | this.setSaveData({ 92 | ctor: Bitburner.Ctor.BitburnerSaveObject, 93 | data, 94 | }); 95 | 96 | if (!this.save.data.PlayerSave.data.exploits.includes(Bitburner.Exploit.EditSaveFile)) { 97 | console.info("Applying EditSaveFile exploit!"); 98 | this.save.data.PlayerSave.data.exploits.push(Bitburner.Exploit.EditSaveFile); 99 | } 100 | 101 | console.info("File processed..."); 102 | }; 103 | 104 | downloadFile = () => { 105 | const rawData: Partial = { 106 | ctor: Bitburner.Ctor.BitburnerSaveObject, 107 | }; 108 | 109 | const data: any = {}; 110 | 111 | Object.values(Bitburner.SaveDataKey).forEach((key) => { 112 | // Each key's value needs to be stringified independently 113 | if (this.save.data[key] === null) { 114 | data[key] = ""; 115 | } else { 116 | data[key] = JSON.stringify(this.save.data[key]); 117 | } 118 | }); 119 | 120 | rawData.data = data; 121 | 122 | const encodedData = Buffer.from(JSON.stringify(rawData)).toString("base64"); 123 | 124 | const blobUrl = window.URL.createObjectURL(new Blob([encodedData], { type: "base64" })); 125 | 126 | // Trick to start a download 127 | const downloadLink = document.createElement("a"); 128 | downloadLink.style.display = "none"; 129 | downloadLink.href = blobUrl; 130 | const match = this.file.name.match(/bitburnerSave_(?\d+)_(?BN.+?)(?:-H4CKeD)*?.json/); 131 | 132 | downloadLink.download = `bitburnerSave_${ 133 | Math.floor(Date.now() / 1000) // Seconds, not milliseconds 134 | }_${match.groups.bn ?? "BN1x0"}-H4CKeD.json`; 135 | 136 | document.body.appendChild(downloadLink); 137 | downloadLink.click(); 138 | 139 | downloadLink.remove(); 140 | 141 | window.URL.revokeObjectURL(blobUrl); 142 | 143 | return encodedData; 144 | }; 145 | 146 | setSaveData = (save: typeof this.save) => { 147 | this.save = save; 148 | }; 149 | } 150 | 151 | export default new FileStore(); 152 | -------------------------------------------------------------------------------- /src/util/format.ts: -------------------------------------------------------------------------------- 1 | const locale = navigator.languages?.[0] ?? navigator.language; 2 | 3 | export const formatMoney = new Intl.NumberFormat(locale, { 4 | style: "currency", 5 | currency: "USD", 6 | // currencySign: "$", 7 | }).format; 8 | 9 | export const formatNumber = new Intl.NumberFormat(locale).format; 10 | -------------------------------------------------------------------------------- /src/util/game.ts: -------------------------------------------------------------------------------- 1 | export function calculateSkill(exp: number, mult = 1): number { 2 | return Math.max(Math.floor(mult * (32 * Math.log(exp + 534.5) - 200)), 1); 3 | } 4 | 5 | export function calculateExp(skill: number, mult = 1): number { 6 | return Math.ceil(Math.exp((skill / mult + 200) / 32) - 534.6); 7 | } 8 | -------------------------------------------------------------------------------- /src/util/hooks.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export function useDebounce(value: T, delay: number) { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const handler = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, delay); 10 | 11 | return () => { 12 | clearTimeout(handler); 13 | }; 14 | }, [value, delay]); 15 | 16 | return debouncedValue; 17 | } 18 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.tsx"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | }; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext", 8 | "es2016", 9 | "es2017.object", 10 | "es2019" 11 | ], 12 | "allowJs": true, 13 | "allowSyntheticDefaultImports": true, 14 | "baseUrl": "src", 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "isolatedModules": true, 18 | "jsx": "react-jsx", 19 | "module": "esnext", 20 | "moduleResolution": "node", 21 | "noEmit": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "resolveJsonModule": true, 24 | "skipLibCheck": true, 25 | "strict": true, 26 | "strictNullChecks": false 27 | }, 28 | "exclude": ["node_modules"], 29 | "include": ["src/**/*"] 30 | } 31 | --------------------------------------------------------------------------------