├── .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 |
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 |
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 |
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 |
4 |
--------------------------------------------------------------------------------
/src/icons/close.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/icons/download.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/filter.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/icons/search.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/icons/upload.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------