-Returns the server's authentication protocol details.
+Returns the darknet-specific details of the server.
+
+If the darknet server has recently gone offline, the returned object will be a dummy server object with `isOnline: false`.
diff --git a/markdown/bitburner.darknetformulas.getauthenticatetime.md b/markdown/bitburner.darknetformulas.getauthenticatetime.md
index cba49b2b3..ee77bed00 100644
--- a/markdown/bitburner.darknetformulas.getauthenticatetime.md
+++ b/markdown/bitburner.darknetformulas.getauthenticatetime.md
@@ -9,7 +9,12 @@ Gets the time it will take to authenticate a server.
**Signature:**
```typescript
-getAuthenticateTime(darknetServerData: DarknetServerData, threads?: number, player?: Person): number;
+getAuthenticateTime(
+ serverDetails: DarknetServerDetails,
+ threads?: number,
+ player?: Person,
+ correctCharactersInPassword?: number,
+ ): number;
```
## Parameters
@@ -32,12 +37,12 @@ Description
-
-The model of the server. Similar models have similar vulnerabilities. The model list is intentionally undocumented. You are supposed to experiment and discover the models.
-
-
-
+
+The model of the server. Similar models have similar vulnerabilities. The model list is intentionally undocumented. You are supposed to experiment and discover the models.
+
+
+
-Darknet server data.
+Details about a darknet server
@@ -1218,17 +1218,6 @@ Various info about resets
A server. Not all servers have all of these properties - optional properties are missing on certain servers.
-
-
-
-Details about a server's authentication schema
-
-
diff --git a/markdown/bitburner.ns.getfunctionramcost.md b/markdown/bitburner.ns.getfunctionramcost.md
index fc0afe171..b1c22cb65 100644
--- a/markdown/bitburner.ns.getfunctionramcost.md
+++ b/markdown/bitburner.ns.getfunctionramcost.md
@@ -4,7 +4,9 @@
## NS.getFunctionRamCost() method
-Get the ram cost of a netscript function.
+Get the RAM cost of a netscript function.
+
+The base RAM cost per script thread can also be retrieved by using `"baseCost"` as argument to this function.
**Signature:**
@@ -42,7 +44,7 @@ string
-The fully-qualified function name, without the leading `ns`. Example inputs: `hack`, `tprint`, `stock.getPosition`.
+The fully-qualified function name, without the leading `ns`.
@@ -56,3 +58,13 @@ number
RAM cost: 0 GB
+## Example
+
+
+```js
+const RAM_baseCost = ns.getFunctionRamCost('baseCost');
+const RAM_for_hack = ns.getFunctionRamCost('hack');
+const RAM_for_tprint = ns.getFunctionRamCost('tprint');
+const RAM_for_stock_getPosition = ns.getFunctionRamCost('stock.getPosition');
+```
+
diff --git a/markdown/bitburner.ns.getserver.md b/markdown/bitburner.ns.getserver.md
index 4b1fd08a2..22ebe3ecd 100644
--- a/markdown/bitburner.ns.getserver.md
+++ b/markdown/bitburner.ns.getserver.md
@@ -6,12 +6,12 @@
Returns data of a server.
-If the server is a darknet server and has recently gone offline, it will return a dummy server object with `isOnline: false`.
+If the server is a darknet server, it will also contain the "isOnline" field. If the darknet server has recently gone offline, the returned object will be a dummy server object with `isOnline: false`.
**Signature:**
```typescript
-getServer(host?: string): Server | (DarknetServerData & { isOnline: boolean });
+getServer(host?: string): Server & { isOnline?: boolean };
```
## Parameters
@@ -52,7 +52,7 @@ _(Optional)_ Optional. Hostname/IP of the server. Defaults to the hostname of th
**Returns:**
-[Server](./bitburner.server.md) \| ([DarknetServerData](./bitburner.darknetserverdata.md) & { isOnline: boolean })
+[Server](./bitburner.server.md) & { isOnline?: boolean }
Data of the server.
diff --git a/markdown/bitburner.ns.isrunning.md b/markdown/bitburner.ns.isrunning.md
index 4e6c69140..2ab1c98dd 100644
--- a/markdown/bitburner.ns.isrunning.md
+++ b/markdown/bitburner.ns.isrunning.md
@@ -9,7 +9,7 @@ Check if a script is running.
**Signature:**
```typescript
-isRunning(script: FilenameOrPID, host?: string, ...args: ScriptArg[]): boolean;
+isRunning(script?: FilenameOrPID, host?: string, ...args: ScriptArg[]): boolean;
```
## Parameters
@@ -42,7 +42,7 @@ script
-Filename or PID of script to check. This is case-sensitive.
+_(Optional)_ Filename (case-sensitive) or PID of script to check. Optional, default to the current script's pid.
diff --git a/markdown/bitburner.ns.md b/markdown/bitburner.ns.md
index 1c710f931..b82bc28f2 100644
--- a/markdown/bitburner.ns.md
+++ b/markdown/bitburner.ns.md
@@ -718,7 +718,9 @@ Get the metadata of a file.
-Get the ram cost of a netscript function.
+Get the RAM cost of a netscript function.
+
+The base RAM cost per script thread can also be retrieved by using `"baseCost"` as argument to this function.
@@ -929,7 +931,7 @@ Get the ram cost of a script.
Returns data of a server.
-If the server is a darknet server and has recently gone offline, it will return a dummy server object with `isOnline: false`.
+If the server is a darknet server, it will also contain the "isOnline" field. If the darknet server has recently gone offline, the returned object will be a dummy server object with `isOnline: false`.
diff --git a/markdown/bitburner.ns.rm.md b/markdown/bitburner.ns.rm.md
index 724fc1be0..defa2f667 100644
--- a/markdown/bitburner.ns.rm.md
+++ b/markdown/bitburner.ns.rm.md
@@ -72,7 +72,7 @@ True if it successfully deletes the file, and false otherwise.
## Remarks
-RAM cost: 1 GB
+RAM cost: 0.6 GB
Removes the specified file from the current server. This function works for every file type except message (.msg) files.
diff --git a/markdown/bitburner.ns.scp.md b/markdown/bitburner.ns.scp.md
index d10a81afe..b0246e19e 100644
--- a/markdown/bitburner.ns.scp.md
+++ b/markdown/bitburner.ns.scp.md
@@ -42,7 +42,7 @@ string \| string\[\]
-Filename or an array of filenames of script/literature files to copy. Note that if a file is located in a subdirectory, the filename must include the leading `/`.
+Filename or an array of filenames of text/script/literature files to copy. Note that if a file is located in a subdirectory, the filename must include the leading `/`.
@@ -90,7 +90,7 @@ True if the file is successfully copied over and false otherwise. If the files a
RAM cost: 0.6 GB
-Copies a script or literature (.lit) file(s) to another server. The files argument can be either a string specifying a single file to copy, or an array of strings specifying multiple files to copy.
+Copies text, script or literature (.lit) file(s) to another server. The files argument can be either a string specifying a single file to copy, or an array of strings specifying multiple files to copy.
## Example 1
diff --git a/markdown/bitburner.serverauthdetails.data.md b/markdown/bitburner.serverauthdetails.data.md
deleted file mode 100644
index ad8c511bd..000000000
--- a/markdown/bitburner.serverauthdetails.data.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[Home](./index.md) > [bitburner](./bitburner.md) > [ServerAuthDetails](./bitburner.serverauthdetails.md) > [data](./bitburner.serverauthdetails.data.md)
-
-## ServerAuthDetails.data property
-
-Data from the passwordHint, if any.
-
-**Signature:**
-
-```typescript
-data: string;
-```
diff --git a/markdown/bitburner.serverauthdetails.isconnectedtocurrentserver.md b/markdown/bitburner.serverauthdetails.isconnectedtocurrentserver.md
deleted file mode 100644
index cee9962ee..000000000
--- a/markdown/bitburner.serverauthdetails.isconnectedtocurrentserver.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[Home](./index.md) > [bitburner](./bitburner.md) > [ServerAuthDetails](./bitburner.serverauthdetails.md) > [isConnectedToCurrentServer](./bitburner.serverauthdetails.isconnectedtocurrentserver.md)
-
-## ServerAuthDetails.isConnectedToCurrentServer property
-
-True if the server is directly connected to the current server
-
-**Signature:**
-
-```typescript
-isConnectedToCurrentServer: boolean;
-```
diff --git a/markdown/bitburner.serverauthdetails.md b/markdown/bitburner.serverauthdetails.md
deleted file mode 100644
index 767c5de69..000000000
--- a/markdown/bitburner.serverauthdetails.md
+++ /dev/null
@@ -1,191 +0,0 @@
-
-
-[Home](./index.md) > [bitburner](./bitburner.md) > [ServerAuthDetails](./bitburner.serverauthdetails.md)
-
-## ServerAuthDetails interface
-
-Details about a server's authentication schema
-
-**Signature:**
-
-```typescript
-interface ServerAuthDetails
-```
-
-## Properties
-
-
-
diff --git a/markdown/bitburner.serverauthdetails.modelid.md b/markdown/bitburner.serverauthdetails.modelid.md
deleted file mode 100644
index 4fa3e4d67..000000000
--- a/markdown/bitburner.serverauthdetails.modelid.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[Home](./index.md) > [bitburner](./bitburner.md) > [ServerAuthDetails](./bitburner.serverauthdetails.md) > [modelId](./bitburner.serverauthdetails.modelid.md)
-
-## ServerAuthDetails.modelId property
-
-The model ID of the server. Similar models share vulnerabilities.
-
-**Signature:**
-
-```typescript
-modelId: string;
-```
diff --git a/markdown/bitburner.serverauthdetails.passwordhint.md b/markdown/bitburner.serverauthdetails.passwordhint.md
deleted file mode 100644
index ca41a4d14..000000000
--- a/markdown/bitburner.serverauthdetails.passwordhint.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[Home](./index.md) > [bitburner](./bitburner.md) > [ServerAuthDetails](./bitburner.serverauthdetails.md) > [passwordHint](./bitburner.serverauthdetails.passwordhint.md)
-
-## ServerAuthDetails.passwordHint property
-
-Static password reminder text set for this server.
-
-**Signature:**
-
-```typescript
-passwordHint: string;
-```
diff --git a/markdown/bitburner.serverauthdetails.passwordlength.md b/markdown/bitburner.serverauthdetails.passwordlength.md
deleted file mode 100644
index 1098d797d..000000000
--- a/markdown/bitburner.serverauthdetails.passwordlength.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[Home](./index.md) > [bitburner](./bitburner.md) > [ServerAuthDetails](./bitburner.serverauthdetails.md) > [passwordLength](./bitburner.serverauthdetails.passwordlength.md)
-
-## ServerAuthDetails.passwordLength property
-
-The number of characters in the password
-
-**Signature:**
-
-```typescript
-passwordLength: number;
-```
diff --git a/markdown/bitburner.singularity.getaugmentationbaseprice.md b/markdown/bitburner.singularity.getaugmentationbaseprice.md
index 4888bd3bb..6f2a94cfa 100644
--- a/markdown/bitburner.singularity.getaugmentationbaseprice.md
+++ b/markdown/bitburner.singularity.getaugmentationbaseprice.md
@@ -52,9 +52,11 @@ Name of Augmentation.
number
-Base price of the augmentation, before price multiplier.
+Base price of the augmentation, before the player's price multiplier.
## Remarks
RAM cost: 2.5 GB \* 16/4/1
+This excludes the player's price multiplier, but does include the relevant BitNode multiplier (for all augs that aren't part of Shadows of Anarchy, which doesn't use BitNode multipliers).
+
diff --git a/markdown/bitburner.singularity.getdarkwebprograms.md b/markdown/bitburner.singularity.getdarkwebprograms.md
index 9372aec13..ff5d94c11 100644
--- a/markdown/bitburner.singularity.getdarkwebprograms.md
+++ b/markdown/bitburner.singularity.getdarkwebprograms.md
@@ -9,11 +9,11 @@ Get a list of programs offered on the dark web.
**Signature:**
```typescript
-getDarkwebPrograms(): string[];
+getDarkwebPrograms(): ProgramName[];
```
**Returns:**
-string\[\]
+[ProgramName](./bitburner.programname.md)\[\]
- a list of programs available for purchase on the dark web, or \[\] if Tor has not been purchased
diff --git a/package-lock.json b/package-lock.json
index 56014234c..7bb2b7f02 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "bitburner",
- "version": "3.0.0",
+ "version": "3.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bitburner",
- "version": "3.0.0",
+ "version": "3.0.1",
"hasInstallScript": true,
"license": "SEE LICENSE IN license.txt",
"dependencies": {
diff --git a/package.json b/package.json
index 46c0418b4..622802a08 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "bitburner",
"license": "SEE LICENSE IN license.txt",
- "version": "3.0.0",
+ "version": "3.0.1",
"main": "electron-main.js",
"author": {
"name": "Daniel Xie, hydroflame, et al."
diff --git a/src/Augmentation/AugmentationHelpers.ts b/src/Augmentation/AugmentationHelpers.ts
index eb867088e..affede475 100644
--- a/src/Augmentation/AugmentationHelpers.ts
+++ b/src/Augmentation/AugmentationHelpers.ts
@@ -14,7 +14,7 @@ import { mergeMultipliers } from "../PersonObjects/Multipliers";
import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
import { prestigeWorkerScripts } from "../NetscriptWorker";
-const soaAugmentationNames = [
+export const soaAugmentationNames = [
AugmentationName.BeautyOfAphrodite,
AugmentationName.ChaosOfDionysus,
AugmentationName.FloodOfPoseidon,
diff --git a/src/Augmentation/Augmentations.ts b/src/Augmentation/Augmentations.ts
index a5bd533d2..242b71b18 100644
--- a/src/Augmentation/Augmentations.ts
+++ b/src/Augmentation/Augmentations.ts
@@ -75,7 +75,7 @@ export const Augmentations: Record = (() => {
repCost: 1e4,
moneyCost: 1e6,
info:
- "Pheromone extruder injected in the thoracodorsal nerve. Emits pleasing scent guaranteed to " +
+ "Pheromone extruder injected in the thoracodorsal nerve. Emits a pleasing scent guaranteed to " +
"make conversational partners more agreeable.",
stats: "This augmentation makes the Bribe minigame easier by indicating the incorrect paths.",
charisma: 1.1,
@@ -90,7 +90,7 @@ export const Augmentations: Record = (() => {
info:
"A chip containing the psyche of the greatest BitRunner to ever exist. " +
"Installing this relic significantly increases ALL of your stats. " +
- "However, it may have unintended consequence on the users mental well-being.",
+ "However, it may have unintended consequences on the user's mental well-being.",
stats: "Grants access to unimaginable power.",
hacking: 2,
strength: 2,
@@ -274,7 +274,7 @@ export const Augmentations: Record = (() => {
`A cybernetic foot augmentation that was specifically created for ${FactionName.Bladeburners} ` +
"during the Synthoid Uprising. The organic musculature of the human foot " +
"is enhanced with flexible carbon nanotube matrices that are controlled by " +
- "intelligent servo-motors.",
+ "intelligent servomotors.",
agility: 1.05,
bladeburner_max_stamina: 1.05,
bladeburner_stamina_gain: 1.05,
@@ -285,10 +285,10 @@ export const Augmentations: Record = (() => {
repCost: 1.25e3,
moneyCost: 1.5e11,
info:
- "A highly-advanced matter phase-shifter module that is embedded " +
+ "A highly advanced matter phase-shifter module that is embedded " +
"in the brainstem and cerebellum. This augmentation allows " +
"the user to project and control a holographic simulacrum within an " +
- "extremely large radius. These specially-modified holograms were specifically " +
+ "extremely large radius. These specially modified holograms were specifically " +
"weaponized by Bladeburner units to be used against Synthoids.",
stats:
"This augmentation allows you to perform Bladeburner actions and other actions (such as working, committing crimes, etc.) at the same time.",
@@ -1028,7 +1028,7 @@ export const Augmentations: Record = (() => {
repCost: 1.5e3,
moneyCost: 5e6,
info:
- "A skin implant that reinforces the skin with highly-advanced synthetic cells. These " +
+ "A skin implant that reinforces the skin with highly advanced synthetic cells. These " +
"cells, when powered, have a negative refractive index. As a result, they bend light " +
"around the skin, making the user much harder to see with the naked eye.",
agility: 1.05,
@@ -1041,7 +1041,7 @@ export const Augmentations: Record = (() => {
moneyCost: 3e7,
info:
"This is a more advanced version of the LuminCloaking-V1 augmentation. This skin implant " +
- "reinforces the skin with highly-advanced synthetic cells. These " +
+ "reinforces the skin with highly advanced synthetic cells. These " +
"cells, when powered, are capable of not only bending light but also of bending heat, " +
"making the user more resilient as well as stealthy.",
prereqs: [AugmentationName.LuminCloaking1],
@@ -1095,7 +1095,7 @@ export const Augmentations: Record = (() => {
repCost: 5.625e5,
moneyCost: 2.875e9,
info:
- "A highly-advanced techno-organic drug that is injected into the skeletal " +
+ "A highly advanced techno-organic drug that is injected into the skeletal " +
"and integumentary system. The drug permanently modifies the DNA of the " +
"body's skin and bone cells, granting them the ability to repair " +
"and restructure themselves.",
@@ -1459,7 +1459,7 @@ export const Augmentations: Record = (() => {
moneyCost: 3.375e9,
info:
"A cutting-edge knowledgebase entirely built off of nanotech rod-logic, training the user on social engineering. " +
- "Thought to be stolen technology, its existance has been a secret until recently.",
+ "Thought to be stolen technology, its existence has been a secret until recently.",
charisma: 1.1,
charisma_exp: 1.4,
factions: [FactionName.TheDarkArmy, FactionName.TheSyndicate],
@@ -1507,7 +1507,7 @@ export const Augmentations: Record = (() => {
moneyCost: 4.875e9,
info:
"The SPTN-97 gene is injected into the genome. The SPTN-97 gene is an " +
- "artificially-synthesized gene that was developed by DARPA to create " +
+ "artificially synthesized gene that was developed by DARPA to create " +
"super-soldiers through genetic modification. The gene was outlawed in " +
"2056.",
strength: 1.75,
@@ -1708,7 +1708,7 @@ export const Augmentations: Record = (() => {
repCost: 8.75e5,
moneyCost: 3.25e9,
info:
- "The NEMEAN Subdermal Weave is a thin, light-weight, graphene plating that houses a dilatant fluid. " +
+ "The NEMEAN Subdermal Weave is a thin, lightweight, graphene plating that houses a dilatant fluid. " +
"The material is implanted underneath the skin, and is the most advanced form of defensive enhancement " +
"that has ever been created. The dilatant fluid, despite being thin and light, is extremely effective " +
"at stopping piercing blows and reducing blunt trauma. The properties of graphene allow the plating to " +
@@ -1882,8 +1882,8 @@ export const Augmentations: Record = (() => {
repCost: 1e4,
moneyCost: 1e6,
info:
- "This unique augmentation allows the user to strike stright to the heart of the matter and sweep aside obstacles in the way of their goals. " +
- "Appearing as a simple insignia on the user's forarm, its true function is unknown. It is said to be one of the tools of The Sculptor. " +
+ "This unique augmentation allows the user to strike straight to the heart of the matter and sweep aside obstacles in the way of their goals. " +
+ "Appearing as a simple insignia on the user's forearm, its true function is unknown. It is said to be one of the tools of The Sculptor. " +
"Awarded to those who discover the secrets of the labyrinth.",
stats:
"This augmentation increases the stasis link limit by one, and raises charisma by 7%, strength by 10%, and darknet money by 10%.",
@@ -1898,7 +1898,7 @@ export const Augmentations: Record = (() => {
repCost: 1e4,
moneyCost: 1e6,
info:
- "This skeletal augmentation greatly enhances the users durability and health. Inspired by the original Staff of Medicine that is said to " +
+ "This skeletal augmentation greatly enhances the user's durability and health. Inspired by the original Staff of Medicine that is said to " +
"have been given to Daedalus as a reward for the completion of the Labyrinth, which all modern augments are a descendant of. ",
stats:
"This augmentation increases the stasis link limit by one, and raises charisma xp, defense, and darknet money by 10%.",
diff --git a/src/BitNode/BitNode.tsx b/src/BitNode/BitNode.tsx
index ed4a8ae1c..189d92118 100644
--- a/src/BitNode/BitNode.tsx
+++ b/src/BitNode/BitNode.tsx
@@ -836,7 +836,7 @@ export function getBitNodeMultipliers(n: number, lvl: number): BitNodeMultiplier
StaneksGiftPowerMultiplier: 0.5,
StaneksGiftExtraSize: 2,
- DarknetMoneyMultiplier: 0.5,
+ DarknetMoneyMultiplier: 0.05,
WorldDaemonDifficulty: 2,
});
@@ -1036,7 +1036,7 @@ export function getBitNodeMultipliers(n: number, lvl: number): BitNodeMultiplier
StaneksGiftPowerMultiplier: 2,
StaneksGiftExtraSize: 1,
- DarknetMoneyMultiplier: 0.5,
+ DarknetMoneyMultiplier: 0.1,
WorldDaemonDifficulty: 3,
});
diff --git a/src/BitNode/ui/BitnodeMultipliersDescription.tsx b/src/BitNode/ui/BitnodeMultipliersDescription.tsx
index 67b50e00f..a0865964c 100644
--- a/src/BitNode/ui/BitnodeMultipliersDescription.tsx
+++ b/src/BitNode/ui/BitnodeMultipliersDescription.tsx
@@ -68,6 +68,7 @@ export const BitNodeMultipliersDisplay = ({ n, level, hideMultsIfCannotAccessFea
+
@@ -226,6 +227,17 @@ function CrimeMults({ mults }: IMultsProps): React.ReactElement {
return ;
}
+function DarknetMults({ mults }: IMultsProps): React.ReactElement {
+ const rows: IBNMultRows = {
+ DarknetMoneyMultiplier: {
+ name: "Darknet Money",
+ color: Settings.theme.money,
+ },
+ };
+
+ return ;
+}
+
function SkillMults({ mults }: IMultsProps): React.ReactElement {
const rows: IBNMultRows = {
HackingLevelMultiplier: {
diff --git a/src/Bladeburner/Bladeburner.ts b/src/Bladeburner/Bladeburner.ts
index b47ffbe99..7405971ca 100644
--- a/src/Bladeburner/Bladeburner.ts
+++ b/src/Bladeburner/Bladeburner.ts
@@ -1274,9 +1274,11 @@ export class Bladeburner implements OperationTeam {
}
this.maxRank = Math.max(this.rank, this.maxRank);
- const bladeburnerFaction = Factions[FactionName.Bladeburners];
- if (bladeburnerFaction.isMember) {
- bladeburnerFaction.playerReputation += calculateActionReputationGain(person, change);
+ if (change > 0) {
+ const bladeburnerFaction = Factions[FactionName.Bladeburners];
+ if (bladeburnerFaction.isMember) {
+ bladeburnerFaction.playerReputation += calculateActionReputationGain(person, change);
+ }
}
// Gain skill points
diff --git a/src/Constants.ts b/src/Constants.ts
index 4de93c313..8debff3c8 100644
--- a/src/Constants.ts
+++ b/src/Constants.ts
@@ -4,10 +4,10 @@
* Constants for specific mechanics or features will NOT be here.
*/
export const CONSTANTS = {
- VersionString: "3.0.0",
+ VersionString: "3.0.1",
isDevBranch: false,
isInTestEnvironment: globalThis.process?.env?.JEST_WORKER_ID !== undefined,
- VersionNumber: 50,
+ VersionNumber: 51,
/** Max level for any skill, assuming no multipliers. Determined by max numerical value in javascript for experience
* and the skill level formula in Player.js. Note that all this means it that when experience hits MAX_INT, then
@@ -111,467 +111,57 @@ export const CONSTANTS = {
// Also update Documentation/doc/en/changelog.md when appropriate (when doing a release)
LatestUpdate: `
-## v3.0.0 Release: 1 May 2026
+## v3.0.1 Dev: 17 May 2026
### BREAKING CHANGES
-- Remove API server (#2084) (@catloversg)
-- Remove support for running NS1 scripts (#2083) (@catloversg)
-- Enforce stricter param check on ns.getBitNodeMultipliers and ns.hacknet.spendHashes (#2085) (@catloversg)
-- Remove APIs that were deprecated a long time ago (#2088) (@catloversg)
-- Moved formatting functions to their own interface (#1635) (@G4mingJon4s)
-- Remove fuzzy matching when checking params (#2091) (@catloversg)
-- Remove deprecated tail-related APIs (#2143) (@catloversg)
-- Rename setAutoJobAssignment API to setJobAssignment (#2146) (@catloversg)
-- Make nuke and port cracking APIs return false instead of throwing error (#1023, #2153) (@Hydrogeniouss, @catloversg)
-- Standardize names of Stock APIs (#2173) (@catloversg)
-- Rename BN multiplier RepToDonateToFaction to FavorToDonateToFaction (#2178) (@catloversg)
-- Make ns.bladeburner.getActionRepGain return the expected reputation gain (#2186) (@catloversg)
-- Rename FactionName.BachmanAssociates to FactionName.BachmanAndAssociates (#2048, #2183) (@masarakki, @catloversg)
-- Remove DreamSense upgrade (#2232) (@catloversg)
-- Use different term for dividend modifier instead of tax (#2237) (@catloversg)
-- Remove Spring Water industry (#2240) (@catloversg)
-- Remove VeChain (#2245) (@catloversg)
-- Make some APIs throw error when server is invalid (#2261) (@catloversg)
-- Rename equipment that uses real brand names (#2293) (@catloversg)
-- Make TIX access independent from WSE account (#2342) (@catloversg)
-- Move and rename purchased server functions to cloud API (#2367) (@gmcew)
-- Fix: Coding contracts may have duplicate names (#2399) (@aaaa-imcute)
-- Generate test contracts on executing host by default. Add support for optional parameter to specify the server (#2417) (@1337JiveTurkey)
-- The "darkweb" server becomes a darknet server (Check new Dark Net feature in #2139) (@ficocelliguy)
-- Remove RAM cost of hacknet namespace and set RAM cost of each hacknet API (#2502) (@catloversg)
-- Cancel sleeve's current task when calling ns.sleeve.travel() (#2559) (@catloversg)
-- Make ns.cloud.purchaseServer() and ns.cloud.deleteServer() use hostname as provided (#2560) (@catloversg)
-- Make implicit string conversion consistent across all coding contracts (#2608) (@catloversg)
-- Rename ns.gang.getOtherGangInformation to getAllGangInformation (#2635) (@lstutzman)
-- Standardize "nextCompletion" promise in tasks (#2687) (@catloversg)
-
-### MAJOR CHANGES
-
-- Added Darknet, a new mechanic based on spreading through an unstable network and cracking passwords. Purchase DarkscapeNavigator.exe in the terminal to unlock access. (#2139) (@ficocelliguy)
-- Balance change: IPvGO: Improve favor gain from wins to balance around the rep value of favor (#2131) (@ficocelliguy)
-- Search and read NS API docs in editor tab and documentation tab (#2163) (@catloversg)
-- Balance change: Infiltration: Rebalance rewards, add min stat requirement, add market demand (#2210) (@ficocelliguy, @d0sboots, @catloversg)
-
-### UI
-
-- Fix: Hacknet's RAM upgrade button is off-by-one (#2093) (@catloversg)
-- Add visual indicators for tooltips of reputation/favor (#2092) (@catloversg)
-- Mitigate crash in Terminal page in edge cases (#2099) (@catloversg)
-- Update favicon files (#2122) (@catloversg)
-- Add option to enable/disable syncing Steam achievements (#2117) (@catloversg)
-- Change min and step value of "Tail render interval" setting (#2129) (@catloversg)
-- Realign and update text in achievement icons (#2127) (@catloversg)
-- Show achievement lists in grids (#2109) (@catloversg)
-- Fix styling of IPvGO score modal (#2166) (@ficocelliguy)
-- Add RAM usage and file size to "ls -l" output (#2135) (@HansLuft778)
-- Fix: Coding contract UI does not handle error properly when answer format is invalid (#2171) (@catloversg)
-- Improve Recovery Mode screen (#2206) (@catloversg)
-- Add "Recent Errors" tab and improved error modal (#2169) (@ficocelliguy)
-- Validate bet input of casino mini games (#1694) (@catloversg)
-- Scroll to top when opening new NS API doc page in popup mode (#2181) (@catloversg)
-- Fix nbsp rendering as text rather than a space (#2225) (@gmcew)
-- Show terminal warning instead of popup for breaking changes (#2244) (@catloversg)
-- Show money in exponential form instead of "0.000" for dividends when it's > 0 but still too small (#2243) (@catloversg)
-- Place tooltips and popups in front of log windows (#2253) (@catloversg)
-- Fix: Go history page shows favor bonus instead of reputation bonus (#2251) (@catloversg)
-- Fix: Error message shows blob URL instead of script name (#2265) (@catloversg)
-- Add tooltip for reputation/favor in page of faction's augmentation list (#2268) (@catloversg)
-- Fix: Dropdown list appears behind modal when it's used in modal (#2282) (@catloversg)
-- Close coding contract popup on prestige (#2285) (@catloversg)
-- Add "Run Beautify on Save" option for built-in editor (#2287) (@TheCleric)
-- Add configurable option for auto-reconnecting to RFA client (#2297) (@catloversg)
-- Fix: Active Scripts page may mix up UI after prestige (#2303) (@catloversg)
-- Better status bar animations (#2317) (@d0sboots)
-- Prevent text from overflowing when script's args are too long in Active Scripts page (#2324) (@catloversg)
-- Print error detail to terminal if script cannot be compiled or there is runtime error while autocompleting (#2328) (@catloversg)
-- Fix: Color picker appears behind theme editor (#2321) (@catloversg)
-- Allow automatically hiding the top menu in Electron (#2325) (@femboyfireball)
-- Clarify why you can't buy 4S when disabled (#2311) (@femboyfireball)
-- Warn player if they run no-arg programs with arguments (#2338) (@catloversg)
-- Show server's money in exponential form if it's too small (#2343) (@catloversg)
-- Format the shared RAM multipler on the factions (#2355) (@mctylr-gh)
-- Fix: Electron app does not show achievement icons (#2362) (@catloversg)
-- Reword tutorial steps to distinguish between instruction and example (#2370) (@catloversg)
-- Prevent showing redundant error toasts on failed RFA auto connect attempts (#2381) (@shyguy1412)
-- Fix: Company's job list shows wrong starting jobs (#2391) (@catloversg)
-- Improve instructions in recovery mode (#2394) (@catloversg)
-- Add option to set fractional digits (#2419) (@CTNOriginals)
-- Add links to documentation pages in tutorial's last step (#2424) (@catloversg)
-- Show useful error messages when loading unsupported save data from newer versions (#2425) (@catloversg)
-- Change notice for breaking changes in v2 (#2426) (@catloversg)
-- Do not show error popup related to Stanek's Gift data when loading save files from pre-v1.1.0 (#2429) (@catloversg)
-- Add Script Editor toggle for Sticky Scroll (#2431) (@gmcew)
-- Display contract answers on completely failed contracts (#2440) (@TheCleric)
-- Remove BitNode's difficulty in BitNode selection popup (#2454) (@catloversg)
-- Add settings to change currency symbol and whether to show it after money value (#2453) (@kaoticengineering)
-- Fix: Monaco shows wrong TSDoc of APIs that have "export" word (#2473) (@catloversg)
-- Show karma in Stats page and explain what it is in documentation (#2475) (@catloversg)
-- Update icon of DARKNET_BACKDOOR and DARKNET_DEPTHS achievements (#2480) (@catloversg)
-- Update incorrect ps command shown in help (#2484) (@Groot-0909)
-- Improve help text of scp and run (#2493) (@catloversg)
-- Add option to disable minimap in script editor (#2504) (@catloversg)
-- Improve navigation system of in-game documentation viewer (#2499) (@catloversg)
-- Use font family setting when rendering MUI Link component (#2511) (@catloversg)
-- Show hints of BitNode documentation and allow opening it in BitVerse (#2513) (@catloversg)
-- Show errors if using nano/vim with patterns that do not match any files (#2515) (@catloversg)
-- Tweak CSS/Position of Darknet Docs link (#2517) (@d0sboots)
-- Add indicator of RFA connection status to overview panel (#2497) (@catloversg)
-- Fix issues with RFA auto-reconnecting feature (#2535) (@catloversg)
-- Add inline script RAM usage text to each active script (#2546) (@vadien)
-- Update toolbar of in-game editor (#2551) (@catloversg)
-- Show "undefined" instead of -1 as pid in error popup when catching promise errors (#2555) (@catloversg)
-- Add option to autosave scripts on focus change (#2565) (@catloversg)
-- Prevent joining banned factions via UI (#2573) (@catloversg)
-- Fix: Import save comparison popup shows wrong BN level (#2595) (@catloversg)
-- Fix: Cannot type in text boxes rendered by players' scripts when terminal tab is shown (#2615, #2622) (@lstutzman, @catloversg)
-- Navigate to gym/university instead of city when stopping focusing on gym/class work (#2613) (@lstutzman)
-- Ensure prompts shown by ns.prompt do not lose focus in the terminal tab (#2631) (@catloversg)
-- Remove unnecessary max-width of tab list in in-game editor (#2643) (@catloversg)
-- Add hooks to sidebar for players to attach custom content (#2651) (@catloversg)
-- Use exponential notation when formatting very small HP or thread values (#2656) (@catloversg)
-- Show effective amount of shared RAM when using UI (#2691) (@catloversg)
-- Use "success" theme color for low infiltration difficulty instead of "primary" (#2693) (@catloversg)
-- Reload immediately after importing, deleting save data or killing all scripts (#2697) (@catloversg)
-- Activate recovery mode on critical BN prestige bugs (#2699) (@catloversg)
+- Change getServer return type; rename getServerAuthDetails and add missing dnet properties (#2746) (@ficocelliguy)
### MISC
-- Ensure IPvGO board has at least 1 offline node (#2072) (@ficocelliguy)
-- Fix: Game crashes when generating CCT in weird case (#2077) (@catloversg)
-- Add ns.dynamicImport() to dynamically import a script (#2036) (@shyguy1412)
-- Add functionality and support to fully allow Players to use IP addresses in place of hostnames (#1990) (@NagaOuroboros)
-- Fix: IPvGO tutorial was getting stuck if you left the tab and returned (#2071) (@ficocelliguy)
-- Fix: Passive faction reputation gain applies Player.mults.faction_rep twice (#2125) (@catloversg)
-- Fix: Electron app does not run on Linux due to incompatible glibc version and wrong usage of net.fetch (#2114) (@catloversg)
-- Change how enums are exposed in NetscriptDefinitions.d.ts (#1998) (@catloversg)
-- Add formulas API for calculating share power and move UI of sharing RAM (#2126) (@catloversg)
-- Use FactionName enum in relevant APIs (#2101) (@catloversg)
-- Prevent running multiple instances of Electron app (#2095) (@catloversg)
-- Tweak "The Covenant" faction's rumor condition (#2110) (@catloversg)
-- Export crash report when a fatal error occurs (#2106) (@catloversg)
-- Correctly end game & winstreak if a cheat attempt critically fails (#2130) (@ficocelliguy)
-- Show custom error message when player imports decompressed save file (#2108) (@catloversg)
-- Add "Total Number of Primes" contract (#2116) (@gmcew)
-- Make IP addresses use the full 32 bit space (#2113) (@whiskeyfur)
-- Add "--tail" to default autocomplete options (#2103) (@catloversg)
-- Update blood donation (#2151, #2216, #2508) (@catloversg, @hydroflame, @d0sboots)
-- Fix typo in sector-12-crime.lit (#2159) (@Boingostarr)
-- Add versionNumber to ns.ui.getGameInfo() (#2155) (@catloversg)
-- Fix typos in literature files (#2164) (@catloversg)
-- Add more enums to ns.enums (#2165) (@catloversg)
-- Add removal of fuzzy matching to list of breaking changes (#2149) (@catloversg)
-- Fix: API break detector does not detect affected code in some cases (#2172) (@catloversg)
-- Mention bug-report channel on Discord for reporting bugs (#2201) (@catloversg)
-- Add file metadata (timestamps) (#1271, #2199) (@Hoekstraa, @catloversg)
-- Allow using E notation in expr CLI (#2209) (@catloversg)
-- Fix: Current work is not shown in edge cases (#2208) (@catloversg)
-- Make ns.codingcontract.createDummyContract throw error if type is invalid (#2188) (@catloversg)
-- Fix: Documentation navigator does not handle external URL properly (#2202) (@catloversg)
-- Detect circular dependencies when generating modules (#2194) (@catloversg)
-- Fix: Running scripts may be loaded before main UI (#1726) (@catloversg)
-- Fix: Autocomplete of "connect" command does not list purchased servers (#2229) (@gmcew)
-- Expose difficulty value of coding contract in NS API (#2230) (@gmcew)
-- Fix: Exporting game via menu of Steam app does not give export bonus (#2241) (@catloversg)
-- Update migration instruction for breaking change of ns.nFormat (#2247) (@catloversg)
-- Fix: Loading code discards entire Go data due to missing migration code for favor/rep (#2252) (@catloversg)
-- Add removal of API server to list of breaking changes (#2205) (@catloversg)
-- Fix typo in KARMA_1000000 achievement (#2264) (@UncleCeiling)
-- Clarify the reason of failure when trying to move a running script (#2160) (@UncleCeiling)
-- Fix: ns.mv writes to destination file even if it cannot delete source file (#2267) (@catloversg)
-- Update messages related to text files (#2266) (@catloversg)
-- Fix calculateExp so that it won't return a too small result (#2274) (@d0sboots)
-- Change ns.alert to accept multiple args as other print functions (#2278) (@vamo89)
-- Fix: Coding contract can be solved manually via UI after it is removed on prestige (#2281) (@TheCleric)
-- Ensure ns.go.analysis.getValidMoves correctly handles playing as white (#2292) (@ficocelliguy)
-- Ensure that player's promises are changed to "gameOver" once the game is over (#2198) (@ficocelliguy)
-- Export save data before migrating to v3 (#2304) (@catloversg)
-- Prevent tiny islands surrounded by offline nodes during initial board generation (#2310) (@ficocelliguy)
-- Add FragmentType to NS Enums (#2341) (@TheCleric)
-- Do not round down amount of hacked money in "hack" CLI (#2344, #2345) (@catloversg)
-- Rename "TIX" interface to "Stock" (#2351) (@catloversg)
-- Add --temporary flag to run command (#2354) (@catloversg)
-- Let ServerProfiler.exe autocomplete servers (#2356) (@rladenson)
-- Correct phrasing in SmartSonar description (#2368) (@The-Chaddeus)
-- Make ActiveFragment extend Fragment (#2373) (@catloversg)
-- Expose theme as css custom props (#2380) (@shyguy1412)
-- Support css file type (#2378) (@shyguy1412)
-- Do not update captures on passed analysis boards (#2415) (@ficocelliguy)
-- Update error message when backdooring without admin rights (#2435) (@CicaProductions)
-- Add literature expanding the augment prestige lore (#2433) (@Nick-Colclasure)
-- Improve script args validation message (#2451) (@ficocelliguy)
-- Add sleeve commands to purchase sleeves and memory via the API (#2443) (@TheAimMan)
-- Allow ns.read to read .msg and .lit files (#2455) (@catloversg)
-- Fix scoring of very large open areas in IPvGO (#2464) (@ficocelliguy)
-- Make threads' bonus apply to timing attack puzzle's extra time per char (#2466) (@ficocelliguy)
-- Improve documentation; remove requirement that getBlockedRam and getDepth be called on a darknet server (#2472) (@ficocelliguy)
-- Prevent blocked ram on the "darkweb" server (#2468) (@ficocelliguy)
-- Add extra hint to sorted echo puzzle at high levels (#2465) (@ficocelliguy)
-- Change coding contract's fallback reward priorities (#2481) (@gmcew)
-- Add JS object properties as server names; refactor save/load/server storage to support this (#2482) (@ficocelliguy)
-- Buff packet sniffing slightly (#2485) (@d0sboots)
-- Fix: Darknet state is not reset properly on prestige (#2486) (@catloversg)
-- Change how coding contract rewards are randomized (#2490) (@catloversg)
-- Expose ProgramName enum (#2492) (@catloversg)
-- More fixes and feedback in darknet (#2489) (@ficocelliguy)
-- Fix missed cases in offline server handling (#2495) (@d0sboots)
-- Adjust darknet balance from player feedback (#2512) (@ficocelliguy)
-- Add "Find Largest Rectangle in a Matrix" coding contract (#2519) (@Misha279-UA)
-- Tweak Dnet based on player feedback (#2533, #2545, #2593) (@ficocelliguy)
-- Allow parsing unknown options with data.flags in autocomplete (#2539) (@catloversg)
-- Fix webstorm by using a mutationLock (#2542) (@d0sboots)
-- Print error message when calling ns.ui.closeTail with nonexistent pid or pid of stopped scripts (#2557) (@catloversg)
-- Add minimum width/height constraints to ns.ui.resizeTail (#2558) (@catloversg)
-- Add API to minimize and expand tail windows (#2556) (@catloversg)
-- Improve error messages for invalid sleeve numbers (#2567) (@catloversg)
-- Improve help text of expr command (#2561) (@catloversg)
-- Rework faction rumor (#2569) (@catloversg)
-- Fix: hacknetNodeCost formula API throws when using documented optional parameter (#2577) (@catloversg)
-- Rework intelligence override (#2575) (@catloversg)
-- Electron: Allow opening dev tools via CLI arguments (#2589) (@catloversg)
-- Support importing Steam Cloud save file manually (#2583) (@catloversg)
-- Electron: Add UI menus and CLI flags to change log levels (#2596) (@catloversg)
-- Import correct cloud file when multiple exist (#2599) (@catloversg)
-- Dnet: Remove packet capture (#2594) (@ficocelliguy)
-- Generate more frequent and lower-reward coding contracts (#2603) (@ficocelliguy)
-- Electron: Fix issues in edge cases of using --export-save (#2590) (@catloversg)
-- Fix recursive alias detection causing infinite recursion (#2610) (@lstutzman)
-- Add "hidden" mkdir command (#2646) (@catloversg)
-- Dnet: Remove bonus time effect on authentication and heartbleed speed; fix ram rounding (#2627) (@ficocelliguy)
-- Fix tab completion for multi-word quoted autocomplete options (#2612) (@lstutzman)
-- Add weakenEffect to formulas.hacking namespace (#2626) (@lstutzman)
-- Update description of "cat" in "help" command (#2654) (@catloversg)
-- Reduce achievements check interval (#2650) (@catloversg)
-- Fix: calculateExp throws errors in edge cases (#2667) (@catloversg)
-- Clear recent scripts when installing augmentations (#2670) (@Mathekatze)
-- Better error message for port serialization failure (#2688) (@d0sboots)
-- Adjust charisma augmentation power (#2698) (@ficocelliguy)
-- Clarify what "backdoor" does in "help" command (#2694) (@abbyintheattic)
-- Fix: Hacknet upgrade cost formulas ignore upgrade level when enforcing max level (#2696) (@lucebac)
-- Improve error messages for invalid module parsing (#2707) (@catloversg)
+- Cache reward fixes (#2731) (@ficocelliguy)
+- Fix typo in darknet authentication response message (#2734) (@catloversg)
+- Fix: Tutorial links to outdated faq url (#2733) (@catloversg)
+- Fix: Player can switch tabs without losing focus on current work (#2724) (@catloversg)
+- Add new command to upload a directory (#2659) (@hexagonrecursion)
+- Add HJKL key mappings for infiltration arrows (#2742) (@mahlquistj)
+- Prevent generating malformed darknet server hostname (#2744) (@catloversg)
+- Support angle bracket type assertions in RAM calculation (#2751) (@catloversg)
+- Fix typo in augmentation description (#2760) (@gmcew)
+- Reduce the RAM cost of ns.rm() to match ns.scp() (#2761) (@NagaOuroboros)
+- Temporarily remove darknet servers with unusual hostnames (#2757) (@catloversg)
+- Fix: Duplicate .lit and .cache files can be generated in dnet (#2763) (@catloversg)
+- Allow getFunctionRamCost to get base RAM cost for scripts (#2771) (@Mathekatze)
### DOCUMENTATION
-- Fix wrong links in some docs pages (#2082) (@mizmantle)
-- Add getContractTypes to Coding Contracts documentation page (#2089) (@gmcew)
-- Clarify applicability of wiki entry in Hamming contracts (#2087) (@gmcew)
-- Make minor improvements in "Getting Started" page (#2112) (@catloversg)
-- Remove mention of Netscript 1.0/2.0/JS in NetscriptDefinitions.d.ts (#2150) (@catloversg)
-- Clarify ns.go.getGameState (#2158) (@Arjan-akkermans)
-- Remove unnecessary br tag in scripts.md (#2204) (@catloversg)
-- Fix minor problem in old changelog (#2203) (@catloversg)
-- Remove mention of NS2 in NetscriptDefinitions.d.ts (#2200) (@catloversg)
-- Move CONTRIBUTING.md (#2191) (@catloversg)
-- Specify parameter types of of ns.go.analysis.highlightPoint and ns.go.analysis.clearPointHighlight (#2175) (@kevinsandow)
-- Fix warnings when generating NS API docs (#2189) (@kevinsandow)
-- Support showing images in markdown docs (#2207) (@catloversg)
-- Update links to CONTRIBUTING.md (#2213) (@catloversg)
-- Update contribution guide (#2214) (@emmanuel-ferdman)
-- Clarify condition of joining Daedalus faction (#2234) (@catloversg)
-- Update infiltration docs (#2259) (@catloversg)
-- Mention TypeScript support and update example in React docs (#2263) (@catloversg)
-- Update Remote API docs (#2258) (@catloversg)
-- Change ns.tail to ns.ui.openTail in example code (#2270) (@gmcew)
-- Clarify description of ArrayJumpingGame contract (#2277) (@acidduk)
-- Update 'Hacking Algorithms' page (#2288) (@gmcew)
-- Reword HammingCode contracts and mention dummy contract API (#2296) (@gmcew)
-- Clarify cross-host characteristic of PID and port (#2336) (@Thaccus)
-- Fix wrong commands in getting_started.md (#2349) (@catloversg)
-- Use alternative fix for newline issue in IPvGO docs (#2350) (@catloversg)
-- Fix missing/wrong TSDoc of APIs having optional host parameter (#2371) (@catloversg)
-- Fix typo in README.md (#2375) (@HolyNaet)
-- Improve wording around host argument of getScriptRam (#2374) (@nickmshelley)
-- Remove references to defunct wiki from Hack/Weaken/Grow (#2404) (@maglinvinn)
-- Clarify behavior of NS.getScriptLogs() for running scripts (#2408) (@sg673)
-- Add instructions to troubleshoot common issues for contributors to CONTRIBUTING.md (#2420) (@catloversg)
-- Add FAQ and another JIT batcher illustration to documentation (#2400) (@HolyNaet)
-- Change wording in Singularity.installAugmentation (#2439) (@HolyNaet)
-- Update README.md to correct Frequently Asked Questions link. (#2444) (@jonathonchase)
-- Add a question and a grammatical fix in faq.md (#2446) (@HolyNaet)
-- Move current changelog to changelog-v2.md (#2461) (@catloversg)
-- Fix some IPvGO docs that do not reflect winstreak rep converted to favor (#2463) (@ficocelliguy)
-- Add glossary of darknet terms to the documentation page (#2469) (@ficocelliguy)
-- Add a note to CONTRIBUTING.md about the npm peer dep issue (#2474) (@d0sboots)
-- Update darknet documentation (#2483) (@ficocelliguy)
-- Fix invalid links in ns.sleep and ns.asleep (#2496) (@catloversg)
-- Use relative links instead of absolute links (#2521) (@catloversg)
-- Document quirky behavior of ns.flags when default value is nullish (#2528) (@catloversg)
-- Clarify how share power affects reputation gain rate of non-hacking work (#2544) (@catloversg)
-- Update guides (#2550) (@catloversg)
-- Add missing newline after RAM cost (#2570) (@catloversg)
-- Update mention of outdated getStockForecast API (#2578) (@catloversg)
-- Fix newline issues in IPvGO docs and add missing RAM cost (#2602) (@catloversg)
-- Document coding contract's generation and rewards (#2624) (@catloversg)
-- Clarify scp and exec darknet permissions in API docs (#2634) (@lstutzman)
-- Update RAM cost of hacknet APIs and remove unnecessary RAM cost docs (#2639) (@catloversg)
-- Update tutorial script for buying cloud servers (#2653) (@catloversg)
-- Improve coding contract documentation (#2689) (@catloversg)
+- Remove TS type annotation from doc example script (#2721) (@ficocelliguy)
+- Update list of RFA community tools (#2722) (@CTNOriginals)
+- Fix incorrect cloud API example (#2738) (@catloversg)
+- Remove non-existent influence namespace in Darknet documentation (#2748) (@Berdes)
+- Remove spoiler for Offline scripts and bonus time page and make it accessible early-game (#2749) (@Berdes)
+- Clarify ns.scp and ns.isRunning (#2769) (@catloversg)
### SPOILER CHANGES - UI
-- Fix: BitVerse does not show all BN multipliers in some cases (#2045) (@catloversg)
-- Add UI hint about scripting tea/party and using Intern (#2179) (@catloversg)
-- Fix: Bladeburner console prints main body's HP instead of sleeve's HP (#2390) (@catloversg)
-- Reduce threshold of showing warning of low population (#2450) (@catloversg)
-- Fix: Hacknet server UI shows NaN hash rate when 100% RAM is being used (#2500) (@catloversg)
-- Prevent ending BNs through reuse of Bladeburner UI event handler (#2574) (@catloversg)
-- Always show Black Operations list (#2592) (@catloversg)
-- Show hints of Sleeves mechanic in pre-endgame (#2605) (@catloversg)
-- Consistently calculate BitNode "level" (#2645) (@catloversg)
-- Add tooltips explaining why Bladeburner skill upgrades are disabled (#2648) (@catloversg)
-- Add button to open Faction page from Gang UI (#2655) (@catloversg)
-- Ensure intelligence override is a positive integer (#2673) (@catloversg)
-- Remove max width of corporation division list (#2686) (@catloversg)
+- Show hints of Gang mechanic in pre-endgame (#2723) (@catloversg)
+- Break out Darknet BN and player mults as separate entries (#2745) (@gmcew)
### SPOILER CHANGES - MISC
-- Add achievement for acquiring SF13.1 (#2107) (@catloversg)
-- Add achievement for completing all BNs (#2128) (@catloversg)
-- Renamed Division.type to Division.industry (#2079, #2152) (@whiskeyfur, @catloversg)
-- Make ns.hacknet.spendHashes handle invalid targets in same way as UI (#2102) (@catloversg)
-- Add achievements for BN14 (#2140) (@catloversg)
-- Print logs when ns.hacknet.spendHashes fails and update param type of APIs using hash upgrade (#2145) (@catloversg)
-- Add ns.singularity.getUnlockedAchievements (#2156) (@UncleCeiling)
-- Remove mention of unusable research "sudo.Assist" (#2187) (@catloversg)
-- Change message of Singularity error and uncaught promise error (#2174) (@catloversg)
-- Add ns.singularity.getHackingLevelRequirementOfProgram (#2271) (@catloversg)
-- Expose gang's discount rate (#2272) (@catloversg)
-- Prevent purchasing product-only research for material industries (#2283) (@catloversg)
-- Fix: Stat levels are not recalculated after grafting augs or accepting Stanek's Gift (#2322) (@catloversg)
-- Expose production limit of material and product (#2330) (@AnteIndustrial)
-- Make some editorial changes in Black Operations' description (#2449) (@catloversg)
-- Add warning when installing backdoor on backdoored server with Singularity API (#2458) (@catloversg)
-- Fix: Sleeves can earn exp and purchase augmentations when enabling disableSleeveExpAndAugmentation (#2467) (@catloversg)
-- Add improved challenge achievement for BN15 (#2479) (@ficocelliguy)
-- Stop randomizing Bladeburner's action difficulty (#2491) (@catloversg)
-- Adjusted Bladeburner's team bonus computation to make one member help (#2541) (@JoshuaCF)
-- Rebalance charisma exp gain of Recruitment action (#2549) (@catloversg)
-- Add APIs to get rank gain and rank loss of an action (#2572) (@catloversg)
-- Reduce RAM cost of inGang and inBladeburner APIs (#2582) (@catloversg)
-- Fix skillMaxUpgradeCount returning 1 at extreme skill levels (#2611) (@lstutzman)
-- API: Expose charged effects of Stanek's Gift active fragments (#2638) (@catloversg)
-- Apply SF override to charisma calculations (#2642) (@catloversg)
-- Update description of "BN9: Challenge" achievement (#2647) (@catloversg)
-- Fix: Intelligence data is incorrectly migrated when Intelligence is not unlocked (#2660, #2666) (@catloversg)
-- Restrict team count of Ops/BlackOps to total team size (#2672) (@catloversg)
-- Fix: DarkscapeNavigator program is not granted on BN prestige when having SF15 (#2690) (@catloversg)
-- Prevent duplicate processing of boost materials (#2695) (@catloversg)
-- Update error message of ns.singularity.joinFaction (#2700) (@catloversg)
-- Update RAM cost of nextUpdate and similar APIs (#2702) (@catloversg)
-- Update description of "You and what army?" achievement (#2703) (@catloversg)
-- Rework material production limit (#2683) (@catloversg)
-- Remove references to "sudo.Assist" research (#2709) (@catloversg)
-
-### SPOILER CHANGES - DOCUMENTATION
-
-- Update rewards of SF14 (#2142) (@catloversg)
-- Fix missing and outdated info of optional parameter (#2176) (@catloversg)
-- Specify param type of many Corporation APIs (#2190) (@catloversg)
-- Fix typo in bitnodes.md (#2221) (@catloversg)
-- Add short guide of BitNode recommendation (#2041) (@1337JiveTurkey)
-- Add comprehensive guide of BitNode recommendation (#2044) (@catloversg)
-- Fix outdated description of Bladeburner interface (#2226) (@catloversg)
-- Clarify advice about Smart Supply in round 1 (#2233) (@catloversg)
-- Fix wrong param/return type and clarify ns.sleep, ns.asleep, ns.singularity.exportGame (#2242) (@catloversg)
-- Clarify ns.gang.getOtherGangInformation (#2289) (@acidduk)
-- Update type of Player.factions, GangGenInfo.faction and CorpMaterialConstantData.name (#2347) (@catloversg)
-- Clarification of Singularity costs inside BN4 (#2403) (@gmcew)
-- Update math notations in corporation docs (#2452) (@catloversg)
-- Update BitNode recommendation short guide (#2523, #2529) (@catloversg)
+- Cancel faction work instead of finishing it when creating gang (#2726) (@catloversg)
+- Lower darknet BN money mults on hard nodes (#2743) (@ficocelliguy)
+- Change getDarkwebPrograms return type to ProgramName[] (#2754) (@catloversg)
+- Fix: getAugmentationBasePrice ignores bitnode mult for SoA augs (#2756) (@SAY-5)
+- Rebalance hash base cost of generating coding contract (#2759) (@gmcew)
+- Ensure induceServerMigration moves servers randomly (#2767) (@catloversg)
+- Do not decrease player reputation when failing bladeburner actions (#2768) (@catloversg)
### CODEBASE/REFACTOR/WORKFLOW/JEST/TOOL/DEPS
-- Electron: Use steamworks.js to integrate with Steamworks SDK (#1563) (@catloversg)
-- Electron: Replace deprecated protocol.interceptFileProtocol with protocol.handle (#2100) (@catloversg)
-- Dev menu: Add tools to set stat level and queue augmentations of faction (#2118) (@catloversg)
-- Remove wrong and irrelevant comments/code (#2111) (@catloversg)
-- Refactor: Change repNeededToDonate to favorNeededToDonate (#2134) (@d0sboots)
-- Workflow: Build artifacts and upload to release (#2120) (@catloversg)
-- Configure .editorconfig to not trim trailing whitespace in NetscriptDefinitions.d.ts (#2137) (@G4mingJon4s)
-- Fix typo of "CorruptableText" (#2144) (@catloversg)
-- Dev menu: Fix bugs in WD tools (#2141) (@catloversg)
-- Make prettier ignore src/Documentation/pages.ts (#2185) (@catloversg)
-- Workflow: Allow enabling dev mode when deploying dev build (#2184) (@catloversg)
-- Refactor: Refactor message handler of Remote API (#2211) (@catloversg)
-- Dev menu: Auto expand last opened tools (#2182) (@catloversg)
-- Update build script to avoid unnecessary newline in generated script (#2224) (@catloversg)
-- Fix inconsistent generated pages.ts (#2236) (@catloversg)
-- Dev menu: Reset hacknet server list properly when setting level of SF9 (#2177) (@catloversg)
-- Update dev dependencies (#2246) (@catloversg)
-- Move all docs into en/ subdirectory (#1505) (@d0sboots)
-- Fix ctrl-clicking after the doc_en refactor (#2256) (@d0sboots)
-- Electron: Specify mime type when loading wasm files (#2262) (@catloversg)
-- Electron: Build universal macOS binary (#2306) (@Snarling)
-- Add Jest test to test migration from v2 to v3 (#2305) (@catloversg)
-- Workflow: Split build-artifacts.yml into 2 workflows (#2309) (@catloversg)
-- Workflow: Update NodeJs version in workflows (#2319) (@catloversg)
-- Update NodeJS to v22 (#2318) (@mctylr-gh)
-- Update api-documentor and api-extractor (#2320) (@mctylr-gh)
-- Improve dev menu for augmentations (#2315) (@d0sboots)
-- Add more debug info for troubleshooting corporation issues (#2327) (@catloversg)
-- Update doc.sh (#2331) (@catloversg)
-- Enable restoreMocks option and fix lint errors (#2333) (@catloversg)
-- Workflow: Add new job to check generated docs (#2329) (@catloversg)
-- Quiet Jest tests output on expected failures (#2332) (@mctylr-gh)
-- Standardize error handling in netscriptGoImplementation.ts (#2335) (@ficocelliguy)
-- Enable linting in test folder (#2337) (@catloversg)
-- Update minor version of material UI and Emotion packages (#2339) (@mctylr-gh)
-- Refactor Stanek's Gift UI code and change internal FragmentType enum (#2346) (@catloversg)
-- Minor React related packages update (#2353) (@mctylr-gh)
-- Remove redundant check in getServer utility function and serverExists API (#2357) (@catloversg)
-- Refactor tests that add home server manually (#2358) (@catloversg)
-- Update Electron version (#2360) (@catloversg)
-- Prepare for save data migration of next beta version (#2369) (@catloversg)
-- Rewrite infiltration to pull state out of React (#2316, #2393) (@d0sboots)
-- Use getCoreBonus function when calculating server growth (#2387) (@aaaa-imcute)
-- Refactor implementation of ports (#2396) (@d0sboots)
-- Avoid re-entrancy issues with EventEmitter (#2397) (@d0sboots)
-- Update packages-lock.json to not use vulnerable versions (#2405) (@mctylr-gh)
-- Allow specifying migrator when migrating player's scripts (#2418) (@catloversg)
-- Refactor code related to in-game documentation link (#2422) (@catloversg)
-- Clean up "doc" folder (#2427) (@catloversg)
-- Update dependencies related to monaco (#2432) (@catloversg)
-- Fix typos and duplicating ms per cycle constant (#2436) (@gmcew)
-- Update dependencies to fix vulnerability warnings (#2445) (@catloversg)
-- Move Result to the public API (#2398) (@d0sboots)
-- Generate display data for math notation at built time and remove runtime mathjax dependency (#2447) (@catloversg)
-- Update SaveData type to be compatible with TS 5.9 and upgrade TS (#2457) (@catloversg)
-- Update NodeJS to v24 (#2456) (@catloversg)
-- Update format created by "build artifacts" workflows (#2470) (@Snarling)
-- Consolidate checks under getFailureResult() (#2476) (@d0sboots)
-- Fix: Global states are not reset properly between each Jest test (#2487) (@catloversg)
-- Change getFailureResult to checkDarknetServer (#2494) (@d0sboots)
-- Speed up by-ip lookups by introducing a new map entry (#2488) (@d0sboots)
-- Refactor/adjust getPixelPosition in darknet UI code (#2501) (@d0sboots)
-- Add tests for checking getAnswer and solver of coding contracts (#2503) (@catloversg)
-- Refactor ImportSave component (#2505) (@catloversg)
-- Show coding contract names when their tests failed (#2520) (@catloversg)
-- Workflow: Fix wrong instruction of generating docs (#2522) (@catloversg)
-- Update comment of LoadingScreen of ComplexPage enum (#2527) (@catloversg)
-- Make getPlayer 10x faster (#2548) (@d0sboots)
-- Create monaco editor instance with null model (#2563) (@catloversg)
-- Update dependencies (#2576) (@catloversg)
-- Remove barrel imports in Bladeburner code (#2580) (@catloversg)
-- Fix React warning in IPvGO scoring explanation popup (#2581) (@catloversg)
-- Update Babel core, presets and module loader for webpack (#2585) (@catloversg)
-- Add script to generate webpack bundle report (#2587) (@catloversg)
-- Update Electron (#2591) (@catloversg)
-- Mitigate issue of forcefullyCrashRenderer (#2597) (@catloversg)
-- Split Settings.ts to reduce number of imports (#2600) (@catloversg)
-- Remove duplicate random alphanumeric string functions (#2601) (@catloversg)
-- Update comments to reflect changes in #2603 (#2606) (@catloversg)
-- Replace ipExists() linear scan with O(1) Map.has() (#2621) (@lstutzman)
-- Refactor and fix issues in db.ts (#2623) (@catloversg)
-- Add dependency array to TerminalInput keydown useEffect (#2620) (@lstutzman)
-- Add dependency array to GameRoot useEffect (#2617) (@lstutzman)
-- Dev menu: Initialize dark net data when setting SF15 level (#2632) (@catloversg)
-- Use type-only imports in ArrayHelpers.ts (#2630) (@catloversg)
-- Remove redundant "$" from JS/TS regex in webpack config (#2649) (@catloversg)
-- Allow specifying commit hash id when building artifacts (#2652) (@catloversg)
-- Fix passive event listener warning (#2671) (@catloversg)
-- Ignore .DS_Store files when generating pages.ts (#2685) (@catloversg)
-- Consistently check when to show intelligence skill (#2692) (@catloversg)
-- Fix api-extractor warnings (#2701) (@catloversg)
-- Add comments explaining redundant check in product calculation (#2705) (@catloversg)
+- Update action versions (#2718) (@Snarling)
+- Remove duplicate getStockFromSymbol function (#2725) (@catloversg)
+- Update game version (#2732) (@catloversg)
+- Harden saving to avoid save data corruption (#2755) (@catloversg)
`,
} as const;
diff --git a/src/DarkNet/Enums.ts b/src/DarkNet/Enums.ts
index 66835e665..ea766bf8b 100644
--- a/src/DarkNet/Enums.ts
+++ b/src/DarkNet/Enums.ts
@@ -1,4 +1,5 @@
-import type { _ValueOf, DarknetServerData } from "@nsdefs";
+import type { _ValueOf, DarknetServerDetails } from "@nsdefs";
+import type { DarknetServerData } from "./utils/darknetServerUtils";
export const HORIZONTAL_CONNECTION_CHANCE = 0.5;
export const VERTICAL_CONNECTION_CHANCE = 0.3;
@@ -67,7 +68,7 @@ export const ResponseCodeEnum = {
ServiceUnavailable: 503,
} as const;
-export const exampleDarknetServerData: DarknetServerData = {
+export const exampleDarknetServerData = {
hostname: "",
ip: "",
hasAdminRights: false,
@@ -87,4 +88,20 @@ export const exampleDarknetServerData: DarknetServerData = {
logTrafficInterval: -1,
isStationary: false,
purchasedByPlayer: false,
-} as const;
+} as const satisfies DarknetServerData;
+
+export const exampleDarknetServerDetails = {
+ isConnectedToCurrentServer: false,
+ hasSession: false,
+ modelId: exampleDarknetServerData.modelId,
+ passwordHint: exampleDarknetServerData.staticPasswordHint,
+ data: exampleDarknetServerData.passwordHintData,
+ logTrafficInterval: exampleDarknetServerData.logTrafficInterval,
+ passwordLength: -1,
+ passwordFormat: "numeric",
+ blockedRam: exampleDarknetServerData.blockedRam,
+ difficulty: exampleDarknetServerData.difficulty,
+ requiredCharismaSkill: exampleDarknetServerData.requiredCharismaSkill,
+ depth: exampleDarknetServerData.depth,
+ isStationary: exampleDarknetServerData.isStationary,
+} as const satisfies DarknetServerDetails;
diff --git a/src/DarkNet/effects/cacheFiles.ts b/src/DarkNet/effects/cacheFiles.ts
index 21ee04990..f067b74c6 100644
--- a/src/DarkNet/effects/cacheFiles.ts
+++ b/src/DarkNet/effects/cacheFiles.ts
@@ -12,6 +12,7 @@ import type { DarknetServer } from "../../Server/DarknetServer";
import { resolveCacheFilePath } from "../../Paths/CacheFilePath";
import type { CacheResult } from "@nsdefs";
import { addClue, cctCooldownReached } from "./effects";
+import { getBitNodeMultipliers } from "../../BitNode/BitNode";
export const generateCacheFilename = (isPhishingCache: boolean, prefix?: string) => {
const filenamePrefix = prefix ?? cachePrefixes[Math.floor(Math.random() * cachePrefixes.length)];
@@ -24,6 +25,9 @@ export const addCacheToServer = (server: DarknetServer, isPhishingCache: boolean
if (!cacheFilename) {
return { success: false, message: `Cannot generate path. prefix: ${prefix}` };
}
+ if (server.caches.includes(cacheFilename)) {
+ return { success: false, message: `Duplicate cache file: ${cacheFilename}` };
+ }
server.caches.push(cacheFilename);
return { success: true, cacheFilename };
};
@@ -41,11 +45,15 @@ export const getRewardFromCache = (server: DarknetServer, cacheName: string, sup
};
}
- const rewards = [getMoneyReward, getProgramAndStockMarketRelatedRewards, getStockReward, getDataFileReward];
+ const rewards = [getProgramAndStockMarketRelatedRewards, getStockReward, getDataFileReward];
if (cacheName.endsWith(".d.cache")) {
// only include ccts from caches generated from phishing attacks
rewards.push(getCCTReward);
}
+ if (getBitNodeMultipliers(Player.bitNodeN, 1).DarknetMoneyMultiplier) {
+ // only include money reward if it is not disabled by the bn mults
+ rewards.push(getMoneyReward);
+ }
const reward = rewards[Math.floor(Math.random() * rewards.length)];
const result = reward(difficulty, server);
@@ -80,7 +88,7 @@ export const getCCTReward = (difficulty: number, server: DarknetServer): string
};
export const getMoneyReward = (difficulty: number): string => {
- const sf15_3Factor = Player.activeSourceFileLvl(15) > 3 ? 1.5 : 1;
+ const sf15_3Factor = Player.activeSourceFileLvl(15) >= 3 ? 1.5 : 1;
const reward =
1.2 ** difficulty *
1e7 *
@@ -98,10 +106,14 @@ export const getStockReward = (difficulty: number): string => {
initStockMarket();
}
const stockSymbols = Object.keys(StockSymbol);
- const randomStock = stockSymbols[Math.floor(Math.random() * stockSymbols.length)];
- const shares = Math.floor(1 + difficulty * 5 + Math.random() * 10);
- StockMarket[randomStock].playerShares += shares;
- return `You have discovered a stock option cache containing ${shares} shares of ${randomStock}!`;
+ const stock = StockMarket[stockSymbols[Math.floor(Math.random() * stockSymbols.length)]];
+ const maxNewShares = stock.maxShares - stock.playerShares - stock.playerShortShares;
+ if (maxNewShares <= 0) {
+ return getMoneyReward(difficulty);
+ }
+ const shares = Math.min(Math.floor(1 + difficulty * 5 + Math.random() * 10), maxNewShares);
+ stock.playerShares += shares;
+ return `You have discovered a stock option cache containing ${shares} shares of ${stock.symbol}!`;
};
export const getDataFileReward = (difficulty: number, server: DarknetServer): string => {
diff --git a/src/DarkNet/effects/effects.ts b/src/DarkNet/effects/effects.ts
index b217f1f55..71fbcb727 100644
--- a/src/DarkNet/effects/effects.ts
+++ b/src/DarkNet/effects/effects.ts
@@ -1,5 +1,5 @@
import { Player } from "@player";
-import type { DarknetServerData, Person as IPerson } from "@nsdefs";
+import type { Person as IPerson, DarknetServerDetails } from "@nsdefs";
import { AugmentationName, CompletedProgramName, LiteratureName } from "@enums";
import {
commonPasswordDictionary,
@@ -16,17 +16,16 @@ import { DarknetServer } from "../../Server/DarknetServer";
import { GenericResponseMessage, ModelIds, NET_WIDTH, ResponseCodeEnum } from "../Enums";
import { addCacheToServer } from "./cacheFiles";
import { populateDarknet } from "../controllers/NetworkGenerator";
-import { getDarknetServer } from "../utils/darknetServerUtils";
+import { type DarknetServerData, getDarknetServer } from "../utils/darknetServerUtils";
import {
getAllMovableDarknetServers,
getBackdooredDarknetServers,
getNearbyNonEmptyPasswordServer,
getStasisLinkServers,
} from "../utils/darknetNetworkUtils";
-import { getSharedChars, getTwoCharsInPassword } from "../utils/darknetAuthUtils";
+import { getTwoCharsInPassword } from "../utils/darknetAuthUtils";
import { getTorRouter } from "../../Server/ServerHelpers";
import { DarknetConstants } from "../Constants";
-import { GetServer } from "../../Server/AllServers";
import { isLabyrinthServer } from "./labyrinth";
import { NetscriptContext } from "../../Netscript/APIWrapper";
import { helpers } from "../../Netscript/NetscriptHelpers";
@@ -54,15 +53,15 @@ export const handleFailedAuth = (server: DarknetServer, threads: number) => {
* Returns the time it takes to authenticate on a server in milliseconds
* @param darknetServerData - the target server to attempt a password on
* @param person - the player's character
- * @param attemptedPassword - the password being attempted
+ * @param correctCharsInPassword - the number of correct characters in the password. Only used for TimingAttack servers, where it adds some small auth time delay per char
* @param threads - the number of threads used for the password attempt (which speeds up the process)
* @param linear - if true, the time scaling is linear with the number of threads instead of having diminishing returns
*/
export const calculateAuthenticationTime = (
- darknetServerData: DarknetServerData,
+ darknetServerData: DarknetServerData | DarknetServerDetails,
person: IPerson = Player,
threads = 1,
- attemptedPassword = "",
+ correctCharsInPassword = 0,
linear = false,
) => {
const chaRequired = darknetServerData.requiredCharismaSkill;
@@ -83,13 +82,8 @@ export const calculateAuthenticationTime = (
const time =
baseTime * skillFactor * backdoorFactor * underleveledFactor * hasBootsFactor * hasSf15_2Factor * threadsFactor;
- // We need to call GetServer and check if it's a dnet server later because this function can be called by formulas
- // APIs (darknetServerData.hostname may be an invalid hostname).
- const server = GetServer(darknetServerData.hostname);
- const password = server instanceof DarknetServer ? server.password : "";
// Add extra time for timing attack server, per correct character
- const sharedChars =
- darknetServerData.modelId === ModelIds.TimingAttack ? getSharedChars(password, attemptedPassword) : 0;
+ const sharedChars = darknetServerData.modelId === ModelIds.TimingAttack ? correctCharsInPassword : 0;
const sharedCharsExtraTime = sharedChars * 50 * threadsFactor;
return time * calculateIntelligenceBonus(person.skills.intelligence, 0.25) + sharedCharsExtraTime;
@@ -131,7 +125,7 @@ export const addClue = (server: DarknetServer) => {
// Basic mechanics hints
if ((Math.random() < 0.7 && server.difficulty <= 3) || Math.random() < 0.1) {
const hint: LiteratureName = hintLiterature[Math.floor(Math.random() * hintLiterature.length)];
- if (hint) {
+ if (hint && !server.messages.includes(hint)) {
server.messages.push(hint);
}
}
@@ -261,7 +255,7 @@ export const chargeServerMigration = (server: DarknetServer, threads = 1) => {
xpGained: xpGained,
};
if (newCharge >= 1) {
- moveDarknetServer(server, -2, 4);
+ moveDarknetServer(server, 2, 4);
DarknetState.migrationInductionServers.set(server.hostname, 0);
}
return result;
diff --git a/src/DarkNet/effects/labyrinth.ts b/src/DarkNet/effects/labyrinth.ts
index 3beea417b..98fa8ec87 100644
--- a/src/DarkNet/effects/labyrinth.ts
+++ b/src/DarkNet/effects/labyrinth.ts
@@ -270,7 +270,7 @@ export const handleLabyrinthPassword = (
return {
passwordAttempted: attemptedPassword,
code: ResponseCodeEnum.Success,
- message: "You have discovered the end the labyrinth.",
+ message: "You have discovered the end of the labyrinth.",
data: labServer.password,
};
}
diff --git a/src/DarkNet/effects/ramblock.ts b/src/DarkNet/effects/ramblock.ts
index c802b73c0..58c27e7ff 100644
--- a/src/DarkNet/effects/ramblock.ts
+++ b/src/DarkNet/effects/ramblock.ts
@@ -8,11 +8,12 @@ import { addCacheToServer } from "./cacheFiles";
import { DarknetState } from "../models/DarknetState";
import { getAllMovableDarknetServers } from "../utils/darknetNetworkUtils";
import { CompletedProgramName } from "@enums";
-import type { DarknetServerData, Person as IPerson } from "@nsdefs";
+import type { Person as IPerson, DarknetServerDetails } from "@nsdefs";
import { clampNumber } from "../../utils/helpers/clampNumber";
import { ResponseCodeEnum } from "../Enums";
import { isLabyrinthServer } from "./labyrinth";
import { roundToTwo } from "../../utils/helpers/roundToTwo";
+import type { DarknetServerData } from "../utils/darknetServerUtils";
/*
* Handles the effects of removing some blocked RAM from a Darknet server.
@@ -67,7 +68,11 @@ export const handleRamBlockClearedRewards = (server: DarknetServer) => {
/*
* Calculates the amount of RAM block that is removed from a Darknet server, based on the number of threads and the player's charisma.
*/
-export const getRamBlockRemoved = (darknetServerData: DarknetServerData, threads = 1, player: IPerson = Player) => {
+export const getRamBlockRemoved = (
+ darknetServerData: DarknetServerData | DarknetServerDetails,
+ threads = 1,
+ player: IPerson = Player,
+) => {
const difficulty = darknetServerData.difficulty;
const remainingRamBlock = darknetServerData.blockedRam;
const charismaFactor = 1 + player.skills.charisma / 100;
diff --git a/src/DarkNet/models/DarknetServerOptions.ts b/src/DarkNet/models/DarknetServerOptions.ts
index ccdce54ca..2d4e4d984 100644
--- a/src/DarkNet/models/DarknetServerOptions.ts
+++ b/src/DarkNet/models/DarknetServerOptions.ts
@@ -18,6 +18,7 @@ import { hasFullDarknetAccess } from "../effects/effects";
import { getFriendlyType, TypeAssertionError } from "../../utils/TypeAssertion";
import { isIPAddress } from "../../Types/strings";
import { roundToTwo } from "../../utils/helpers/roundToTwo";
+import { safelyReverseString } from "../../utils/StringHelperFunctions";
export type PasswordResponse = {
code: DarknetResponseCode;
@@ -158,11 +159,11 @@ const decorateName = (name: string): string => {
const connector = connectors[Math.floor(Math.random() * connectors.length)];
if (Math.random() < 0.3) {
- updatedName = l33tifyName(name);
+ updatedName = l33tifyName(updatedName);
}
if (Math.random() < 0.05) {
- updatedName = updatedName.split("").reverse().join("");
+ updatedName = safelyReverseString(updatedName);
}
if (Math.random() < 0.1) {
@@ -180,7 +181,11 @@ const decorateName = (name: string): string => {
}
} while (GetServer(updatedName) !== null);
- return updatedName;
+ // Defensive coding. All operations above preserve well-formed UTF-16, so this is currently redundant. It's a
+ // safeguard to ensure the function never returns ill-formed UTF-16 if future changes introduce code unit–level
+ // manipulation.
+ // This normalization is lossy (lone surrogates -> U+FFFD).
+ return updatedName.toWellFormed();
};
const l33tifyName = (name: string): string => {
@@ -191,7 +196,11 @@ const l33tifyName = (name: string): string => {
const replacement: string = l33t[char] ?? "";
updatedName = updatedName.replaceAll(char, replacement);
}
- return updatedName;
+ // Defensive coding. All operations above preserve well-formed UTF-16, so this is currently redundant. It's a
+ // safeguard to ensure the function never returns ill-formed UTF-16 if future changes introduce code unit–level
+ // manipulation.
+ // This normalization is lossy (lone surrogates -> U+FFFD).
+ return updatedName.toWellFormed();
};
const getMaxRam = (difficulty: number): number => {
diff --git a/src/DarkNet/models/dictionaryData.ts b/src/DarkNet/models/dictionaryData.ts
index 9492390e4..26fc8c10c 100644
--- a/src/DarkNet/models/dictionaryData.ts
+++ b/src/DarkNet/models/dictionaryData.ts
@@ -209,8 +209,6 @@ export const presetNames = [
"Anor_Londo",
"The_Painted_World",
"The_Depths",
- "__proto__",
- "constructor",
] as const;
export const ServerNamePrefixes = [
diff --git a/src/DarkNet/utils/darknetServerUtils.ts b/src/DarkNet/utils/darknetServerUtils.ts
index 38005b3eb..39ab00a1a 100644
--- a/src/DarkNet/utils/darknetServerUtils.ts
+++ b/src/DarkNet/utils/darknetServerUtils.ts
@@ -19,3 +19,50 @@ export function getDarknetServerOrThrow(host: string): DarknetServer {
}
return server;
}
+
+/**
+ * Darknet server data.
+ */
+export interface DarknetServerData {
+ /** Hostname. Must be unique */
+ hostname: string;
+ /** IP Address. Must be unique */
+ ip: string;
+ /** Flag indicating whether the player has admin/root access to this server */
+ hasAdminRights: boolean;
+ /** Flag indicating whether the player's terminal is currently connected to this server */
+ isConnectedTo: boolean;
+ /** Number of CPU cores */
+ cpuCores: number;
+ /** Used RAM (GB). i.e. unavailable RAM */
+ ramUsed: number;
+ /** Max RAM (GB) of this server */
+ maxRam: number;
+ /** Flag indicating whether this server has a backdoor installed by the player */
+ backdoorInstalled: boolean;
+ /** If the server has a stasis link applied */
+ hasStasisLink: boolean;
+ /** The amount of ram blocked by the server owner */
+ blockedRam: number;
+ /**
+ * The model of the server. Similar models have similar vulnerabilities. The model list is intentionally undocumented.
+ * You are supposed to experiment and discover the models.
+ */
+ modelId: string;
+ /** The generic password prompt for the server */
+ staticPasswordHint: string;
+ /** Data associated with the password hint */
+ passwordHintData: string;
+ /** The difficulty rating of the server, associated with its original depth in the net */
+ difficulty: number;
+ /** The depth of the server in the net */
+ depth: number;
+ /** The charisma skill required to heartbleed the server */
+ requiredCharismaSkill: number;
+ /** The interval at which the server periodically adds to its logs, in seconds. */
+ logTrafficInterval: number;
+ /** If this darknet server cannot be moved. True for fixed/story servers. */
+ isStationary: boolean;
+ /** Whether this server was purchased by the player. Always false for darknet servers */
+ purchasedByPlayer: boolean;
+}
diff --git a/src/Documentation/doc/en/basic/hacking.md b/src/Documentation/doc/en/basic/hacking.md
index aaaa0d2a4..461844e5b 100644
--- a/src/Documentation/doc/en/basic/hacking.md
+++ b/src/Documentation/doc/en/basic/hacking.md
@@ -39,7 +39,7 @@ Once you have enough ports opened on a [server](servers.md) and have ran the NUK
### For specific details of how Hacking work "offline"
-See [Offline And Bonus Time](../advanced/offlineandbonustime.md).
+See [Offline And Bonus Time](../programming/offlineandbonustime.md).
## General Hacking Mechanics
diff --git a/src/Documentation/doc/en/basic/scripts.md b/src/Documentation/doc/en/basic/scripts.md
index 5b8b40bcc..80255621f 100644
--- a/src/Documentation/doc/en/basic/scripts.md
+++ b/src/Documentation/doc/en/basic/scripts.md
@@ -17,7 +17,7 @@ Scripts can be run on any [server](servers.md) you have root access to, but not
Being actual JavaScript, Bitburner also contains some quirks and limitations.
For this reason, it is not possible for Bitburner scripts to run the same way at all times.
However, you will continue to earn money and exp when Bitburner is not running, though at a slower rate.
-See [How Scripts Work Offline](../advanced/offlineandbonustime.md) for more details.
+See [How Scripts Work Offline](../programming/offlineandbonustime.md) for more details.
## Identifying a Script
diff --git a/src/Documentation/doc/en/changelog.md b/src/Documentation/doc/en/changelog.md
index bd7da06f0..ca3a7a6b9 100644
--- a/src/Documentation/doc/en/changelog.md
+++ b/src/Documentation/doc/en/changelog.md
@@ -1,5 +1,58 @@
# Changelog
+## v3.0.1 Dev: 17 May 2026
+
+### BREAKING CHANGES
+
+- Change getServer return type; rename getServerAuthDetails and add missing dnet properties (#2746) (@ficocelliguy)
+
+### MISC
+
+- Cache reward fixes (#2731) (@ficocelliguy)
+- Fix typo in darknet authentication response message (#2734) (@catloversg)
+- Fix: Tutorial links to outdated faq url (#2733) (@catloversg)
+- Fix: Player can switch tabs without losing focus on current work (#2724) (@catloversg)
+- Add new command to upload a directory (#2659) (@hexagonrecursion)
+- Add HJKL key mappings for infiltration arrows (#2742) (@mahlquistj)
+- Prevent generating malformed darknet server hostname (#2744) (@catloversg)
+- Support angle bracket type assertions in RAM calculation (#2751) (@catloversg)
+- Fix typo in augmentation description (#2760) (@gmcew)
+- Reduce the RAM cost of ns.rm() to match ns.scp() (#2761) (@NagaOuroboros)
+- Temporarily remove darknet servers with unusual hostnames (#2757) (@catloversg)
+- Fix: Duplicate .lit and .cache files can be generated in dnet (#2763) (@catloversg)
+- Allow getFunctionRamCost to get base RAM cost for scripts (#2771) (@Mathekatze)
+
+### DOCUMENTATION
+
+- Remove TS type annotation from doc example script (#2721) (@ficocelliguy)
+- Update list of RFA community tools (#2722) (@CTNOriginals)
+- Fix incorrect cloud API example (#2738) (@catloversg)
+- Remove non-existent influence namespace in Darknet documentation (#2748) (@Berdes)
+- Remove spoiler for Offline scripts and bonus time page and make it accessible early-game (#2749) (@Berdes)
+- Clarify ns.scp and ns.isRunning (#2769) (@catloversg)
+
+### SPOILER CHANGES - UI
+
+- Show hints of Gang mechanic in pre-endgame (#2723) (@catloversg)
+- Break out Darknet BN and player mults as separate entries (#2745) (@gmcew)
+
+### SPOILER CHANGES - MISC
+
+- Cancel faction work instead of finishing it when creating gang (#2726) (@catloversg)
+- Lower darknet BN money mults on hard nodes (#2743) (@ficocelliguy)
+- Change getDarkwebPrograms return type to ProgramName[] (#2754) (@catloversg)
+- Fix: getAugmentationBasePrice ignores bitnode mult for SoA augs (#2756) (@SAY-5)
+- Rebalance hash base cost of generating coding contract (#2759) (@gmcew)
+- Ensure induceServerMigration moves servers randomly (#2767) (@catloversg)
+- Do not decrease player reputation when failing bladeburner actions (#2768) (@catloversg)
+
+### CODEBASE/REFACTOR/WORKFLOW/JEST/TOOL/DEPS
+
+- Update action versions (#2718) (@Snarling)
+- Remove duplicate getStockFromSymbol function (#2725) (@catloversg)
+- Update game version (#2732) (@catloversg)
+- Harden saving to avoid save data corruption (#2755) (@catloversg)
+
## v3.0.0: 1 May 2026
### BREAKING CHANGES
@@ -462,3 +515,5 @@
- Consistently check when to show intelligence skill (#2692) (@catloversg)
- Fix api-extractor warnings (#2701) (@catloversg)
- Add comments explaining redundant check in product calculation (#2705) (@catloversg)
+- Update tests related to calculateExp and clampNumber (#2713) (@catloversg)
+- Update Electron (#2712) (@catloversg)
diff --git a/src/Documentation/doc/en/help/getting_started.md b/src/Documentation/doc/en/help/getting_started.md
index d9c18b028..503c7df06 100644
--- a/src/Documentation/doc/en/help/getting_started.md
+++ b/src/Documentation/doc/en/help/getting_started.md
@@ -377,7 +377,7 @@ Paste the following code into the [Script](../basic/scripts.md) editor:
// amount of servers
while (i < ns.cloud.getServerLimit()) {
// Check if we have enough money to purchase access to a server
- if (ns.getServerMoneyAvailable("home") > ns.cloud.getRamLimit(ram)) {
+ if (ns.getServerMoneyAvailable("home") > ns.cloud.getServerCost(ram)) {
// If we have enough money, then:
// 1. Purchase the server
// 2. Copy our hacking script onto the newly purchased cloud server
diff --git a/src/Documentation/doc/en/index.md b/src/Documentation/doc/en/index.md
index 0994d34d9..7ae6cb072 100644
--- a/src/Documentation/doc/en/index.md
+++ b/src/Documentation/doc/en/index.md
@@ -28,8 +28,8 @@
## Advanced Mechanics
- [Hacking algorithms](programming/hackingalgorithms.md)
+- [Offline scripts and bonus time](programming/offlineandbonustime.md)
- [List of factions and their requirements](advanced/faction_list.md)
-- [Offline scripts and bonus time](advanced/offlineandbonustime.md)
- [BitNodes](advanced/bitnodes.md)
- [BitNode recommendation - Short guide](advanced/bitnode_recommendation_short_guide.md)
- [BitNode recommendation - Comprehensive guide](advanced/bitnode_recommendation_comprehensive_guide.md)
diff --git a/src/Documentation/doc/en/programming/darknet.md b/src/Documentation/doc/en/programming/darknet.md
index 244e7640c..88bb235d1 100644
--- a/src/Documentation/doc/en/programming/darknet.md
+++ b/src/Documentation/doc/en/programming/darknet.md
@@ -20,12 +20,12 @@ In some cases, the only way to get to deeper parts of the net is to hitch a ride
**There is an example starter script at the bottom of this document, to see some of these API methods in action.**
-- `dnet.getServerAuthDetails(hostname)` tells you a server's password hint and format, and if the server is offline or connected to the current server.
+- `dnet.getServerDetails(hostname)` tells you a server's password hint and format, and if the server is offline or connected to the current server.
- `ns.dnet.probe()` lets you find darknet servers directly connected to your current server. Use this to find targets to crack and copy your script onto.
- `await ns.dnet.authenticate(hostname, password)` lets you guess and check passwords for servers directly connected to your script's server. If you guess right, you get admin access and can use `exec` and `scp` to move scripts onto that server.
- Some servers require interactive feedback to guess their password. Use `await ns.dnet.heartbleed(hostname)` to check that server's logs and get clues after you attempt a password.
- `ns.dnet.connectToSession(hostName, password)` lets you use a password you already know to log in to a darknet server at a distance. This is required to scp files there.
-- Some servers will have part of their max ram blocked off. Use `ns.dnet.influence.memoryReallocation()` to free it.
+- Some servers will have part of their max ram blocked off. Use `ns.dnet.memoryReallocation()` to free it.
- Some servers have valuable .cache files you can open with `ns.dnet.openCache(fileName)`
- Darknet servers allow you to run `ns.dnet.phishingAttack()` to get money or .cache files based off of your charisma and crime success stat.
- Using `ns.dnet.setStasisLink()` will stasis lock the current server. This prevents it from moving or going offline, and also allows getting a session on the server at a distance like backdooring does.
@@ -86,12 +86,12 @@ Darknet servers cannot simply be broken into with a few port openers you can buy
### Cracking servers with dnet.authenticate and dnet.heartbleed
-Darknet servers require a password to interact with. To get started, use `dnet.getServerAuthDetails` to find out critical information about a server. It will give a hint to the password, and tell you if the server is still online.
+Darknet servers require a password to interact with. To get started, use `dnet.getServerDetails` to find out critical information about a server. It will give a hint to the password, and tell you if the server is still online.
-You can use `await ns.dnet.authenticate` to check if a guessed password is correct. (Remember to await it, network requests take time!) The higher your charisma, the faster you can smooth-talk your way through these vulnerable servers' security. Using more threads also speeds up this process. It may be faster to divide up the work across multiple scripts, if you can coordinate them. (Note that **`dnet.authenticate` can only target nearby connected servers**. You can verify if a server is connected to the current one using `dnet.probe` or `dnet.getServerAuthDetails`.)
+You can use `await ns.dnet.authenticate` to check if a guessed password is correct. (Remember to await it, network requests take time!) The higher your charisma, the faster you can smooth-talk your way through these vulnerable servers' security. Using more threads also speeds up this process. It may be faster to divide up the work across multiple scripts, if you can coordinate them. (Note that **`dnet.authenticate` can only target nearby connected servers**. You can verify if a server is connected to the current one using `dnet.probe` or `dnet.getServerDetails`.)
```js
-const details = ns.dnet.getServerAuthDetails(hostname);
+const details = ns.dnet.getServerDetails(hostname);
if (!details.isConnectedToCurrentServer || !details.isOnline) {
/* If the server isn't connected or is offline, we can't authenticate */
return false;
@@ -123,12 +123,12 @@ Darknet servers are password-protected. This means that you will need to get a s
Once you have authenticated, other scripts can then connect to that same server using `dnet.connectToSession` and the password for the server. This is synchronous and can be done at any distance, meaning you don't have to wait for an authenticate call.
-`scp` file transfers can be performed at any distance once you have established a session. However, `exec` also requires the script to either be run from a server adjacent to and connected to the target server, or a backdoor or stasis link on the target server. You can identify direct connections using `probe` or `getServerAuthDetails`.
+`scp` file transfers can be performed at any distance once you have established a session. However, `exec` also requires the script to either be run from a server adjacent to and connected to the target server, or a backdoor or stasis link on the target server. You can identify direct connections using `probe` or `getServerDetails`.
```js
// the darknet server in "hostname" must be either backdoored, stasis linked, or directly connected to the server this script is running on
// to allow exec calls from the current server
-if (ns.dnet.getServerAuthDetails(hostname).isConnectedToCurrentServer) {
+if (ns.dnet.getServerDetails(hostname).isConnectedToCurrentServer) {
ns.dnet.connectToSession(hostname, previouslyDiscoveredPassword);
ns.scp("my_script.js", hostname);
ns.exec("my_script.js", hostname, {
@@ -214,7 +214,7 @@ export async function main(ns) {
*/
export const serverSolver = async (ns, hostname) => {
// Get key info about the server, so we know what kind it is and how to authenticate with it
- const details = ns.dnet.getServerAuthDetails(hostname);
+ const details = ns.dnet.getServerDetails(hostname);
if (!details.isConnectedToCurrentServer || !details.isOnline) {
// If the server isn't connected or is offline, we can't authenticate
return false;
@@ -240,7 +240,7 @@ export const serverSolver = async (ns, hostname) => {
/** Authenticates on 'ZeroLogon' type servers, which always have an empty password.
* @param {NS} ns
- * @param {string} hostname - the name of the server to attempt to authorize on
+ * @param {string} hostname - the name of the server to attempt to authorize on
*/
const authenticateWithNoPassword = async (ns, hostname) => {
const result = await ns.dnet.authenticate(hostname, "");
@@ -248,9 +248,10 @@ const authenticateWithNoPassword = async (ns, hostname) => {
return result.success;
};
-// This lets you tab-complete putting "--tail" on the run command so you can see the script logs as it runs, if you want
-// If you add support to the script to take other arguments, you can add them here as well for convenience
-export function autocomplete(data: AutocompleteData) {
+/** This lets you tab-complete putting "--tail" on the run command so you can see the script logs as it runs, if you want
+ * If you add support to the script to take other arguments, you can add them here as well for convenience
+ * @param {AutocompleteData} data */
+export function autocomplete(data) {
return ["--tail"];
}
```
diff --git a/src/Documentation/doc/en/programming/game_frozen.md b/src/Documentation/doc/en/programming/game_frozen.md
index 3ebac8600..7a4c82c53 100644
--- a/src/Documentation/doc/en/programming/game_frozen.md
+++ b/src/Documentation/doc/en/programming/game_frozen.md
@@ -37,7 +37,7 @@ Common infinite loop when translating the server purchasing script in starting g
let i = ns.cloud.getServerNames().length;
while (i < ns.cloud.getServerLimit()) {
- if (ns.getServerMoneyAvailable("home") > ns.cloud.getRamLimit(ram)) {
+ if (ns.getServerMoneyAvailable("home") > ns.cloud.getServerCost(ram)) {
const hostname = ns.cloud.purchaseServer("cloud-server-" + i, ram);
ns.scp("early-hack-template.js", hostname);
ns.exec("early-hack-template.js", hostname, 3);
diff --git a/src/Documentation/doc/en/advanced/offlineandbonustime.md b/src/Documentation/doc/en/programming/offlineandbonustime.md
similarity index 86%
rename from src/Documentation/doc/en/advanced/offlineandbonustime.md
rename to src/Documentation/doc/en/programming/offlineandbonustime.md
index 19bd4b59b..748e0ac5f 100644
--- a/src/Documentation/doc/en/advanced/offlineandbonustime.md
+++ b/src/Documentation/doc/en/programming/offlineandbonustime.md
@@ -24,4 +24,4 @@ Also, note that because of the way the JavaScript engine works, whenever you rel
Because of the above details, some activities in Bitburner accumulate "Bonus Time" while the game is closed or in an inactive browser tab. For mechanics that have a Bonus Time effect, the rate of the associated activity or task is significantly increased.
-For example, if a certain [Bladeburner](bladeburners.md) contract requires 15 seconds to complete under normal conditions, the same task will be finished instead in 3 seconds if the Bonus Time effect is 5x. The specific details and effects of Bonus Time vary by mechanic.
+For example, if your opponent in [IPvGO](../programming/go_algorithms.md) takes 1 second to play a move under normal conditions, it will only take 200ms if the Bonus Time effect is 5x. The specific details and effects of Bonus Time vary by mechanic.
diff --git a/src/Documentation/doc/en/programming/remote_api.md b/src/Documentation/doc/en/programming/remote_api.md
index deb155ab0..810fd74a4 100644
--- a/src/Documentation/doc/en/programming/remote_api.md
+++ b/src/Documentation/doc/en/programming/remote_api.md
@@ -13,11 +13,13 @@ All these tools support synchronizing scripts to Bitburner and transpiling TypeS
Links:
-- https://github.com/bitburner-official/typescript-template
-- https://github.com/Tanimodori/viteburner
-- https://github.com/shyguy1412/bb-external-editor
+- [typescript-template](https://github.com/bitburner-official/typescript-template): A template for synchronizing Typescript/Javascript from your computer to the game.
+- [viteburner](https://github.com/Tanimodori/viteburner): Daemon tools of bitburner using vite for script transform, file syncing, RAM monitoring and more!
+- [bb-external-editor](https://github.com/shyguy1412/bb-external-editor): This tool uses esbuild to transpile and bundle your scripts. It supports JS, TS and React as well as importing from any browser-compatible npm library out of the box.
+- [BitburnerGoFilesync](https://github.com/CTNOriginals/BitburnerGoFilesync): A standalone binary cli tool that doesn't require any setup or third party libraries. It is designed to be very minimal and easy to use out of the box.
-`typescript-template` has a small set of options and features. Its simplicity is by design. `viteburner` and `bb-external-editor` have more fancy features.
+`typescript-template` and `BitburnerGoFilesync` both have a small set of options and features, their simplicity is by design.
+`viteburner` and `bb-external-editor` have more fancy features and may offer more control for specific use cases.
## Troubleshooting tips
diff --git a/src/Documentation/pages.ts b/src/Documentation/pages.ts
index 043b73e55..07ca39f4e 100644
--- a/src/Documentation/pages.ts
+++ b/src/Documentation/pages.ts
@@ -26,44 +26,44 @@ import file23 from "./doc/en/advanced/gang.md?raw";
import file24 from "./doc/en/advanced/grafting.md?raw";
import file25 from "./doc/en/advanced/hacknetservers.md?raw";
import file26 from "./doc/en/advanced/intelligence.md?raw";
-import file27 from "./doc/en/advanced/offlineandbonustime.md?raw";
-import file28 from "./doc/en/advanced/sleeves.md?raw";
-import file29 from "./doc/en/advanced/sourcefiles.md?raw";
-import file30 from "./doc/en/advanced/stanek.md?raw";
-import file31 from "./doc/en/basic/augmentations.md?raw";
-import file32 from "./doc/en/basic/autocomplete.md?raw";
-import file33 from "./doc/en/basic/codingcontracts.md?raw";
-import file34 from "./doc/en/basic/companies.md?raw";
-import file35 from "./doc/en/basic/crimes.md?raw";
-import file36 from "./doc/en/basic/factions.md?raw";
-import file37 from "./doc/en/basic/hacking.md?raw";
-import file38 from "./doc/en/basic/hacknet_nodes.md?raw";
-import file39 from "./doc/en/basic/infiltration.md?raw";
-import file40 from "./doc/en/basic/programs.md?raw";
-import file41 from "./doc/en/basic/ram.md?raw";
-import file42 from "./doc/en/basic/reputation.md?raw";
-import file43 from "./doc/en/basic/scripts.md?raw";
-import file44 from "./doc/en/basic/servers.md?raw";
-import file45 from "./doc/en/basic/stats.md?raw";
-import file46 from "./doc/en/basic/stockmarket.md?raw";
-import file47 from "./doc/en/basic/terminal.md?raw";
-import file48 from "./doc/en/basic/world.md?raw";
-import file49 from "./doc/en/changelog-v0.md?raw";
-import file50 from "./doc/en/changelog-v1.md?raw";
-import file51 from "./doc/en/changelog-v2.md?raw";
-import file52 from "./doc/en/changelog.md?raw";
-import file53 from "./doc/en/help/faq.md?raw";
-import file54 from "./doc/en/help/getting_started.md?raw";
-import file55 from "./doc/en/help/tools_and_resources.md?raw";
-import file56 from "./doc/en/index.md?raw";
-import file57 from "./doc/en/migrations/ns2.md?raw";
-import file58 from "./doc/en/migrations/v1.md?raw";
-import file59 from "./doc/en/migrations/v2.md?raw";
-import file60 from "./doc/en/programming/darknet.md?raw";
-import file61 from "./doc/en/programming/game_frozen.md?raw";
-import file62 from "./doc/en/programming/go_algorithms.md?raw";
-import file63 from "./doc/en/programming/hackingalgorithms.md?raw";
-import file64 from "./doc/en/programming/learn.md?raw";
+import file27 from "./doc/en/advanced/sleeves.md?raw";
+import file28 from "./doc/en/advanced/sourcefiles.md?raw";
+import file29 from "./doc/en/advanced/stanek.md?raw";
+import file30 from "./doc/en/basic/augmentations.md?raw";
+import file31 from "./doc/en/basic/autocomplete.md?raw";
+import file32 from "./doc/en/basic/codingcontracts.md?raw";
+import file33 from "./doc/en/basic/companies.md?raw";
+import file34 from "./doc/en/basic/crimes.md?raw";
+import file35 from "./doc/en/basic/factions.md?raw";
+import file36 from "./doc/en/basic/hacking.md?raw";
+import file37 from "./doc/en/basic/hacknet_nodes.md?raw";
+import file38 from "./doc/en/basic/infiltration.md?raw";
+import file39 from "./doc/en/basic/programs.md?raw";
+import file40 from "./doc/en/basic/ram.md?raw";
+import file41 from "./doc/en/basic/reputation.md?raw";
+import file42 from "./doc/en/basic/scripts.md?raw";
+import file43 from "./doc/en/basic/servers.md?raw";
+import file44 from "./doc/en/basic/stats.md?raw";
+import file45 from "./doc/en/basic/stockmarket.md?raw";
+import file46 from "./doc/en/basic/terminal.md?raw";
+import file47 from "./doc/en/basic/world.md?raw";
+import file48 from "./doc/en/changelog-v0.md?raw";
+import file49 from "./doc/en/changelog-v1.md?raw";
+import file50 from "./doc/en/changelog-v2.md?raw";
+import file51 from "./doc/en/changelog.md?raw";
+import file52 from "./doc/en/help/faq.md?raw";
+import file53 from "./doc/en/help/getting_started.md?raw";
+import file54 from "./doc/en/help/tools_and_resources.md?raw";
+import file55 from "./doc/en/index.md?raw";
+import file56 from "./doc/en/migrations/ns2.md?raw";
+import file57 from "./doc/en/migrations/v1.md?raw";
+import file58 from "./doc/en/migrations/v2.md?raw";
+import file59 from "./doc/en/programming/darknet.md?raw";
+import file60 from "./doc/en/programming/game_frozen.md?raw";
+import file61 from "./doc/en/programming/go_algorithms.md?raw";
+import file62 from "./doc/en/programming/hackingalgorithms.md?raw";
+import file63 from "./doc/en/programming/learn.md?raw";
+import file64 from "./doc/en/programming/offlineandbonustime.md?raw";
import file65 from "./doc/en/programming/remote_api.md?raw";
import file66 from "./doc/en/programming/typescript_react.md?raw";
@@ -428,7 +428,7 @@ import nsDoc_bitburner_darknet_connecttosession_md from "../../markdown/bitburne
import nsDoc_bitburner_darknet_getblockedram_md from "../../markdown/bitburner.darknet.getblockedram.md?raw";
import nsDoc_bitburner_darknet_getdarknetinstability_md from "../../markdown/bitburner.darknet.getdarknetinstability.md?raw";
import nsDoc_bitburner_darknet_getdepth_md from "../../markdown/bitburner.darknet.getdepth.md?raw";
-import nsDoc_bitburner_darknet_getserverauthdetails_md from "../../markdown/bitburner.darknet.getserverauthdetails.md?raw";
+import nsDoc_bitburner_darknet_getserverdetails_md from "../../markdown/bitburner.darknet.getserverdetails.md?raw";
import nsDoc_bitburner_darknet_getserverrequiredcharismalevel_md from "../../markdown/bitburner.darknet.getserverrequiredcharismalevel.md?raw";
import nsDoc_bitburner_darknet_getstasislinkedservers_md from "../../markdown/bitburner.darknet.getstasislinkedservers.md?raw";
import nsDoc_bitburner_darknet_getstasislinklimit_md from "../../markdown/bitburner.darknet.getstasislinklimit.md?raw";
@@ -456,26 +456,20 @@ import nsDoc_bitburner_darknetinstability_md from "../../markdown/bitburner.dark
import nsDoc_bitburner_darknetresponsecode_md from "../../markdown/bitburner.darknetresponsecode.md?raw";
import nsDoc_bitburner_darknetresponsecodetype_md from "../../markdown/bitburner.darknetresponsecodetype.md?raw";
import nsDoc_bitburner_darknetresult_md from "../../markdown/bitburner.darknetresult.md?raw";
-import nsDoc_bitburner_darknetserverdata_backdoorinstalled_md from "../../markdown/bitburner.darknetserverdata.backdoorinstalled.md?raw";
-import nsDoc_bitburner_darknetserverdata_blockedram_md from "../../markdown/bitburner.darknetserverdata.blockedram.md?raw";
-import nsDoc_bitburner_darknetserverdata_cpucores_md from "../../markdown/bitburner.darknetserverdata.cpucores.md?raw";
-import nsDoc_bitburner_darknetserverdata_depth_md from "../../markdown/bitburner.darknetserverdata.depth.md?raw";
-import nsDoc_bitburner_darknetserverdata_difficulty_md from "../../markdown/bitburner.darknetserverdata.difficulty.md?raw";
-import nsDoc_bitburner_darknetserverdata_hasadminrights_md from "../../markdown/bitburner.darknetserverdata.hasadminrights.md?raw";
-import nsDoc_bitburner_darknetserverdata_hasstasislink_md from "../../markdown/bitburner.darknetserverdata.hasstasislink.md?raw";
-import nsDoc_bitburner_darknetserverdata_hostname_md from "../../markdown/bitburner.darknetserverdata.hostname.md?raw";
-import nsDoc_bitburner_darknetserverdata_ip_md from "../../markdown/bitburner.darknetserverdata.ip.md?raw";
-import nsDoc_bitburner_darknetserverdata_isconnectedto_md from "../../markdown/bitburner.darknetserverdata.isconnectedto.md?raw";
-import nsDoc_bitburner_darknetserverdata_isstationary_md from "../../markdown/bitburner.darknetserverdata.isstationary.md?raw";
-import nsDoc_bitburner_darknetserverdata_logtrafficinterval_md from "../../markdown/bitburner.darknetserverdata.logtrafficinterval.md?raw";
-import nsDoc_bitburner_darknetserverdata_maxram_md from "../../markdown/bitburner.darknetserverdata.maxram.md?raw";
-import nsDoc_bitburner_darknetserverdata_md from "../../markdown/bitburner.darknetserverdata.md?raw";
-import nsDoc_bitburner_darknetserverdata_modelid_md from "../../markdown/bitburner.darknetserverdata.modelid.md?raw";
-import nsDoc_bitburner_darknetserverdata_passwordhintdata_md from "../../markdown/bitburner.darknetserverdata.passwordhintdata.md?raw";
-import nsDoc_bitburner_darknetserverdata_purchasedbyplayer_md from "../../markdown/bitburner.darknetserverdata.purchasedbyplayer.md?raw";
-import nsDoc_bitburner_darknetserverdata_ramused_md from "../../markdown/bitburner.darknetserverdata.ramused.md?raw";
-import nsDoc_bitburner_darknetserverdata_requiredcharismaskill_md from "../../markdown/bitburner.darknetserverdata.requiredcharismaskill.md?raw";
-import nsDoc_bitburner_darknetserverdata_staticpasswordhint_md from "../../markdown/bitburner.darknetserverdata.staticpasswordhint.md?raw";
+import nsDoc_bitburner_darknetserverdetails_blockedram_md from "../../markdown/bitburner.darknetserverdetails.blockedram.md?raw";
+import nsDoc_bitburner_darknetserverdetails_data_md from "../../markdown/bitburner.darknetserverdetails.data.md?raw";
+import nsDoc_bitburner_darknetserverdetails_depth_md from "../../markdown/bitburner.darknetserverdetails.depth.md?raw";
+import nsDoc_bitburner_darknetserverdetails_difficulty_md from "../../markdown/bitburner.darknetserverdetails.difficulty.md?raw";
+import nsDoc_bitburner_darknetserverdetails_hassession_md from "../../markdown/bitburner.darknetserverdetails.hassession.md?raw";
+import nsDoc_bitburner_darknetserverdetails_isconnectedtocurrentserver_md from "../../markdown/bitburner.darknetserverdetails.isconnectedtocurrentserver.md?raw";
+import nsDoc_bitburner_darknetserverdetails_isstationary_md from "../../markdown/bitburner.darknetserverdetails.isstationary.md?raw";
+import nsDoc_bitburner_darknetserverdetails_logtrafficinterval_md from "../../markdown/bitburner.darknetserverdetails.logtrafficinterval.md?raw";
+import nsDoc_bitburner_darknetserverdetails_md from "../../markdown/bitburner.darknetserverdetails.md?raw";
+import nsDoc_bitburner_darknetserverdetails_modelid_md from "../../markdown/bitburner.darknetserverdetails.modelid.md?raw";
+import nsDoc_bitburner_darknetserverdetails_passwordformat_md from "../../markdown/bitburner.darknetserverdetails.passwordformat.md?raw";
+import nsDoc_bitburner_darknetserverdetails_passwordhint_md from "../../markdown/bitburner.darknetserverdetails.passwordhint.md?raw";
+import nsDoc_bitburner_darknetserverdetails_passwordlength_md from "../../markdown/bitburner.darknetserverdetails.passwordlength.md?raw";
+import nsDoc_bitburner_darknetserverdetails_requiredcharismaskill_md from "../../markdown/bitburner.darknetserverdetails.requiredcharismaskill.md?raw";
import nsDoc_bitburner_division_awareness_md from "../../markdown/bitburner.division.awareness.md?raw";
import nsDoc_bitburner_division_cities_md from "../../markdown/bitburner.division.cities.md?raw";
import nsDoc_bitburner_division_industry_md from "../../markdown/bitburner.division.industry.md?raw";
@@ -1273,15 +1267,6 @@ import nsDoc_bitburner_server_servergrowth_md from "../../markdown/bitburner.ser
import nsDoc_bitburner_server_smtpportopen_md from "../../markdown/bitburner.server.smtpportopen.md?raw";
import nsDoc_bitburner_server_sqlportopen_md from "../../markdown/bitburner.server.sqlportopen.md?raw";
import nsDoc_bitburner_server_sshportopen_md from "../../markdown/bitburner.server.sshportopen.md?raw";
-import nsDoc_bitburner_serverauthdetails_data_md from "../../markdown/bitburner.serverauthdetails.data.md?raw";
-import nsDoc_bitburner_serverauthdetails_hassession_md from "../../markdown/bitburner.serverauthdetails.hassession.md?raw";
-import nsDoc_bitburner_serverauthdetails_isconnectedtocurrentserver_md from "../../markdown/bitburner.serverauthdetails.isconnectedtocurrentserver.md?raw";
-import nsDoc_bitburner_serverauthdetails_logtrafficinterval_md from "../../markdown/bitburner.serverauthdetails.logtrafficinterval.md?raw";
-import nsDoc_bitburner_serverauthdetails_md from "../../markdown/bitburner.serverauthdetails.md?raw";
-import nsDoc_bitburner_serverauthdetails_modelid_md from "../../markdown/bitburner.serverauthdetails.modelid.md?raw";
-import nsDoc_bitburner_serverauthdetails_passwordformat_md from "../../markdown/bitburner.serverauthdetails.passwordformat.md?raw";
-import nsDoc_bitburner_serverauthdetails_passwordhint_md from "../../markdown/bitburner.serverauthdetails.passwordhint.md?raw";
-import nsDoc_bitburner_serverauthdetails_passwordlength_md from "../../markdown/bitburner.serverauthdetails.passwordlength.md?raw";
import nsDoc_bitburner_simpleopponentstats_md from "../../markdown/bitburner.simpleopponentstats.md?raw";
import nsDoc_bitburner_singularity_applytocompany_md from "../../markdown/bitburner.singularity.applytocompany.md?raw";
import nsDoc_bitburner_singularity_b1tflum3_md from "../../markdown/bitburner.singularity.b1tflum3.md?raw";
@@ -1649,44 +1634,44 @@ AllPages["en/advanced/gang.md"] = file23;
AllPages["en/advanced/grafting.md"] = file24;
AllPages["en/advanced/hacknetservers.md"] = file25;
AllPages["en/advanced/intelligence.md"] = file26;
-AllPages["en/advanced/offlineandbonustime.md"] = file27;
-AllPages["en/advanced/sleeves.md"] = file28;
-AllPages["en/advanced/sourcefiles.md"] = file29;
-AllPages["en/advanced/stanek.md"] = file30;
-AllPages["en/basic/augmentations.md"] = file31;
-AllPages["en/basic/autocomplete.md"] = file32;
-AllPages["en/basic/codingcontracts.md"] = file33;
-AllPages["en/basic/companies.md"] = file34;
-AllPages["en/basic/crimes.md"] = file35;
-AllPages["en/basic/factions.md"] = file36;
-AllPages["en/basic/hacking.md"] = file37;
-AllPages["en/basic/hacknet_nodes.md"] = file38;
-AllPages["en/basic/infiltration.md"] = file39;
-AllPages["en/basic/programs.md"] = file40;
-AllPages["en/basic/ram.md"] = file41;
-AllPages["en/basic/reputation.md"] = file42;
-AllPages["en/basic/scripts.md"] = file43;
-AllPages["en/basic/servers.md"] = file44;
-AllPages["en/basic/stats.md"] = file45;
-AllPages["en/basic/stockmarket.md"] = file46;
-AllPages["en/basic/terminal.md"] = file47;
-AllPages["en/basic/world.md"] = file48;
-AllPages["en/changelog-v0.md"] = file49;
-AllPages["en/changelog-v1.md"] = file50;
-AllPages["en/changelog-v2.md"] = file51;
-AllPages["en/changelog.md"] = file52;
-AllPages["en/help/faq.md"] = file53;
-AllPages["en/help/getting_started.md"] = file54;
-AllPages["en/help/tools_and_resources.md"] = file55;
-AllPages["en/index.md"] = file56;
-AllPages["en/migrations/ns2.md"] = file57;
-AllPages["en/migrations/v1.md"] = file58;
-AllPages["en/migrations/v2.md"] = file59;
-AllPages["en/programming/darknet.md"] = file60;
-AllPages["en/programming/game_frozen.md"] = file61;
-AllPages["en/programming/go_algorithms.md"] = file62;
-AllPages["en/programming/hackingalgorithms.md"] = file63;
-AllPages["en/programming/learn.md"] = file64;
+AllPages["en/advanced/sleeves.md"] = file27;
+AllPages["en/advanced/sourcefiles.md"] = file28;
+AllPages["en/advanced/stanek.md"] = file29;
+AllPages["en/basic/augmentations.md"] = file30;
+AllPages["en/basic/autocomplete.md"] = file31;
+AllPages["en/basic/codingcontracts.md"] = file32;
+AllPages["en/basic/companies.md"] = file33;
+AllPages["en/basic/crimes.md"] = file34;
+AllPages["en/basic/factions.md"] = file35;
+AllPages["en/basic/hacking.md"] = file36;
+AllPages["en/basic/hacknet_nodes.md"] = file37;
+AllPages["en/basic/infiltration.md"] = file38;
+AllPages["en/basic/programs.md"] = file39;
+AllPages["en/basic/ram.md"] = file40;
+AllPages["en/basic/reputation.md"] = file41;
+AllPages["en/basic/scripts.md"] = file42;
+AllPages["en/basic/servers.md"] = file43;
+AllPages["en/basic/stats.md"] = file44;
+AllPages["en/basic/stockmarket.md"] = file45;
+AllPages["en/basic/terminal.md"] = file46;
+AllPages["en/basic/world.md"] = file47;
+AllPages["en/changelog-v0.md"] = file48;
+AllPages["en/changelog-v1.md"] = file49;
+AllPages["en/changelog-v2.md"] = file50;
+AllPages["en/changelog.md"] = file51;
+AllPages["en/help/faq.md"] = file52;
+AllPages["en/help/getting_started.md"] = file53;
+AllPages["en/help/tools_and_resources.md"] = file54;
+AllPages["en/index.md"] = file55;
+AllPages["en/migrations/ns2.md"] = file56;
+AllPages["en/migrations/v1.md"] = file57;
+AllPages["en/migrations/v2.md"] = file58;
+AllPages["en/programming/darknet.md"] = file59;
+AllPages["en/programming/game_frozen.md"] = file60;
+AllPages["en/programming/go_algorithms.md"] = file61;
+AllPages["en/programming/hackingalgorithms.md"] = file62;
+AllPages["en/programming/learn.md"] = file63;
+AllPages["en/programming/offlineandbonustime.md"] = file64;
AllPages["en/programming/remote_api.md"] = file65;
AllPages["en/programming/typescript_react.md"] = file66;
@@ -2051,7 +2036,7 @@ AllPages["nsDoc/bitburner.darknet.connecttosession.md"] = nsDoc_bitburner_darkne
AllPages["nsDoc/bitburner.darknet.getblockedram.md"] = nsDoc_bitburner_darknet_getblockedram_md;
AllPages["nsDoc/bitburner.darknet.getdarknetinstability.md"] = nsDoc_bitburner_darknet_getdarknetinstability_md;
AllPages["nsDoc/bitburner.darknet.getdepth.md"] = nsDoc_bitburner_darknet_getdepth_md;
-AllPages["nsDoc/bitburner.darknet.getserverauthdetails.md"] = nsDoc_bitburner_darknet_getserverauthdetails_md;
+AllPages["nsDoc/bitburner.darknet.getserverdetails.md"] = nsDoc_bitburner_darknet_getserverdetails_md;
AllPages["nsDoc/bitburner.darknet.getserverrequiredcharismalevel.md"] = nsDoc_bitburner_darknet_getserverrequiredcharismalevel_md;
AllPages["nsDoc/bitburner.darknet.getstasislinkedservers.md"] = nsDoc_bitburner_darknet_getstasislinkedservers_md;
AllPages["nsDoc/bitburner.darknet.getstasislinklimit.md"] = nsDoc_bitburner_darknet_getstasislinklimit_md;
@@ -2079,26 +2064,20 @@ AllPages["nsDoc/bitburner.darknetinstability.md"] = nsDoc_bitburner_darknetinsta
AllPages["nsDoc/bitburner.darknetresponsecode.md"] = nsDoc_bitburner_darknetresponsecode_md;
AllPages["nsDoc/bitburner.darknetresponsecodetype.md"] = nsDoc_bitburner_darknetresponsecodetype_md;
AllPages["nsDoc/bitburner.darknetresult.md"] = nsDoc_bitburner_darknetresult_md;
-AllPages["nsDoc/bitburner.darknetserverdata.backdoorinstalled.md"] = nsDoc_bitburner_darknetserverdata_backdoorinstalled_md;
-AllPages["nsDoc/bitburner.darknetserverdata.blockedram.md"] = nsDoc_bitburner_darknetserverdata_blockedram_md;
-AllPages["nsDoc/bitburner.darknetserverdata.cpucores.md"] = nsDoc_bitburner_darknetserverdata_cpucores_md;
-AllPages["nsDoc/bitburner.darknetserverdata.depth.md"] = nsDoc_bitburner_darknetserverdata_depth_md;
-AllPages["nsDoc/bitburner.darknetserverdata.difficulty.md"] = nsDoc_bitburner_darknetserverdata_difficulty_md;
-AllPages["nsDoc/bitburner.darknetserverdata.hasadminrights.md"] = nsDoc_bitburner_darknetserverdata_hasadminrights_md;
-AllPages["nsDoc/bitburner.darknetserverdata.hasstasislink.md"] = nsDoc_bitburner_darknetserverdata_hasstasislink_md;
-AllPages["nsDoc/bitburner.darknetserverdata.hostname.md"] = nsDoc_bitburner_darknetserverdata_hostname_md;
-AllPages["nsDoc/bitburner.darknetserverdata.ip.md"] = nsDoc_bitburner_darknetserverdata_ip_md;
-AllPages["nsDoc/bitburner.darknetserverdata.isconnectedto.md"] = nsDoc_bitburner_darknetserverdata_isconnectedto_md;
-AllPages["nsDoc/bitburner.darknetserverdata.isstationary.md"] = nsDoc_bitburner_darknetserverdata_isstationary_md;
-AllPages["nsDoc/bitburner.darknetserverdata.logtrafficinterval.md"] = nsDoc_bitburner_darknetserverdata_logtrafficinterval_md;
-AllPages["nsDoc/bitburner.darknetserverdata.maxram.md"] = nsDoc_bitburner_darknetserverdata_maxram_md;
-AllPages["nsDoc/bitburner.darknetserverdata.md"] = nsDoc_bitburner_darknetserverdata_md;
-AllPages["nsDoc/bitburner.darknetserverdata.modelid.md"] = nsDoc_bitburner_darknetserverdata_modelid_md;
-AllPages["nsDoc/bitburner.darknetserverdata.passwordhintdata.md"] = nsDoc_bitburner_darknetserverdata_passwordhintdata_md;
-AllPages["nsDoc/bitburner.darknetserverdata.purchasedbyplayer.md"] = nsDoc_bitburner_darknetserverdata_purchasedbyplayer_md;
-AllPages["nsDoc/bitburner.darknetserverdata.ramused.md"] = nsDoc_bitburner_darknetserverdata_ramused_md;
-AllPages["nsDoc/bitburner.darknetserverdata.requiredcharismaskill.md"] = nsDoc_bitburner_darknetserverdata_requiredcharismaskill_md;
-AllPages["nsDoc/bitburner.darknetserverdata.staticpasswordhint.md"] = nsDoc_bitburner_darknetserverdata_staticpasswordhint_md;
+AllPages["nsDoc/bitburner.darknetserverdetails.blockedram.md"] = nsDoc_bitburner_darknetserverdetails_blockedram_md;
+AllPages["nsDoc/bitburner.darknetserverdetails.data.md"] = nsDoc_bitburner_darknetserverdetails_data_md;
+AllPages["nsDoc/bitburner.darknetserverdetails.depth.md"] = nsDoc_bitburner_darknetserverdetails_depth_md;
+AllPages["nsDoc/bitburner.darknetserverdetails.difficulty.md"] = nsDoc_bitburner_darknetserverdetails_difficulty_md;
+AllPages["nsDoc/bitburner.darknetserverdetails.hassession.md"] = nsDoc_bitburner_darknetserverdetails_hassession_md;
+AllPages["nsDoc/bitburner.darknetserverdetails.isconnectedtocurrentserver.md"] = nsDoc_bitburner_darknetserverdetails_isconnectedtocurrentserver_md;
+AllPages["nsDoc/bitburner.darknetserverdetails.isstationary.md"] = nsDoc_bitburner_darknetserverdetails_isstationary_md;
+AllPages["nsDoc/bitburner.darknetserverdetails.logtrafficinterval.md"] = nsDoc_bitburner_darknetserverdetails_logtrafficinterval_md;
+AllPages["nsDoc/bitburner.darknetserverdetails.md"] = nsDoc_bitburner_darknetserverdetails_md;
+AllPages["nsDoc/bitburner.darknetserverdetails.modelid.md"] = nsDoc_bitburner_darknetserverdetails_modelid_md;
+AllPages["nsDoc/bitburner.darknetserverdetails.passwordformat.md"] = nsDoc_bitburner_darknetserverdetails_passwordformat_md;
+AllPages["nsDoc/bitburner.darknetserverdetails.passwordhint.md"] = nsDoc_bitburner_darknetserverdetails_passwordhint_md;
+AllPages["nsDoc/bitburner.darknetserverdetails.passwordlength.md"] = nsDoc_bitburner_darknetserverdetails_passwordlength_md;
+AllPages["nsDoc/bitburner.darknetserverdetails.requiredcharismaskill.md"] = nsDoc_bitburner_darknetserverdetails_requiredcharismaskill_md;
AllPages["nsDoc/bitburner.division.awareness.md"] = nsDoc_bitburner_division_awareness_md;
AllPages["nsDoc/bitburner.division.cities.md"] = nsDoc_bitburner_division_cities_md;
AllPages["nsDoc/bitburner.division.industry.md"] = nsDoc_bitburner_division_industry_md;
@@ -2896,15 +2875,6 @@ AllPages["nsDoc/bitburner.server.servergrowth.md"] = nsDoc_bitburner_server_serv
AllPages["nsDoc/bitburner.server.smtpportopen.md"] = nsDoc_bitburner_server_smtpportopen_md;
AllPages["nsDoc/bitburner.server.sqlportopen.md"] = nsDoc_bitburner_server_sqlportopen_md;
AllPages["nsDoc/bitburner.server.sshportopen.md"] = nsDoc_bitburner_server_sshportopen_md;
-AllPages["nsDoc/bitburner.serverauthdetails.data.md"] = nsDoc_bitburner_serverauthdetails_data_md;
-AllPages["nsDoc/bitburner.serverauthdetails.hassession.md"] = nsDoc_bitburner_serverauthdetails_hassession_md;
-AllPages["nsDoc/bitburner.serverauthdetails.isconnectedtocurrentserver.md"] = nsDoc_bitburner_serverauthdetails_isconnectedtocurrentserver_md;
-AllPages["nsDoc/bitburner.serverauthdetails.logtrafficinterval.md"] = nsDoc_bitburner_serverauthdetails_logtrafficinterval_md;
-AllPages["nsDoc/bitburner.serverauthdetails.md"] = nsDoc_bitburner_serverauthdetails_md;
-AllPages["nsDoc/bitburner.serverauthdetails.modelid.md"] = nsDoc_bitburner_serverauthdetails_modelid_md;
-AllPages["nsDoc/bitburner.serverauthdetails.passwordformat.md"] = nsDoc_bitburner_serverauthdetails_passwordformat_md;
-AllPages["nsDoc/bitburner.serverauthdetails.passwordhint.md"] = nsDoc_bitburner_serverauthdetails_passwordhint_md;
-AllPages["nsDoc/bitburner.serverauthdetails.passwordlength.md"] = nsDoc_bitburner_serverauthdetails_passwordlength_md;
AllPages["nsDoc/bitburner.simpleopponentstats.md"] = nsDoc_bitburner_simpleopponentstats_md;
AllPages["nsDoc/bitburner.singularity.applytocompany.md"] = nsDoc_bitburner_singularity_applytocompany_md;
AllPages["nsDoc/bitburner.singularity.b1tflum3.md"] = nsDoc_bitburner_singularity_b1tflum3_md;
diff --git a/src/Faction/FactionInfo.tsx b/src/Faction/FactionInfo.tsx
index 75372fb5b..d45ed3ab2 100644
--- a/src/Faction/FactionInfo.tsx
+++ b/src/Faction/FactionInfo.tsx
@@ -40,6 +40,8 @@ import { CONSTANTS } from "../Constants";
import { BladeburnerConstants } from "../Bladeburner/data/Constants";
import type { PlayerObject } from "../PersonObjects/Player/PlayerObject";
import { CovenantCampaign } from "./ui/CovenantCampaign";
+import { GangCampaign } from "./ui/GangCampaign";
+import { GangConstants } from "../Gang/data/Constants";
interface FactionInfoParams {
infoText?: JSX.Element;
@@ -812,3 +814,7 @@ export const FactionInfos: Record = {
},
}),
};
+
+for (const factionName of GangConstants.Names) {
+ FactionInfos[factionName].campaign = () => ;
+}
diff --git a/src/Faction/ui/CreateGangModal.tsx b/src/Faction/ui/CreateGangModal.tsx
index 36c1d3ed7..0d87aa5fd 100644
--- a/src/Faction/ui/CreateGangModal.tsx
+++ b/src/Faction/ui/CreateGangModal.tsx
@@ -7,6 +7,8 @@ import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import { KEY } from "../../utils/KeyboardEventKey";
import { FactionName } from "@enums";
+import { canCreateGang } from "../../Gang/helpers";
+import { dialogBoxCreate } from "../../ui/React/DialogBox";
interface IProps {
open: boolean;
@@ -33,6 +35,11 @@ export function CreateGangModal(props: IProps): React.ReactElement {
}
function createGang(): void {
+ const checkResult = canCreateGang(props.facName);
+ if (!checkResult.success) {
+ dialogBoxCreate(checkResult.message);
+ return;
+ }
Player.startGang(props.facName, isHacking());
props.onClose();
Router.toPage(Page.Gang);
diff --git a/src/Faction/ui/FactionRoot.tsx b/src/Faction/ui/FactionRoot.tsx
index bcb2a607a..56b6827f1 100644
--- a/src/Faction/ui/FactionRoot.tsx
+++ b/src/Faction/ui/FactionRoot.tsx
@@ -17,7 +17,6 @@ import { Player } from "@player";
import { Typography, Button } from "@mui/material";
import { FactionWorkType } from "@enums";
-import { GangButton } from "./GangButton";
import { FactionWork } from "../../Work/FactionWork";
import { useCycleRerender } from "../../ui/React/hooks";
import { favorNeededToDonate } from "../formulas/donation";
@@ -111,7 +110,6 @@ function MainPage({ faction, rerender, onAugmentations }: IMainProps): React.Rea
{faction.name}
-
{!isPlayersGang && (
<>
{factionInfo.offersWork() && (
diff --git a/src/Faction/ui/GangButton.tsx b/src/Faction/ui/GangButton.tsx
deleted file mode 100644
index 0f08a5df8..000000000
--- a/src/Faction/ui/GangButton.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { Button, Typography, Box, Paper, Tooltip } from "@mui/material";
-import React, { useState } from "react";
-import { GangConstants } from "../../Gang/data/Constants";
-import { Router } from "../../ui/GameRoot";
-import { Page } from "../../ui/Router";
-import { Player } from "@player";
-import { Faction } from "../Faction";
-import { CreateGangModal } from "./CreateGangModal";
-
-interface IProps {
- faction: Faction;
-}
-
-export function GangButton({ faction }: IProps): React.ReactElement {
- const [gangOpen, setGangOpen] = useState(false);
-
- if (
- !GangConstants.Names.includes(faction.name) || // not even a gang
- !Player.isAwareOfGang() || // doesn't know about gang
- (Player.gang && Player.getGangName() !== faction.name) // already in another gang
- ) {
- return <>>;
- }
-
- let data = {
- enabled: false,
- title: "",
- tooltip: "" as string | React.ReactElement,
- description: "",
- };
-
- if (Player.gang) {
- data = {
- enabled: true,
- title: "Manage Gang",
- tooltip: "",
- description: "Manage a gang for this Faction. Gangs will earn you money and faction reputation",
- };
- } else {
- const checkResult = Player.canAccessGang();
- data = {
- enabled: checkResult.success,
- title: "Create Gang",
- tooltip: !checkResult.success ? (
- Unlocked when reaching {GangConstants.GangKarmaRequirement} karma
- ) : (
- ""
- ),
- description: "Create a gang for this Faction. Gangs will earn you money and faction reputation",
- };
- }
-
- const manageGang = (): void => {
- // If player already has a gang, just go to the gang UI
- if (Player.inGang()) {
- return Router.toPage(Page.Gang);
- }
-
- setGangOpen(true);
- };
-
- return (
- <>
-
-
-
-
-
-
-
- {data.description}
-
-
-
- setGangOpen(false)} />
- >
- );
-}
diff --git a/src/Faction/ui/GangCampaign.tsx b/src/Faction/ui/GangCampaign.tsx
new file mode 100644
index 000000000..236fcd3c3
--- /dev/null
+++ b/src/Faction/ui/GangCampaign.tsx
@@ -0,0 +1,110 @@
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import Paper from "@mui/material/Paper";
+import Tooltip from "@mui/material/Tooltip";
+import Typography from "@mui/material/Typography";
+import { Player } from "@player";
+import React, { useState } from "react";
+import { knowAboutBitverse } from "../../BitNode/BitNodeUtils";
+import { GangConstants } from "../../Gang/data/Constants";
+import { Router } from "../../ui/GameRoot";
+import { Modal } from "../../ui/React/Modal";
+import { Page } from "../../ui/Router";
+import { FactionName } from "../Enums";
+import { CreateGangModal } from "./CreateGangModal";
+import { Option } from "./Option";
+
+function GangIncompleteCampaign() {
+ const [open, setOpen] = useState(false);
+ return (
+ <>
+
+ setOpen(false)}>
+
+ Each time you attempt to execute the plan, it is abruptly interrupted for reasons no one can explain. You
+ receive the same distorted message every time:
+
+
+ #@)($*&@__Y0U__^%$#@&*()__HAV3__(&@#*$%(@
+
+ ()@#*$%(__N0T__@&$#)@*(__S33N__)(*@#&$)(
+
+ @&*($#@&__TH3__#@A@*)(@$#@)*
+
+ %$#@&()@__TRU1H__()*@#$&()@#$
+
+
+ >
+ );
+}
+
+export function GangCampaign({ factionName }: { factionName: FactionName }) {
+ const [gangOpen, setGangOpen] = useState(false);
+
+ if (!GangConstants.Names.includes(factionName)) {
+ throw new Error(`Cannot create gang with ${factionName}`);
+ }
+ if (!knowAboutBitverse()) {
+ return ;
+ }
+
+ const data = {
+ enabled: false,
+ title: "",
+ tooltip: "" as string | React.ReactElement,
+ description: "",
+ };
+
+ if (Player.gang) {
+ if (Player.getGangName() !== factionName) {
+ data.enabled = false;
+ data.title = "Create Gang";
+ data.tooltip = "You already created a gang with another faction";
+ } else {
+ data.enabled = true;
+ data.title = "Manage Gang";
+ data.description = "Manage a gang for this Faction. Gangs will earn you money and faction reputation";
+ }
+ } else {
+ const checkResult = Player.canAccessGang();
+ data.enabled = checkResult.success;
+ data.title = "Create Gang";
+ data.tooltip = !checkResult.success ? checkResult.message : "";
+ data.description = "Create a gang for this Faction. Gangs will earn you money and faction reputation";
+ }
+
+ const manageGang = (): void => {
+ // If player already has a gang, just go to the gang UI
+ if (Player.inGang()) {
+ return Router.toPage(Page.Gang);
+ }
+
+ setGangOpen(true);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {data.description}
+
+
+
+ setGangOpen(false)} />
+ >
+ );
+}
diff --git a/src/Gang/data/Constants.ts b/src/Gang/data/Constants.ts
index 6c9e7e9cc..3e97db7c8 100644
--- a/src/Gang/data/Constants.ts
+++ b/src/Gang/data/Constants.ts
@@ -23,7 +23,7 @@ export const GangConstants = {
FactionName.SpeakersForTheDead,
FactionName.NiteSec,
FactionName.TheBlackHand,
- ] as string[],
+ ] as FactionName[],
GangKarmaRequirement: -54000,
/** Normal number of game cycles processed at once (2 seconds) */
minCyclesToProcess: 2000 / CONSTANTS.MilliPerCycle,
diff --git a/src/Gang/helpers.ts b/src/Gang/helpers.ts
new file mode 100644
index 000000000..769c5e73b
--- /dev/null
+++ b/src/Gang/helpers.ts
@@ -0,0 +1,26 @@
+import { Result } from "@nsdefs";
+import { Player } from "@player";
+import { FactionName } from "../Enums";
+import { GangConstants } from "./data/Constants";
+
+export function canCreateGang(faction: FactionName): Result {
+ if (Player.gang) {
+ return { success: false, message: "You already have a gang." };
+ }
+ const checkResult = Player.canAccessGang();
+ if (!checkResult.success) {
+ return { success: false, message: checkResult.message };
+ }
+ if (!GangConstants.Names.includes(faction)) {
+ return {
+ success: false,
+ message: `${faction} does not allow creating a gang. You can only do that with ${GangConstants.Names.join(
+ ", ",
+ )}.`,
+ };
+ }
+ if (!Player.factions.includes(faction)) {
+ return { success: false, message: `You are not a member of ${faction}.` };
+ }
+ return { success: true };
+}
diff --git a/src/Hacknet/data/HashUpgradesMetadata.tsx b/src/Hacknet/data/HashUpgradesMetadata.tsx
index a57db679a..8bf802f3d 100644
--- a/src/Hacknet/data/HashUpgradesMetadata.tsx
+++ b/src/Hacknet/data/HashUpgradesMetadata.tsx
@@ -106,7 +106,7 @@ export const HashUpgradesMetadata: HashUpgradeParams[] = [
value: 10,
},
{
- costPerLevel: 200,
+ costPerLevel: 25,
desc: "Generate a random Coding Contract somewhere on the network",
name: HashUpgradeEnum.GenerateCodingContract,
effectText: (level: number): JSX.Element | null => <>Generated {level} contracts.>,
diff --git a/src/Infiltration/utils.ts b/src/Infiltration/utils.ts
index 81a33251b..77c647bca 100644
--- a/src/Infiltration/utils.ts
+++ b/src/Infiltration/utils.ts
@@ -14,15 +14,19 @@ export function getArrow(event: KeyboardLikeEvent): Arrow | undefined {
switch (event.key) {
case KEY.UP_ARROW:
case KEY.W:
+ case KEY.K:
return upArrowSymbol;
case KEY.LEFT_ARROW:
case KEY.A:
+ case KEY.H:
return leftArrowSymbol;
case KEY.DOWN_ARROW:
case KEY.S:
+ case KEY.J:
return downArrowSymbol;
case KEY.RIGHT_ARROW:
case KEY.D:
+ case KEY.L:
return rightArrowSymbol;
}
}
diff --git a/src/Netscript/RamCostGenerator.ts b/src/Netscript/RamCostGenerator.ts
index 3e1b135c7..eab73b952 100644
--- a/src/Netscript/RamCostGenerator.ts
+++ b/src/Netscript/RamCostGenerator.ts
@@ -42,7 +42,6 @@ export const RamCostConstants = {
GetStock: 2.0,
BuySellStock: 2.5,
Round: 0.05,
- ReadWrite: 1.0,
ArbScript: 1.0,
GetScript: 0.1,
GetRunningScript: 0.3,
@@ -245,7 +244,7 @@ const dnet = {
getStasisLinkLimit: 0,
getStasisLinkedServers: 0,
getServer: 2,
- getServerAuthDetails: RamCostConstants.GetServer,
+ getServerDetails: RamCostConstants.GetServer,
induceServerMigration: 4,
unleashStormSeed: 0.1,
isDarknetServer: RamCostConstants.GetServer,
@@ -631,7 +630,7 @@ export const RamCosts: RamCostTree = {
nextPortWrite: 0,
readPort: 0,
getPortHandle: 0,
- rm: RamCostConstants.ReadWrite,
+ rm: RamCostConstants.Scp,
scriptRunning: RamCostConstants.ArbScript,
scriptKill: RamCostConstants.ArbScript,
getScriptName: 0,
diff --git a/src/Netscript/TypeAssertion.ts b/src/Netscript/TypeAssertion.ts
index 5d6cf36eb..27ef6fb2a 100644
--- a/src/Netscript/TypeAssertion.ts
+++ b/src/Netscript/TypeAssertion.ts
@@ -1,7 +1,7 @@
-import { exampleDarknetServerData } from "../DarkNet/Enums";
-import type { DarknetServerData } from "@nsdefs";
+import { exampleDarknetServerDetails } from "../DarkNet/Enums";
import type { NetscriptContext } from "./APIWrapper";
import { errorMessage } from "./ErrorMessages";
+import type { DarknetServerDetails } from "@nsdefs";
const clip = (s: string): string => {
if (s.length > 15) {
@@ -68,8 +68,8 @@ export function missingKey(expect: object, actual: unknown): string | false {
return false;
}
-export function assertDarknetServerData(ctx: NetscriptContext, data: unknown): asserts data is DarknetServerData {
- const error = missingKey(exampleDarknetServerData, data);
+export function assertDarknetServerDetails(ctx: NetscriptContext, data: unknown): asserts data is DarknetServerDetails {
+ const error = missingKey(exampleDarknetServerDetails, data);
if (error) {
throw errorMessage(ctx, `Invalid darknet server data.\n${error}`, "TYPE");
}
diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts
index 331e0a837..b9cdb0ae6 100644
--- a/src/NetscriptFunctions.ts
+++ b/src/NetscriptFunctions.ts
@@ -74,7 +74,7 @@ import { NetscriptCorporation } from "./NetscriptFunctions/Corporation";
import { NetscriptFormulas } from "./NetscriptFunctions/Formulas";
import { NetscriptStockMarket } from "./NetscriptFunctions/StockMarket";
import { NetscriptGrafting } from "./NetscriptFunctions/Grafting";
-import type { NS, RecentScript, ProcessInfo, NSEnums, Server as NSInterfaceServer, DarknetServerData } from "@nsdefs";
+import type { NS, RecentScript, ProcessInfo, NSEnums, Server as NSInterfaceServer } from "@nsdefs";
import { NetscriptSingularity } from "./NetscriptFunctions/Singularity";
import { NetscriptCloud } from "./NetscriptFunctions/Cloud";
@@ -96,13 +96,13 @@ import { hasScriptExtension } from "./Paths/ScriptFilePath";
import { hasTextExtension } from "./Paths/TextFilePath";
import { ContentFilePath } from "./Paths/ContentFile";
import { hasContractExtension } from "./Paths/ContractFilePath";
-import { getRamCost } from "./Netscript/RamCostGenerator";
+import { getRamCost, RamCostConstants } from "./Netscript/RamCostGenerator";
import { getEnumHelper } from "./utils/EnumHelper";
import { ServerConstants } from "./Server/data/Constants";
import { assertFunctionWithNSContext } from "./Netscript/TypeAssertion";
import { Router } from "./ui/GameRoot";
import { Page } from "./ui/Router";
-import { NetscriptDarknet } from "./NetscriptFunctions/Darknet";
+import { getDarknetPropertiesForDeprecationSupport, NetscriptDarknet } from "./NetscriptFunctions/Darknet";
import { canAccessBitNodeFeature } from "./BitNode/BitNodeUtils";
import { validBitNodes } from "./BitNode/Constants";
import { isIPAddress } from "./Types/strings";
@@ -116,6 +116,7 @@ import { exampleDarknetServerData, ResponseCodeEnum } from "./DarkNet/Enums";
import { renderToStaticMarkup } from "react-dom/server";
import { Literatures } from "./Literature/Literatures";
import { Messages } from "./Message/MessageHelpers";
+import { setDeprecatedProperties } from "./utils/DeprecationHelper";
export const enums: NSEnums = {
CityName,
@@ -929,42 +930,22 @@ export const ns: InternalAPI = {
if (!server) {
// If the server is offline, return a dummy object with isOnline = false.
const isIp = isIPAddress(host);
- return {
- isOnline: false,
+ const result = {
+ sshPortOpen: false,
+ ftpPortOpen: false,
+ smtpPortOpen: false,
+ httpPortOpen: false,
+ sqlPortOpen: false,
+ organizationName: "",
...exampleDarknetServerData,
hostname: isIp ? "" : host,
ip: isIp ? host : "",
- } satisfies DarknetServerData & { isOnline: boolean };
+ isOnline: false,
+ };
+ setDeprecatedProperties(result, getDarknetPropertiesForDeprecationSupport(result));
+ return result satisfies NSInterfaceServer & { isOnline: boolean };
}
- if (server instanceof DarknetServer) {
- return {
- isOnline: true,
- hostname: server.hostname,
- ip: server.ip,
- hasAdminRights: server.hasAdminRights,
- isConnectedTo: server.isConnectedTo,
- cpuCores: server.cpuCores,
- ramUsed: server.ramUsed,
- maxRam: server.maxRam,
- backdoorInstalled: server.backdoorInstalled,
- depth: server.depth,
- modelId: server.modelId,
- hasStasisLink: server.hasStasisLink,
- blockedRam: server.blockedRam,
- staticPasswordHint: server.staticPasswordHint,
- passwordHintData: server.passwordHintData,
- difficulty: server.difficulty,
- requiredCharismaSkill: server.requiredCharismaSkill,
- logTrafficInterval: server.logTrafficInterval,
- isStationary: server.isStationary,
- purchasedByPlayer: false,
- } satisfies DarknetServerData & { isOnline: boolean };
- }
- // Throw if it's an isolated non-dnet server (e.g., pre-TOR darkweb, pre-TRP WD).
- if (server.serversOnNetwork.length === 0) {
- throw helpers.errorMessage(ctx, `Server ${host} does not exist.`);
- }
- return {
+ const result = {
hostname: server.hostname,
ip: server.ip,
sshPortOpen: server.sshPortOpen,
@@ -990,6 +971,20 @@ export const ns: InternalAPI = {
requiredHackingSkill: server.requiredHackingSkill,
serverGrowth: server.serverGrowth,
} satisfies NSInterfaceServer;
+
+ if (server instanceof DarknetServer) {
+ const resultWithAdditionalProps = {
+ ...result,
+ isOnline: true,
+ };
+ setDeprecatedProperties(resultWithAdditionalProps, getDarknetPropertiesForDeprecationSupport(server));
+ return resultWithAdditionalProps satisfies NSInterfaceServer & { isOnline: boolean };
+ }
+ // Throw if it's an isolated non-dnet server (e.g., pre-TOR darkweb, pre-TRP WD).
+ if (server.serversOnNetwork.length === 0) {
+ throw helpers.errorMessage(ctx, `Server ${host} does not exist.`);
+ }
+ return result;
},
getServerMoneyAvailable: (ctx) => (_host?) => {
const server = helpers.getNormalServer(ctx, _host);
@@ -1505,6 +1500,9 @@ export const ns: InternalAPI = {
}),
getFunctionRamCost: (ctx) => (_name) => {
const name = helpers.string(ctx, "name", _name);
+ if (name === "baseCost") {
+ return RamCostConstants.Base;
+ }
return getRamCost(name.split("."), true);
},
tprintRaw: () => (value) => {
diff --git a/src/NetscriptFunctions/Darknet.ts b/src/NetscriptFunctions/Darknet.ts
index d28e0411f..28bcf7f67 100644
--- a/src/NetscriptFunctions/Darknet.ts
+++ b/src/NetscriptFunctions/Darknet.ts
@@ -38,7 +38,7 @@ import {
logger,
} from "../DarkNet/effects/offlineServerHandling";
import { DarknetServer } from "../Server/DarknetServer";
-import { GenericResponseMessage, ResponseCodeEnum } from "../DarkNet/Enums";
+import { exampleDarknetServerDetails, GenericResponseMessage, ResponseCodeEnum } from "../DarkNet/Enums";
import { getRewardFromCache } from "../DarkNet/effects/cacheFiles";
import { CONSTANTS } from "../Constants";
import { getStasisLinkServers } from "../DarkNet/utils/darknetNetworkUtils";
@@ -46,8 +46,9 @@ import { resolveCacheFilePath } from "../Paths/CacheFilePath";
import type { CacheResult } from "@nsdefs";
import { MAX_PASSWORD_LENGTH } from "../DarkNet/Constants";
import { isIPAddress } from "../Types/strings";
-import { getDarknetServerOrThrow } from "../DarkNet/utils/darknetServerUtils";
+import { type DarknetServerData, getDarknetServerOrThrow } from "../DarkNet/utils/darknetServerUtils";
import { shuffle } from "lodash";
+import { getSharedChars } from "../DarkNet/utils/darknetAuthUtils";
type CompleteHeartbleedOptions = {
peek: boolean;
@@ -121,7 +122,8 @@ export function NetscriptDarknet(): InternalAPI {
const server = serverCheck.server;
const threads = ctx.workerScript.scriptRef.threads;
- const networkDelay = calculateAuthenticationTime(server, Player, threads, password) + additionalMsec;
+ const sharedChars = getSharedChars(server.password, password);
+ const networkDelay = calculateAuthenticationTime(server, Player, threads, sharedChars) + additionalMsec;
logger(ctx)(
`Connecting to ${server.hostname} with password '${password}'... (Est: ${formatNumber(
@@ -377,22 +379,15 @@ export function NetscriptDarknet(): InternalAPI {
logger(ctx)(`Stasis linked servers: ${serverNames}`);
return serverNames;
},
- getServerAuthDetails: (ctx) => (_host) => {
+ getServerDetails: (ctx) => (_host) => {
const targetHost = helpers.string(ctx, "host", _host ?? ctx.workerScript.hostname);
const serverCheck = checkDarknetServer(ctx, targetHost);
if (!serverCheck.success) {
logger(ctx)(serverCheck.message);
return {
+ ...exampleDarknetServerDetails,
isOnline: false,
- isConnectedToCurrentServer: false,
- hasSession: false,
- modelId: "",
- passwordHint: "",
- data: "",
- logTrafficInterval: -1,
- passwordLength: -1,
- passwordFormat: "numeric",
- } satisfies ReturnType;
+ } satisfies ReturnType;
}
const targetServer = serverCheck.server;
const localServer = ctx.workerScript.getServer();
@@ -408,7 +403,12 @@ export function NetscriptDarknet(): InternalAPI {
logTrafficInterval: targetServer.logTrafficInterval,
passwordLength: targetServer.password.length,
passwordFormat: getPasswordType(targetServer.password),
- } satisfies ReturnType;
+ blockedRam: targetServer.blockedRam,
+ difficulty: targetServer.difficulty,
+ requiredCharismaSkill: targetServer.requiredCharismaSkill,
+ depth: targetServer.depth,
+ isStationary: targetServer.isStationary,
+ } satisfies ReturnType;
},
induceServerMigration:
(ctx) =>
@@ -704,3 +704,56 @@ export function NetscriptDarknet(): InternalAPI {
},
};
}
+
+export const getDarknetPropertiesForDeprecationSupport = (dnetServer: DarknetServerData) => ({
+ depth: {
+ identifier: "ns.getServer().depth",
+ message: "Use ns.dnet.getServerDetails().depth instead.",
+ value: dnetServer.depth,
+ },
+ modelId: {
+ identifier: "ns.getServer().modelId",
+ message: "Use ns.dnet.getServerDetails().modelId instead.",
+ value: dnetServer.modelId,
+ },
+ hasStasisLink: {
+ identifier: "ns.getServer().hasStasisLink",
+ message: "Use ns.dnet.getServerDetails().hasStasisLink instead.",
+ value: dnetServer.hasStasisLink,
+ },
+ blockedRam: {
+ identifier: "ns.getServer().blockedRam",
+ message: "Use ns.dnet.getServerDetails().blockedRam instead.",
+ value: dnetServer.blockedRam,
+ },
+ staticPasswordHint: {
+ identifier: "ns.getServer().staticPasswordHint",
+ message: "Use ns.dnet.getServerDetails().staticPasswordHint instead.",
+ value: dnetServer.staticPasswordHint,
+ },
+ passwordHintData: {
+ identifier: "ns.getServer().passwordHintData",
+ message: "Use ns.dnet.getServerDetails().passwordHintData instead.",
+ value: dnetServer.passwordHintData,
+ },
+ difficulty: {
+ identifier: "ns.getServer().difficulty",
+ message: "Use ns.dnet.getServerDetails().difficulty instead.",
+ value: dnetServer.difficulty,
+ },
+ requiredCharismaSkill: {
+ identifier: "ns.getServer().requiredCharismaSkill",
+ message: "Use ns.dnet.getServerDetails().requiredCharismaSkill instead.",
+ value: dnetServer.requiredCharismaSkill,
+ },
+ logTrafficInterval: {
+ identifier: "ns.getServer().logTrafficInterval",
+ message: "Use ns.dnet.getServerDetails().logTrafficInterval instead.",
+ value: dnetServer.logTrafficInterval,
+ },
+ isStationary: {
+ identifier: "ns.getServer().isStationary",
+ message: "Use ns.dnet.getServerDetails().isStationary instead.",
+ value: dnetServer.isStationary,
+ },
+});
diff --git a/src/NetscriptFunctions/Formulas.ts b/src/NetscriptFunctions/Formulas.ts
index a31573a70..65e9be065 100644
--- a/src/NetscriptFunctions/Formulas.ts
+++ b/src/NetscriptFunctions/Formulas.ts
@@ -54,7 +54,7 @@ import type { PositiveNumber } from "../types";
import { Crimes } from "../Crime/Crimes";
import { calculateEffectiveSharedThreads, calculateShareBonus } from "../NetworkShare/Share";
import { calculateAuthenticationTime } from "../DarkNet/effects/effects";
-import { assertDarknetServerData } from "../Netscript/TypeAssertion";
+import { assertDarknetServerDetails } from "../Netscript/TypeAssertion";
import { getRamBlockRemoved } from "../DarkNet/effects/ramblock";
export function NetscriptFormulas(): InternalAPI {
@@ -482,27 +482,28 @@ export function NetscriptFormulas(): InternalAPI {
dnet: {
getAuthenticateTime:
(ctx) =>
- (_darknetServerData, _threads, _player): number => {
- assertDarknetServerData(ctx, _darknetServerData);
+ (_darknetServerDetails, _threads, _player, _correctCharactersInPassword): number => {
+ assertDarknetServerDetails(ctx, _darknetServerDetails);
const threads = helpers.number(ctx, "threads", _threads ?? 1);
const person = helpers.person(ctx, _player ?? Player);
- return calculateAuthenticationTime(_darknetServerData, person, threads);
+ const correctChars = helpers.number(ctx, "correctCharactersInPassword", _correctCharactersInPassword ?? 0);
+ return calculateAuthenticationTime(_darknetServerDetails, person, threads, correctChars);
},
getHeartbleedTime:
(ctx) =>
- (_darknetServerData, _threads, _player): number => {
- assertDarknetServerData(ctx, _darknetServerData);
+ (_darknetServerDetails, _threads, _player): number => {
+ assertDarknetServerDetails(ctx, _darknetServerDetails);
const threads = helpers.number(ctx, "threads", _threads ?? 1);
const person = helpers.person(ctx, _player ?? Player);
- return calculateAuthenticationTime(_darknetServerData, person, threads) * 1.5;
+ return calculateAuthenticationTime(_darknetServerDetails, person, threads) * 1.5;
},
getExpectedRamBlockRemoved:
(ctx) =>
- (_darknetServerData, _threads, _person): number => {
- assertDarknetServerData(ctx, _darknetServerData);
+ (_darknetServerDetails, _threads, _person): number => {
+ assertDarknetServerDetails(ctx, _darknetServerDetails);
const threads = helpers.number(ctx, "threads", _threads ?? 1);
const person = helpers.person(ctx, _person ?? Player);
- return getRamBlockRemoved(_darknetServerData, threads, person);
+ return getRamBlockRemoved(_darknetServerDetails, threads, person);
},
},
};
diff --git a/src/NetscriptFunctions/Gang.ts b/src/NetscriptFunctions/Gang.ts
index 935a498eb..79c3bba0c 100644
--- a/src/NetscriptFunctions/Gang.ts
+++ b/src/NetscriptFunctions/Gang.ts
@@ -7,13 +7,13 @@ import { type InternalAPI, type NetscriptContext, setRemovedFunctions } from "..
import { GangPromise, RecruitmentResult } from "../Gang/Gang";
import { Player } from "@player";
import { FactionName } from "@enums";
-import { GangConstants } from "../Gang/data/Constants";
import { AllGangs } from "../Gang/AllGangs";
import { GangMemberTasks } from "../Gang/GangMemberTasks";
import { GangMemberUpgrades } from "../Gang/GangMemberUpgrades";
import { helpers } from "../Netscript/NetscriptHelpers";
import { getEnumHelper } from "../utils/EnumHelper";
import { CONSTANTS } from "../Constants";
+import { canCreateGang } from "../Gang/helpers";
export function NetscriptGang(): InternalAPI {
/** Functions as an API check and also returns the gang object */
@@ -40,26 +40,11 @@ export function NetscriptGang(): InternalAPI {
const gangFunctions: InternalAPI = {
createGang: (ctx) => (_faction) => {
const faction = getEnumHelper("FactionName").nsGetMember(ctx, _faction);
- if (Player.gang) {
- return false;
- }
- const checkResult = Player.canAccessGang();
+ const checkResult = canCreateGang(faction);
if (!checkResult.success) {
helpers.log(ctx, () => checkResult.message);
return false;
}
- if (!GangConstants.Names.includes(faction)) {
- helpers.log(
- ctx,
- () =>
- `${faction} does not allow creating a gang. You can only do that with ${GangConstants.Names.join(", ")}.`,
- );
- return false;
- }
- if (!Player.factions.includes(faction)) {
- helpers.log(ctx, () => `You are not a member of ${faction}.`);
- return false;
- }
const isHacking = faction === FactionName.NiteSec || faction === FactionName.TheBlackHand;
Player.startGang(faction, isHacking);
diff --git a/src/NetscriptFunctions/Grafting.ts b/src/NetscriptFunctions/Grafting.ts
index 22d8a0853..94862db15 100644
--- a/src/NetscriptFunctions/Grafting.ts
+++ b/src/NetscriptFunctions/Grafting.ts
@@ -89,7 +89,6 @@ export function NetscriptGrafting(): InternalAPI {
Player.startFocusing();
Router.toPage(Page.Work);
} else if (wasFocusing) {
- Player.stopFocusing();
Router.toPage(Page.Terminal);
}
diff --git a/src/NetscriptFunctions/Singularity.ts b/src/NetscriptFunctions/Singularity.ts
index 06cb750ea..3967cb8f2 100644
--- a/src/NetscriptFunctions/Singularity.ts
+++ b/src/NetscriptFunctions/Singularity.ts
@@ -5,7 +5,7 @@ import { CityName, CompletedProgramName, FactionWorkType, LocationName } from "@
import { purchaseAugmentation, joinFaction, getFactionAugmentationsFiltered } from "../Faction/FactionHelpers";
import { startWorkerScript } from "../NetscriptWorker";
import { Augmentations } from "../Augmentation/Augmentations";
-import { getAugCost, installAugmentations } from "../Augmentation/AugmentationHelpers";
+import { getAugCost, installAugmentations, soaAugmentationNames } from "../Augmentation/AugmentationHelpers";
import { CONSTANTS } from "../Constants";
import { RunningScript } from "../Script/RunningScript";
import { calculateAchievements } from "../Achievements/Achievements";
@@ -141,6 +141,11 @@ export function NetscriptSingularity(): InternalAPI {
helpers.checkSingularityAccess(ctx);
const augName = getEnumHelper("AugmentationName").nsGetMember(ctx, _augName);
const aug = Augmentations[augName];
+ // SoA augmentations don't use the bitnode AugmentationMoneyCost multiplier;
+ // their cost only scales with the number of SoA augs already owned.
+ if (soaAugmentationNames.includes(augName)) {
+ return aug.baseCost;
+ }
return aug.baseCost * currentNodeMults.AugmentationMoneyCost;
},
getAugmentationPrice: (ctx) => (_augName) => {
@@ -283,7 +288,6 @@ export function NetscriptSingularity(): InternalAPI {
Player.startFocusing();
Router.toPage(Page.Work);
} else if (wasFocusing) {
- Player.stopFocusing();
Router.toPage(Page.Terminal);
}
helpers.log(ctx, () => `Started ${classType} at ${universityName}`);
@@ -365,7 +369,6 @@ export function NetscriptSingularity(): InternalAPI {
Player.startFocusing();
Router.toPage(Page.Work);
} else if (wasFocusing) {
- Player.stopFocusing();
Router.toPage(Page.Terminal);
}
helpers.log(ctx, () => `Started training ${classType} at ${gymName}`);
@@ -570,7 +573,6 @@ export function NetscriptSingularity(): InternalAPI {
Router.toPage(Page.Work);
return true;
} else if (Player.focus && !focus) {
- Player.stopFocusing();
Router.toPage(Page.Terminal);
return true;
}
@@ -709,7 +711,6 @@ export function NetscriptSingularity(): InternalAPI {
Player.startFocusing();
Router.toPage(Page.Work);
} else if (wasFocused) {
- Player.stopFocusing();
Router.toPage(Page.Terminal);
}
helpers.log(ctx, () => `Began working at '${companyName}' with position '${jobName}'`);
@@ -834,7 +835,6 @@ export function NetscriptSingularity(): InternalAPI {
Player.startFocusing();
Router.toPage(Page.Work);
} else if (wasFocusing) {
- Player.stopFocusing();
Router.toPage(Page.Terminal);
}
helpers.log(ctx, () => `Started carrying out hacking contracts for '${faction.name}'`);
@@ -855,7 +855,6 @@ export function NetscriptSingularity(): InternalAPI {
Player.startFocusing();
Router.toPage(Page.Work);
} else if (wasFocusing) {
- Player.stopFocusing();
Router.toPage(Page.Terminal);
}
helpers.log(ctx, () => `Started carrying out field missions for '${faction.name}'`);
@@ -876,7 +875,6 @@ export function NetscriptSingularity(): InternalAPI {
Player.startFocusing();
Router.toPage(Page.Work);
} else if (wasFocusing) {
- Player.stopFocusing();
Router.toPage(Page.Terminal);
}
helpers.log(ctx, () => `Started carrying out security work for '${faction.name}'`);
@@ -1009,7 +1007,6 @@ export function NetscriptSingularity(): InternalAPI {
Player.startFocusing();
Router.toPage(Page.Work);
} else if (wasFocusing) {
- Player.stopFocusing();
Router.toPage(Page.Terminal);
}
helpers.log(ctx, () => `Began creating program: '${programName}'`);
@@ -1059,7 +1056,6 @@ export function NetscriptSingularity(): InternalAPI {
Player.startFocusing();
Router.toPage(Page.Work);
} else if (wasFocusing) {
- Player.stopFocusing();
Router.toPage(Page.Terminal);
}
return crimeTime;
diff --git a/src/NetscriptFunctions/StockMarket.ts b/src/NetscriptFunctions/StockMarket.ts
index 3851ccca2..ace7a8889 100644
--- a/src/NetscriptFunctions/StockMarket.ts
+++ b/src/NetscriptFunctions/StockMarket.ts
@@ -26,6 +26,15 @@ import { getEnumHelper } from "../utils/EnumHelper";
import { CONSTANTS } from "../Constants";
import { getDarknetVolatilityMult } from "../DarkNet/effects/effects";
+export const getStockFromSymbol = function (ctx: NetscriptContext, symbol: string): Stock {
+ const stock = SymbolToStockMap[symbol];
+ if (stock == null) {
+ throw helpers.errorMessage(ctx, `Invalid stock symbol: '${symbol}'`);
+ }
+
+ return stock;
+};
+
export function NetscriptStockMarket(): InternalAPI {
/** Checks if the player has TIX API access. Throws an error if the player does not */
const checkTixApiAccess = function (ctx: NetscriptContext): void {
@@ -34,15 +43,6 @@ export function NetscriptStockMarket(): InternalAPI {
}
};
- const getStockFromSymbol = function (ctx: NetscriptContext, symbol: string): Stock {
- const stock = SymbolToStockMap[symbol];
- if (stock == null) {
- throw helpers.errorMessage(ctx, `Invalid stock symbol: '${symbol}'`);
- }
-
- return stock;
- };
-
const stockFunctions: InternalAPI = {
getConstants: () => () => structuredClone(StockMarketConstants),
hasWseAccount: () => () => Player.hasWseAccount,
@@ -353,12 +353,3 @@ export function NetscriptStockMarket(): InternalAPI {
return stockFunctions;
}
-
-export const getStockFromSymbol = function (ctx: NetscriptContext, symbol: string): Stock {
- const stock = SymbolToStockMap[symbol];
- if (stock == null) {
- throw helpers.errorMessage(ctx, `Invalid stock symbol: '${symbol}'`);
- }
-
- return stock;
-};
diff --git a/src/Paths/Directory.ts b/src/Paths/Directory.ts
index 510f56956..692fe4f38 100644
--- a/src/Paths/Directory.ts
+++ b/src/Paths/Directory.ts
@@ -25,7 +25,7 @@ export const root = "" as Directory;
* #: Invalid because it might have a use in the terminal in the future.
* (quote marks): Invalid to avoid conflict with quote marks used in the terminal.
* (whitespace): Invalid to avoid confusion with terminal command separator */
-const invalidCharacters = ["/", "*", "?", "[", "]", "!", "\\", "~", "|", "#", '"', "'"];
+export const invalidCharacters = ["/", "*", "?", "[", "]", "!", "\\", "~", "|", "#", '"', "'"] as const;
/** A valid character is any character that is not one of the invalid characters */
export const oneValidCharacter = `[^${escapeRegExp(invalidCharacters.join(""))}\\s]`;
diff --git a/src/Paths/TextFilePath.ts b/src/Paths/TextFilePath.ts
index 4127838aa..dbf76a13b 100644
--- a/src/Paths/TextFilePath.ts
+++ b/src/Paths/TextFilePath.ts
@@ -5,9 +5,11 @@ import { FilePath, resolveFilePath } from "./FilePath";
type WithTextExtension = string & { __fileType: "Text" };
export type TextFilePath = FilePath & WithTextExtension;
+export const validTextExtensions = [".txt", ".json", ".css"];
+
/** Check extension only */
export function hasTextExtension(path: string): path is WithTextExtension {
- return path.endsWith(".txt") || path.endsWith(".json") || path.endsWith(".css");
+ return validTextExtensions.some((extension) => path.endsWith(extension));
}
/** Sanitize a player input, resolve any relative paths, and for imports add the correct extension if missing */
diff --git a/src/PersonObjects/Player/PlayerObject.ts b/src/PersonObjects/Player/PlayerObject.ts
index 02e8a968c..f3ac8abf7 100644
--- a/src/PersonObjects/Player/PlayerObject.ts
+++ b/src/PersonObjects/Player/PlayerObject.ts
@@ -212,6 +212,12 @@ export class PlayerObject extends Person implements IPlayer {
delete player.jobs[loadedCompanyName as CompanyName];
}
}
+ // A bug created ill-formed UTF-16 darknet hostnames that caused the in-game editor to crash. Player.currentServer
+ // may point to one of these invalid hostnames. This code migrates the invalid hostnames and protects against
+ // similar issues in the future.
+ if (!player.currentServer.isWellFormed()) {
+ player.currentServer = player.currentServer.toWellFormed();
+ }
return player;
}
}
diff --git a/src/PersonObjects/Player/PlayerObjectGangMethods.ts b/src/PersonObjects/Player/PlayerObjectGangMethods.ts
index 7cdd00086..b60a2ffdf 100644
--- a/src/PersonObjects/Player/PlayerObjectGangMethods.ts
+++ b/src/PersonObjects/Player/PlayerObjectGangMethods.ts
@@ -54,8 +54,9 @@ export function hasGangWith(this: PlayerObject, facName: FactionName): boolean {
}
export function startGang(this: PlayerObject, factionName: FactionName, hacking: boolean): void {
- // isFactionWork handles null internally, finishWork might need to be run with true
- if (isFactionWork(this.currentWork) && this.currentWork.factionName === factionName) this.finishWork(false);
+ if (isFactionWork(this.currentWork) && this.currentWork.factionName === factionName) {
+ this.finishWork(true);
+ }
this.gang = new Gang(factionName, hacking);
diff --git a/src/ScriptEditor/NetscriptDefinitions.d.ts b/src/ScriptEditor/NetscriptDefinitions.d.ts
index ba4640532..25fc865a1 100644
--- a/src/ScriptEditor/NetscriptDefinitions.d.ts
+++ b/src/ScriptEditor/NetscriptDefinitions.d.ts
@@ -2655,9 +2655,10 @@ export interface Singularity {
* @remarks
* RAM cost: 2.5 GB * 16/4/1
*
+ * This excludes the player's price multiplier, but does include the relevant BitNode multiplier (for all augs that aren't part of Shadows of Anarchy, which doesn't use BitNode multipliers).
*
* @param augName - Name of Augmentation.
- * @returns Base price of the augmentation, before price multiplier.
+ * @returns Base price of the augmentation, before the player's price multiplier.
*/
getAugmentationBasePrice(augName: string): number;
@@ -2835,7 +2836,7 @@ export interface Singularity {
* @returns - a list of programs available for purchase on the dark web, or [] if Tor has not
* been purchased
*/
- getDarkwebPrograms(): string[];
+ getDarkwebPrograms(): ProgramName[];
/**
* Check the price of an exploit on the dark web
@@ -4412,54 +4413,6 @@ type DarknetResponseCode = _ValueOf;
/** @public */
export type DarknetResult = { success: boolean; code: DarknetResponseCode; message: string };
-/**
- * Darknet server data.
- * @public
- */
-export interface DarknetServerData {
- /** Hostname. Must be unique */
- hostname: string;
- /** IP Address. Must be unique */
- ip: string;
- /** Flag indicating whether the player has admin/root access to this server */
- hasAdminRights: boolean;
- /** Flag indicating whether the player's terminal is currently connected to this server */
- isConnectedTo: boolean;
- /** Number of CPU cores */
- cpuCores: number;
- /** Used RAM (GB). i.e. unavailable RAM */
- ramUsed: number;
- /** Max RAM (GB) of this server */
- maxRam: number;
- /** Flag indicating whether this server has a backdoor installed by the player */
- backdoorInstalled: boolean;
- /** If the server has a stasis link applied */
- hasStasisLink: boolean;
- /** The amount of ram blocked by the server owner */
- blockedRam: number;
- /**
- * The model of the server. Similar models have similar vulnerabilities. The model list is intentionally undocumented.
- * You are supposed to experiment and discover the models.
- */
- modelId: string;
- /** The generic password prompt for the server */
- staticPasswordHint: string;
- /** Data associated with the password hint */
- passwordHintData: string;
- /** The difficulty rating of the server, associated with its original depth in the net */
- difficulty: number;
- /** The depth of the server in the net */
- depth: number;
- /** The charisma skill required to heartbleed the server */
- requiredCharismaSkill: number;
- /** The interval at which the server periodically adds to its logs, in seconds. */
- logTrafficInterval: number;
- /** If this darknet server cannot be moved. True for fixed/story servers. */
- isStationary: boolean;
- /** Whether this server was purchased by the player. Always false for darknet servers */
- purchasedByPlayer: boolean;
-}
-
/** @public */
export type CacheResult = {
success: boolean;
@@ -4468,15 +4421,18 @@ export type CacheResult = {
};
/**
- * Details about a server's authentication schema
+ * Details about a darknet server
* @public
*/
-interface ServerAuthDetails {
+interface DarknetServerDetails {
/** True if the server is directly connected to the current server */
isConnectedToCurrentServer: boolean;
/** True if the current script has authenticated to this server with the right password using authenticate() or connectToSesssion() */
hasSession: boolean;
- /** The model ID of the server. Similar models share vulnerabilities. */
+ /**
+ * The model of the server. Similar models have similar vulnerabilities. The model list is intentionally undocumented.
+ * You are supposed to experiment and discover the models.
+ */
modelId: string;
/** Static password reminder text set for this server. */
passwordHint: string;
@@ -4488,6 +4444,16 @@ interface ServerAuthDetails {
passwordLength: number;
/** The character set used in the password */
passwordFormat: "numeric" | "alphabetic" | "alphanumeric" | "ASCII" | "unicode";
+ /** The amount of ram blocked by the server owner */
+ blockedRam: number;
+ /** The difficulty rating of the server, associated with its original depth in the net */
+ difficulty: number;
+ /** The current depth in the darknet of the server */
+ depth: number;
+ /** The charisma skill required to authenticate on the server */
+ requiredCharismaSkill: number;
+ /** If this darknet server cannot be moved. True for fixed/story servers. */
+ isStationary: boolean;
}
/**
@@ -4653,15 +4619,18 @@ export interface Darknet {
getStasisLinkedServers(returnByIP?: boolean): string[];
/**
- * Returns the server's authentication protocol details.
+ * Returns the darknet-specific details of the server.
+ *
+ * If the darknet server has recently gone offline, the returned object will be a dummy server object with
+ * `isOnline: false`.
*
* @remarks
* RAM cost: 0.1 GB
*
* @param host - Hostname/IP of the server to analyze. Defaults to the running script's server if not specified.
- * @returns An object containing the server's authentication protocol details.
+ * @returns An object containing the server's darknet-specific details.
*/
- getServerAuthDetails(host?: string): ServerAuthDetails & { isOnline: boolean };
+ getServerDetails(host?: string): DarknetServerDetails & { isOnline: boolean };
/**
* Increases the chance that the target server will move to other parts of the darknet, by overloading the connections between it and the current server.
@@ -6528,26 +6497,32 @@ interface BladeburnerFormulas {
interface DarknetFormulas {
/**
* Gets the time it will take to authenticate a server.
- * @param darknetServerData - The server to check authentication time on.
+ * @param serverDetails - The server to check authentication time on.
* @param threads - The number of threads to use for the authentication. Optional, defaults to 1
* @param player - The player object. Optional, defaults to the current player status
+ * @param correctCharactersInPassword - only used for 2G_cellular model servers. The number of correct characters in the attempted password. Optional, defaults to 0
*/
- getAuthenticateTime(darknetServerData: DarknetServerData, threads?: number, player?: Person): number;
+ getAuthenticateTime(
+ serverDetails: DarknetServerDetails,
+ threads?: number,
+ player?: Person,
+ correctCharactersInPassword?: number,
+ ): number;
/**
* Gets the time it will take to scrape logs from a server.
- * @param darknetServerData - The server to check heartbleed log scraping time on.
+ * @param serverDetails - The server to check heartbleed log scraping time on.
* @param threads - The number of threads to use for the authentication. Optional, defaults to 1
* @param player - The player object. Optional, defaults to the current player status
*/
- getHeartbleedTime(darknetServerData: DarknetServerData, threads?: number, player?: Person): number;
+ getHeartbleedTime(serverDetails: DarknetServerDetails, threads?: number, player?: Person): number;
/**
* Gets the expected amount off ram that will be freed by a call to dnet.memoryReallocation
- * @param darknetServerData - The server to check ram freed on.
+ * @param serverDetails - The server to check ram freed on.
* @param threads - The number of threads used in the memoryReallocation call. Optional, defaults to 1
* @param player - The player object. Optional, defaults to the current player status
*/
- getExpectedRamBlockRemoved(darknetServerData: DarknetServerData, threads?: number, player?: Person): number;
+ getExpectedRamBlockRemoved(serverDetails: DarknetServerDetails, threads?: number, player?: Person): number;
}
/**
@@ -8057,7 +8032,7 @@ export interface NS {
* @remarks
* RAM cost: 0.6 GB
*
- * Copies a script or literature (.lit) file(s) to another server. The files argument can be either a string
+ * Copies text, script or literature (.lit) file(s) to another server. The files argument can be either a string
* specifying a single file to copy, or an array of strings specifying multiple files to copy.
*
* @example
@@ -8082,7 +8057,7 @@ export interface NS {
* connection) or {@link Darknet.connectToSession | dnet.connectToSession} (at any distance) to
* establish a session.
*
- * @param files - Filename or an array of filenames of script/literature files to copy. Note that if a file is located in a subdirectory, the filename must include the leading `/`.
+ * @param files - Filename or an array of filenames of text/script/literature files to copy. Note that if a file is located in a subdirectory, the filename must include the leading `/`.
* @param destination - Hostname/IP of the destination server, which is the server to which the file will be copied.
* @param source - Hostname/IP of the source server, which is the server from which the file will be copied. This argument is optional and if it’s omitted the source will be the current server.
* @returns True if the file is successfully copied over and false otherwise. If the files argument is an array then this function will return false if any of the operations failed.
@@ -8209,8 +8184,8 @@ export interface NS {
/**
* Returns data of a server.
*
- * If the server is a darknet server and has recently gone offline, it will return a dummy server object with
- * `isOnline: false`.
+ * If the server is a darknet server, it will also contain the "isOnline" field. If the darknet server has recently
+ * gone offline, the returned object will be a dummy server object with `isOnline: false`.
*
* @remarks
* RAM cost: 2 GB
@@ -8218,7 +8193,7 @@ export interface NS {
* @param host - Optional. Hostname/IP of the server. Defaults to the hostname of the running script's server.
* @returns Data of the server.
*/
- getServer(host?: string): Server | (DarknetServerData & { isOnline: boolean });
+ getServer(host?: string): Server & { isOnline?: boolean };
/**
* Get money available on a server.
@@ -8404,12 +8379,12 @@ export interface NS {
* //The function call will return true if there is a script named foo.js running with the arguments 1, 5, and “test” (in that order) on the joesguns server, and false otherwise:
* ns.isRunning("foo.js", "joesguns", 1, 5, "test");
* ```
- * @param script - Filename or PID of script to check. This is case-sensitive.
+ * @param script - Filename (case-sensitive) or PID of script to check. Optional, default to the current script's pid.
* @param host - Hostname/IP of target server. Optional, defaults to the server the calling script is running on.
* @param args - Arguments to specify/identify the script. Optional, when looking for scripts run without arguments.
* @returns True if the specified script is running on the target server, and false otherwise.
*/
- isRunning(script: FilenameOrPID, host?: string, ...args: ScriptArg[]): boolean;
+ isRunning(script?: FilenameOrPID, host?: string, ...args: ScriptArg[]): boolean;
/**
* Get general info about a running script.
@@ -8614,7 +8589,7 @@ export interface NS {
/**
* Delete a file.
* @remarks
- * RAM cost: 1 GB
+ * RAM cost: 0.6 GB
*
* Removes the specified file from the current server. This function works for every file
* type except message (.msg) files.
@@ -9027,12 +9002,22 @@ export interface NS {
getResetInfo(): ResetInfo;
/**
- * Get the ram cost of a netscript function.
+ * Get the RAM cost of a netscript function.
+ *
+ * The base RAM cost per script thread can also be retrieved by using `"baseCost"` as argument to this function.
*
* @remarks
* RAM cost: 0 GB
*
- * @param name - The fully-qualified function name, without the leading `ns`. Example inputs: `hack`, `tprint`, `stock.getPosition`.
+ * @param name - The fully-qualified function name, without the leading `ns`.
+ *
+ * @example
+ * ```js
+ * const RAM_baseCost = ns.getFunctionRamCost('baseCost');
+ * const RAM_for_hack = ns.getFunctionRamCost('hack');
+ * const RAM_for_tprint = ns.getFunctionRamCost('tprint');
+ * const RAM_for_stock_getPosition = ns.getFunctionRamCost('stock.getPosition');
+ * ```
*/
getFunctionRamCost(name: string): number;
diff --git a/src/Server/AllServers.ts b/src/Server/AllServers.ts
index 24720489e..a2116ce8d 100644
--- a/src/Server/AllServers.ts
+++ b/src/Server/AllServers.ts
@@ -148,6 +148,25 @@ export function loadAllServers(saveString: string): void {
if (!(server instanceof Server) && !(server instanceof HacknetServer) && !(server instanceof DarknetServer)) {
throw new Error(`Server ${serverName} is not an instance of Server or HacknetServer or DarknetServer.`);
}
+ // Sanitize hostname
+ // A bug created ill-formed UTF-16 darknet hostnames that caused the in-game editor to crash. This code migrates
+ // those invalid hostnames and protects against similar issues in the future.
+ if (!server.hostname.isWellFormed()) {
+ server.hostname = server.hostname.toWellFormed();
+ for (const script of server.scripts.values()) {
+ script.server = server.hostname;
+ }
+ if (server.savedScripts) {
+ for (const script of server.savedScripts) {
+ script.server = server.hostname;
+ }
+ }
+ }
+ // Sanitize hostnames in server.serversOnNetwork
+ for (const [index, value] of server.serversOnNetwork.entries()) {
+ server.serversOnNetwork[index] = value.toWellFormed();
+ }
+
AllServers.set(server.hostname, server);
AllServers.set(server.ip, server);
}
diff --git a/src/Server/BaseServer.ts b/src/Server/BaseServer.ts
index eb21907c8..148d703fc 100644
--- a/src/Server/BaseServer.ts
+++ b/src/Server/BaseServer.ts
@@ -322,6 +322,12 @@ export abstract class BaseServer implements IServer {
if (value.data.runningScripts != null && Array.isArray(value.data.runningScripts)) {
server.savedScripts = value.data.runningScripts;
}
+ // Remove duplicate .lit and .msg files.
+ const messageSet = new Set(server.messages);
+ if (messageSet.size !== server.messages.length) {
+ console.warn("Found duplicate messages in ", server.messages);
+ server.messages = [...messageSet];
+ }
// If textFiles is not an array, we've already done the 2.3 migration to textFiles and scripts as maps + path changes.
if (!Array.isArray(server.textFiles)) return server;
diff --git a/src/Server/DarknetServer.ts b/src/Server/DarknetServer.ts
index 839c49588..cc5fa751b 100644
--- a/src/Server/DarknetServer.ts
+++ b/src/Server/DarknetServer.ts
@@ -1,10 +1,10 @@
import { BaseServer } from "./BaseServer";
import { constructorsForReviver, IReviverValue } from "../utils/JSONReviver";
-import type { DarknetServerData } from "@nsdefs";
import { exampleDarknetServerData } from "../DarkNet/Enums";
import { createRandomIp } from "../utils/IPAddress";
import type { CacheFilePath } from "../Paths/CacheFilePath";
import type { IPAddress } from "../Types/strings";
+import type { DarknetServerData } from "../DarkNet/utils/darknetServerUtils";
export interface DarknetServerConstructorParams {
// Properties of BaseServer
@@ -89,7 +89,14 @@ export class DarknetServer extends BaseServer implements DarknetServerData {
}
static fromJSON(value: IReviverValue): DarknetServer {
- return BaseServer.fromJSONBase(value, DarknetServer, includedKeys);
+ const server = BaseServer.fromJSONBase(value, DarknetServer, includedKeys);
+ // Remove duplicate .cache files.
+ const cacheSet = new Set(server.caches);
+ if (cacheSet.size !== server.caches.length) {
+ console.warn("Found duplicate cache files in ", server.caches);
+ server.caches = [...cacheSet];
+ }
+ return server;
}
}
diff --git a/src/Terminal/HelpText.ts b/src/Terminal/HelpText.ts
index 3ef23c24e..f8fa4f249 100644
--- a/src/Terminal/HelpText.ts
+++ b/src/Terminal/HelpText.ts
@@ -14,6 +14,7 @@ export const TerminalHelpText: string[] = [
" connect [hostname] Connects to a remote server",
" cp [src] [dest] Copy a file",
" download [script/text file] Downloads scripts or text files to your computer",
+ " upload [dir] Upload scripts or text files from your computer",
" expr [math expression] Evaluate a mathematical expression",
" free Check the machine's memory (RAM) usage",
" grep [opts]... pattern [file]... Search for PATTERN (string/regular expression) in each FILE and print results to terminal",
@@ -216,6 +217,18 @@ export const HelpTexts: Record = {
"Download all text files: download *.txt",
" ",
],
+ upload: [
+ "Usage: upload [dir]",
+ " ",
+ "Uploads a directory from your computer into the game.",
+ " ",
+ "Examples:",
+ " ",
+ " upload path/to/dir",
+ " ",
+ " upload .",
+ " ",
+ ],
expr: [
"Usage: expr [mathematical expression]",
" ",
diff --git a/src/Terminal/Terminal.ts b/src/Terminal/Terminal.ts
index a3225a9dd..ce5a11a33 100644
--- a/src/Terminal/Terminal.ts
+++ b/src/Terminal/Terminal.ts
@@ -44,6 +44,7 @@ import { check } from "./commands/check";
import { connect } from "./commands/connect";
import { cp } from "./commands/cp";
import { download } from "./commands/download";
+import { upload } from "./commands/upload";
import { expr } from "./commands/expr";
import { free } from "./commands/free";
import { grep } from "./commands/grep";
@@ -105,6 +106,7 @@ export const TerminalCommands: Record {
+ return new Promise((resolve) => {
+ const input = document.createElement("input");
+ input.type = "file";
+ input.webkitdirectory = true;
+ input.onchange = () => {
+ resolve(input.files);
+ };
+ input.oncancel = () => {
+ resolve(null);
+ };
+ input.click();
+ });
+}
+
+function askConfirm(txt: string): Promise {
+ return new Promise((resolve, reject) => {
+ PromptEvent.emit({
+ txt,
+ resolve: (value: string | boolean) => {
+ if (typeof value === "string") {
+ reject(new Error("PromptEvent got a string, expected boolean"));
+ } else {
+ resolve(value);
+ }
+ },
+ });
+ });
+}
+
+async function uploadAsync(args: (string | number | boolean)[], server: BaseServer) {
+ if (args.length !== 1) {
+ return Terminal.error("Incorrect usage of upload command. Usage: upload [dir]");
+ }
+ const destinationInput = String(args[0]);
+ const destination = Terminal.getDirectory(destinationInput);
+ if (destination === null) {
+ return Terminal.error(`Could not resolve ${destinationInput} as a Directory`);
+ }
+ const files = await pickDirectory();
+ if (files === null || files.length === 0) {
+ return;
+ }
+ const withPath: (
+ | { badPath: string }
+ | { overwrite: ContentFilePath; file: File }
+ | { create: ContentFilePath; file: File }
+ )[] = [...files].map((f) => {
+ const { webkitRelativePath } = f;
+ /*
+ If the player has a directory /home/alice/foo/bar on her computer
+ and wants to upload the contents of the directory
+ and if the directory hierarchy looks like this:
+ /home/alice/foo/bar
+ ├── hello
+ │ └── world.js
+ └── more
+ └── files.txt
+ `webkitRelativePath` for world.js will be "bar/hello/world.js"
+ `path` will be "hello/world.js"
+ `webkitRelativePath` for files.txt will "bar/more/files.txt"
+ `path` will be "more/files.txt"
+ */
+ const path = webkitRelativePath.substring(1 + webkitRelativePath.indexOf("/"));
+ if (!isFilePath(path)) {
+ return { badPath: path };
+ }
+ const destFilePath = combinePath(destination, path);
+ let fileExists: boolean;
+ if (hasTextExtension(destFilePath)) {
+ fileExists = server.textFiles.has(destFilePath);
+ } else if (hasScriptExtension(destFilePath)) {
+ fileExists = server.scripts.has(destFilePath);
+ } else {
+ return { badPath: path };
+ }
+ if (fileExists) {
+ return {
+ overwrite: destFilePath,
+ file: f,
+ };
+ }
+ return {
+ create: destFilePath,
+ file: f,
+ };
+ });
+ const overwrite = withPath.filter((item) => "overwrite" in item);
+ const skipped = withPath.filter((item) => "badPath" in item);
+ const create = withPath.filter((item) => "create" in item);
+ const destForPrint = destination === "" ? "/" : destination;
+ const lines = [`Upload files to ${destForPrint}?`];
+ if (overwrite.length !== 0) {
+ lines.push(
+ "",
+ `${pluralize(overwrite.length, "file")} will be overwritten:`,
+ ...overwrite.map(({ overwrite }) => overwrite),
+ );
+ }
+ if (skipped.length !== 0) {
+ const extensions = [...validScriptExtensions, ...validTextExtensions];
+ lines.push(
+ "",
+ `Characters ${invalidCharacters
+ .filter((v) => v !== "/")
+ .join(" ")} and whitespace are not allowed in file paths.`,
+ `Only file extensions ${extensions.join(", ")} are allowed.`,
+ "A file name must have at least one character before the extension.",
+ "",
+ `${pluralize(skipped.length, "file")} will be skipped due to prohibited file paths:`,
+ ...skipped.map(({ badPath }) => badPath),
+ );
+ }
+ if (create.length !== 0) {
+ lines.push("", `${pluralize(create.length, "new file")} will be created:`, ...create.map(({ create }) => create));
+ }
+ if (!(await askConfirm(lines.join("\n")))) {
+ return;
+ }
+ Terminal.print(`Starting to upload files to ${destForPrint}`);
+ for (const item of [...overwrite, ...create]) {
+ const destFilePath = "create" in item ? item.create : item.overwrite;
+ let text: string | undefined = undefined;
+ try {
+ text = await item.file.text();
+ } catch (error) {
+ console.error(error);
+ Terminal.error(`Failed to upload ${destFilePath}. Error: ${error}`);
+ continue;
+ }
+ server.writeToContentFile(destFilePath, text);
+ }
+ Terminal.print(`Successfully uploaded files to ${destForPrint}`);
+}
+
+export function upload(args: (string | number | boolean)[], server: BaseServer): void {
+ uploadAsync(args, server).catch((error) => {
+ console.error(error);
+ Terminal.error(`Error while uploading files. Error: ${error}`);
+ });
+}
diff --git a/src/Terminal/getTabCompletionPossibilities.ts b/src/Terminal/getTabCompletionPossibilities.ts
index b699599cd..781c1e247 100644
--- a/src/Terminal/getTabCompletionPossibilities.ts
+++ b/src/Terminal/getTabCompletionPossibilities.ts
@@ -185,6 +185,7 @@ export async function getTabCompletionPossibilities(terminalText: string, baseDi
case "cd":
case "ls":
+ case "upload":
if (onFirstCommandArg && !relativeDir) addDirectories();
return possibilities;
diff --git a/src/ThirdParty/acorn-typescript-walk/index.ts b/src/ThirdParty/acorn-typescript-walk/index.ts
index a4e72d2dd..61c8a2fab 100644
--- a/src/ThirdParty/acorn-typescript-walk/index.ts
+++ b/src/ThirdParty/acorn-typescript-walk/index.ts
@@ -81,7 +81,7 @@ export function extendAcornWalkForTypeScriptNodes(base: any) {
}
// Only walk relevant TypeScript nodes.
base.TSModuleBlock = base.BlockStatement;
- base.TSAsExpression = base.TSNonNullExpression = base.ExpressionStatement;
+ base.TSTypeAssertion = base.TSAsExpression = base.TSNonNullExpression = base.ExpressionStatement;
base.TSModuleDeclaration = (node: any, state: any, callback: any) => {
callback(node.body, state);
};
diff --git a/src/db.ts b/src/db.ts
index ad688f803..d83dbeab6 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -1,5 +1,6 @@
import type { SaveData } from "./types";
-import { isSaveData } from "./utils/TypeAssertion";
+import { InvalidSaveData } from "./utils/SaveDataUtils";
+import { isSaveData, validateSaveData } from "./utils/TypeAssertion";
export class IndexedDBVersionError extends Error {
constructor(message: string, options: ErrorOptions) {
@@ -100,6 +101,12 @@ export async function load(skipCheckingLoadedData = false): Promise {
+ // Validate save data at runtime. Players may accidentally manipulate the prototypes of built-in JS objects and cause
+ // functions in SaveObject.ts to generate invalid save data.
+ const validationResult = validateSaveData(saveData);
+ if (!validationResult.success) {
+ throw new InvalidSaveData(validationResult.message);
+ }
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(["savestring"], "readwrite");
diff --git a/src/engine.tsx b/src/engine.tsx
index 78cf9160c..c64072970 100644
--- a/src/engine.tsx
+++ b/src/engine.tsx
@@ -8,7 +8,6 @@ import { Factions } from "./Faction/Factions";
import { staneksGift } from "./CotMG/Helper";
import { processPassiveFactionRepGain, inviteToFaction } from "./Faction/FactionHelpers";
import { Router } from "./ui/GameRoot";
-import "./utils/Protections"; // Side-effect: Protect against certain unrecoverable errors
import "./PersonObjects/Player/PlayerObject"; // For side-effect of creating Player
import {
diff --git a/src/index.tsx b/src/index.tsx
index 836100dc5..ce3298141 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,3 +1,5 @@
+import "./utils/Protections"; // Side-effect: Protect against certain unrecoverable errors
+
import React from "react";
import ReactDOM from "react-dom";
diff --git a/src/ui/CharacterStats.tsx b/src/ui/CharacterStats.tsx
index ec9d1fadb..53d6e9bd0 100644
--- a/src/ui/CharacterStats.tsx
+++ b/src/ui/CharacterStats.tsx
@@ -533,14 +533,18 @@ export function CharacterStats(): React.ReactElement {
effValue: Player.mults.crime_money * currentNodeMults.CrimeMoney,
color: Settings.theme.money,
},
+ ]}
+ color={Settings.theme.combat}
+ />
+
{Player.canAccessBladeburner() && currentNodeMults.BladeburnerRank > 0 && (
{
diff --git a/src/ui/InteractiveTutorial/InteractiveTutorialRoot.tsx b/src/ui/InteractiveTutorial/InteractiveTutorialRoot.tsx
index 8fd360ae7..20bbdf2d3 100644
--- a/src/ui/InteractiveTutorial/InteractiveTutorialRoot.tsx
+++ b/src/ui/InteractiveTutorial/InteractiveTutorialRoot.tsx
@@ -563,8 +563,8 @@ export async function main(ns) {
materials for all NS APIs.
- The FAQ contains questions often asked
- by beginners of the game.
+ The FAQ contains questions often asked by
+ beginners of the game.
diff --git a/src/ui/React/AlertManager.tsx b/src/ui/React/AlertManager.tsx
index 8f153318e..4971ad6f9 100644
--- a/src/ui/React/AlertManager.tsx
+++ b/src/ui/React/AlertManager.tsx
@@ -67,7 +67,9 @@ export function AlertManager({ hidden }: { hidden: boolean }): React.ReactElemen
textPropsAsString = JSON.stringify(text.props);
} catch (e) {
console.error(e);
- // Use the current timestamp as the fallback value.
+ }
+ // Use the current timestamp as the fallback value.
+ if (textPropsAsString == null) {
textPropsAsString = Date.now().toString();
}
return cyrb53(textPropsAsString);
diff --git a/src/ui/WorkInProgressRoot.tsx b/src/ui/WorkInProgressRoot.tsx
index 2a26f6d10..957490ef1 100644
--- a/src/ui/WorkInProgressRoot.tsx
+++ b/src/ui/WorkInProgressRoot.tsx
@@ -219,7 +219,6 @@ export function WorkInProgressRoot(): React.ReactElement {
},
unfocus: () => {
Router.toPage(Page.City);
- Player.stopFocusing();
},
},
title: `You are attempting ${crime.workName}`,
@@ -266,7 +265,6 @@ export function WorkInProgressRoot(): React.ReactElement {
},
unfocus: () => {
Router.toPage(Page.Location, { location: Locations[classWork.location] });
- Player.stopFocusing();
},
},
title: (
@@ -303,7 +301,6 @@ export function WorkInProgressRoot(): React.ReactElement {
},
unfocus: () => {
Router.toPage(Page.Terminal);
- Player.stopFocusing();
},
},
title: (
@@ -334,7 +331,6 @@ export function WorkInProgressRoot(): React.ReactElement {
},
unfocus: () => {
Router.toPage(Page.Terminal);
- Player.stopFocusing();
},
},
title: (
@@ -388,7 +384,6 @@ export function WorkInProgressRoot(): React.ReactElement {
},
unfocus: () => {
Router.toPage(Page.Faction, { faction });
- Player.stopFocusing();
},
},
title: (
@@ -439,7 +434,6 @@ export function WorkInProgressRoot(): React.ReactElement {
Router.toPage(Page.Job);
},
unfocus: () => {
- Player.stopFocusing();
Router.toPage(Page.Job);
},
},
diff --git a/src/utils/APIBreaks/3.0.1.ts b/src/utils/APIBreaks/3.0.1.ts
new file mode 100644
index 000000000..413ef296e
--- /dev/null
+++ b/src/utils/APIBreaks/3.0.1.ts
@@ -0,0 +1,19 @@
+import type { VersionBreakingChange } from "./APIBreak";
+
+export const breakingChanges301: VersionBreakingChange = {
+ apiBreakingChanges: [
+ {
+ brokenAPIs: [
+ {
+ name: "ns.dnet.getServerAuthDetails",
+ migration: {
+ searchValue: "getServerAuthDetails",
+ replaceValue: "getServerDetails",
+ },
+ },
+ ],
+ info: "ns.dnet.getServerAuthDetails has been renamed to ns.dnet.getServerDetails",
+ showWarning: false,
+ },
+ ],
+};
diff --git a/src/utils/ErrorHelper.ts b/src/utils/ErrorHelper.ts
index 63d9494f3..5d6a5729a 100644
--- a/src/utils/ErrorHelper.ts
+++ b/src/utils/ErrorHelper.ts
@@ -186,7 +186,9 @@ Copy your save here if possible
\`\`\`
`.trim();
- const issueUrl = `${newIssueUrl}?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
+ const issueUrl = `${newIssueUrl}?title=${encodeURIComponent(title.toWellFormed())}&body=${encodeURIComponent(
+ body.toWellFormed(),
+ )}`;
return {
metadata,
diff --git a/src/utils/Protections.ts b/src/utils/Protections.ts
index 1ff968576..1f1509682 100644
--- a/src/utils/Protections.ts
+++ b/src/utils/Protections.ts
@@ -1,5 +1,5 @@
// This file is imported for side effects only.
-/* Prevent inadvertantly redefining certain window properties,
+/* Prevent inadvertently redefining certain window properties,
which are known to cause unrecoverable game errors when redefined.
The player is able to redefine these properties as writable if desired. */
Object.defineProperties(window, {
@@ -7,3 +7,18 @@ Object.defineProperties(window, {
Object: { writable: false },
String: { writable: false },
});
+
+// Prevent accidentally manipulating IndexedDB APIs.
+Object.freeze(IDBFactory.prototype);
+Object.freeze(IDBDatabase.prototype);
+Object.freeze(IDBTransaction.prototype);
+Object.freeze(IDBObjectStore.prototype);
+Object.freeze(IDBRequest.prototype);
+Object.freeze(IDBOpenDBRequest.prototype);
+if (window.indexedDB) {
+ Object.freeze(window.indexedDB);
+ Object.defineProperty(window, "indexedDB", {
+ value: window.indexedDB,
+ writable: false,
+ });
+}
diff --git a/src/utils/SaveDataMigrationUtils.ts b/src/utils/SaveDataMigrationUtils.ts
index 64b7c5677..82a19d347 100644
--- a/src/utils/SaveDataMigrationUtils.ts
+++ b/src/utils/SaveDataMigrationUtils.ts
@@ -35,6 +35,7 @@ import { officeInitialCost, officeInitialSize, warehouseInitialCost } from "../C
import { load } from "../db";
import { downloadContentAsFile } from "./FileUtils";
import { initDarkwebServer } from "../DarkNet/controllers/NetworkGenerator";
+import { breakingChanges301 } from "./APIBreaks/3.0.1";
/** Function for performing a series of defined replacements. See 0.58.0 for usage */
function convert(code: string, changes: [RegExp, string][]): string {
@@ -644,4 +645,7 @@ Error: ${e}`,
}
showAPIBreaks("3.0.0", breakingChanges300);
}
+ if (ver < 51) {
+ showAPIBreaks("3.0.1", breakingChanges301);
+ }
}
diff --git a/src/utils/SaveDataUtils.ts b/src/utils/SaveDataUtils.ts
index d62f942f8..f023b44b4 100644
--- a/src/utils/SaveDataUtils.ts
+++ b/src/utils/SaveDataUtils.ts
@@ -35,6 +35,19 @@ async function decompress(binaryData: Uint8Array): Promise
}
export async function encodeJsonSaveString(jsonSaveString: string): Promise {
+ if (jsonSaveString == null) {
+ throw new InvalidSaveData(`jsonSaveString is ${jsonSaveString}`);
+ }
+ if (typeof jsonSaveString !== "string") {
+ console.error(jsonSaveString);
+ throw new InvalidSaveData(`Type of jsonSaveString is ${typeof jsonSaveString}`);
+ }
+ if (!jsonSaveString.startsWith(`{"ctor":"BitburnerSaveObject"`)) {
+ console.error(jsonSaveString);
+ throw new InvalidSaveData(
+ `Invalid jsonSaveString (doesn't seem to contain a BitburnerSaveObject): ${jsonSaveString.slice(0, 100)}`,
+ );
+ }
// Fallback to the base64 format if player's browser does not support Compression Streams API.
if (canUseBinaryFormat()) {
return await compress(jsonSaveString);
diff --git a/src/utils/StringHelperFunctions.ts b/src/utils/StringHelperFunctions.ts
index 5991972e6..ee7496242 100644
--- a/src/utils/StringHelperFunctions.ts
+++ b/src/utils/StringHelperFunctions.ts
@@ -105,3 +105,18 @@ export function getKeyFromReactElements(a: string | React.JSX.Element, b: string
const keyOfb = typeof b === "string" ? b : b.key ?? "";
return keyOfA + keyOfb;
}
+
+const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
+
+/**
+ * input.split("") operates on UTF-16 code units and can break surrogate pairs.
+ * For example, 'a🅱️b' is 'a\uD83C\uDD71\uFE0Fb'. A naive reverse yields 'b\uFE0F\uDD71\uD83Ca', which is ill-formed
+ * UTF-16 and not 'b🅱️a' as expected.
+ * Passing such a string to encodeURIComponent may throw a URIError (e.g. in Monaco editor code when processing model
+ * ids).
+ */
+export function safelyReverseString(input: string): string {
+ return Array.from(graphemeSegmenter.segment(input), (s) => s.segment)
+ .reverse()
+ .join("");
+}
diff --git a/src/utils/TypeAssertion.ts b/src/utils/TypeAssertion.ts
index 997df2c24..6a5685af2 100644
--- a/src/utils/TypeAssertion.ts
+++ b/src/utils/TypeAssertion.ts
@@ -1,4 +1,5 @@
import type { SaveData, Unknownify } from "../types";
+import type { Result } from "@nsdefs";
// This function is empty because Unknownify is a typesafe assertion on any object with no runtime checks needed.
// eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -90,12 +91,35 @@ export function assertNumberArray(unknownData: unknown, assertFinite = false): a
}
}
-export function isSaveData(unknownData: unknown): unknownData is SaveData {
- if (typeof unknownData === "string") {
- return true;
+export function validateSaveData(unknownData: unknown): Result {
+ if (unknownData == null) {
+ return { success: false, message: `Save data is ${unknownData}` };
}
- return unknownData instanceof Uint8Array && unknownData.buffer instanceof ArrayBuffer;
+ if (unknownData === "") {
+ return { success: false, message: "Save data is an empty string" };
+ }
+ if (typeof unknownData === "string") {
+ return { success: true };
+ }
+
+ if (!(unknownData instanceof Uint8Array)) {
+ console.error(unknownData);
+ return { success: false, message: "Save data is not an instance of Uint8Array" };
+ }
+ if (unknownData.length === 0) {
+ return { success: false, message: "Save data is an empty Uint8Array" };
+ }
+ if (!(unknownData.buffer instanceof ArrayBuffer)) {
+ console.error(unknownData.buffer);
+ return { success: false, message: "Save data is a Uint8Array, but its buffer is not an ArrayBuffer" };
+ }
+
+ return { success: true };
+}
+
+export function isSaveData(unknownData: unknown): unknownData is SaveData {
+ return validateSaveData(unknownData).success;
}
export function assertSaveData(unknownData: unknown): asserts unknownData is SaveData {
diff --git a/test/jest/Bladeburner/Actions.test.ts b/test/jest/Bladeburner/Actions.test.ts
index f3595ce94..f00db9f24 100644
--- a/test/jest/Bladeburner/Actions.test.ts
+++ b/test/jest/Bladeburner/Actions.test.ts
@@ -13,6 +13,7 @@ import {
BladeburnerOperationName,
CityName,
CrimeType,
+ FactionName,
} from "@enums";
import { FormatsNeedToChange } from "../../../src/ui/formatNumber";
import { CrimeWork } from "../../../src/Work/CrimeWork";
@@ -21,6 +22,8 @@ import type { Skills } from "@nsdefs";
import { applyAugmentation } from "../../../src/Augmentation/AugmentationHelpers";
import { PlayerOwnedAugmentation } from "../../../src/Augmentation/PlayerOwnedAugmentation";
import { BlackOperation } from "../../../src/Bladeburner/Actions/BlackOperation";
+import { joinFaction } from "../../../src/Faction/FactionHelpers";
+import { Factions } from "../../../src/Faction/Factions";
describe("Bladeburner Actions", () => {
const SampleContract = Contract.createId(BladeburnerContractName.Tracking);
@@ -244,8 +247,12 @@ describe("Bladeburner Actions", () => {
describe.each([SampleOperation, SampleBlackOp])("operations and black operations decrease rank", (id) => {
it(`${id.type}`, () => {
before = bb.rank;
+ joinFaction(Factions[FactionName.Bladeburners]);
+ expect(Player.factions.includes(FactionName.Bladeburners)).toBe(true);
+ expect(Factions[FactionName.Bladeburners].playerReputation).toBe(0);
complete(id, forceFailure);
expect(bb.rank).toBeLessThan(before);
+ expect(Factions[FactionName.Bladeburners].playerReputation).toBe(0);
});
});
});
@@ -262,6 +269,7 @@ describe("Bladeburner Actions", () => {
beforeEach(() => {
setPlayer(new PlayerObject());
+ Factions[FactionName.Bladeburners].prestigeSourceFile();
/** Need BN5 to receive Int EXP */
Player.sourceFiles.set(5, 3);
diff --git a/test/jest/Darknet/Darknet.test.ts b/test/jest/Darknet/Darknet.test.ts
index 40d29a8cf..9fe4246f0 100644
--- a/test/jest/Darknet/Darknet.test.ts
+++ b/test/jest/Darknet/Darknet.test.ts
@@ -61,9 +61,11 @@ import { DarknetServer } from "../../../src/Server/DarknetServer";
import { isDirectoryPath } from "../../../src/Paths/Directory";
import { isFilePath } from "../../../src/Paths/FilePath";
import { LAB_CACHE_NAME } from "../../../src/DarkNet/effects/labyrinth";
-import { generateCacheFilename } from "../../../src/DarkNet/effects/cacheFiles";
+import { generateCacheFilename, getStockReward } from "../../../src/DarkNet/effects/cacheFiles";
import { getAllDarknetServers } from "../../../src/DarkNet/utils/darknetNetworkUtils";
import { prestigeAugmentation } from "../../../src/Prestige";
+import { initStockMarket, StockMarket } from "../../../src/StockMarket/StockMarket";
+import { StockSymbol } from "@enums";
beforeAll(() => {
initGameEnvironment();
@@ -778,6 +780,7 @@ describe("mutateDarknet and webstorm", () => {
function validatePath(hostname: string): void {
expectWithMessage(isDirectoryPath(`${hostname}/`), true, `Invalid hostname: ${hostname}`);
expectWithMessage(isFilePath(`${hostname}/data.txt`), true, `Invalid hostname: ${hostname}`);
+ expectWithMessage(hostname.isWellFormed(), true, `Malformed hostname: ${hostname}`);
}
describe("Darknet server name generator", () => {
@@ -839,3 +842,48 @@ describe("Clue filename generator", () => {
}
});
});
+
+describe("Stock cache reward", () => {
+ test("stock reward does not exceed maxShares and falls back to money reward", () => {
+ initStockMarket();
+
+ const remaining = 3;
+ // Fill every stock to near capacity so no matter which one is randomly picked, it triggers clamping
+ for (const stockName of Object.keys(StockSymbol)) {
+ const stock = StockMarket[stockName];
+ stock.playerShares = stock.maxShares - remaining;
+ stock.playerShortShares = 0;
+ }
+
+ // Use high difficulty to ensure the unclamped share count would exceed remaining
+ const difficulty = 100;
+ const result = getStockReward(difficulty);
+
+ // Should have awarded at most `remaining` shares
+ expect(result).toContain(`${remaining} shares`);
+
+ // Verify the chosen stock was clamped to exactly maxShares
+ for (const stockName of Object.keys(StockSymbol)) {
+ const stock = StockMarket[stockName];
+ expect(stock.playerShares).toBeLessThanOrEqual(stock.maxShares);
+ }
+ });
+
+ test("stock reward falls back to money when stock is fully owned", () => {
+ initStockMarket();
+
+ // Fill every stock to max capacity
+ for (const stockName of Object.keys(StockSymbol)) {
+ const stock = StockMarket[stockName];
+ stock.playerShares = stock.maxShares;
+ stock.playerShortShares = 0;
+ }
+
+ const moneyBefore = Player.money;
+ const result = getStockReward(5);
+
+ // Should have fallen back to a money reward
+ expect(result).toContain("discovered a cache with");
+ expect(Player.money).toBeGreaterThan(moneyBefore);
+ });
+});
diff --git a/test/jest/Migration/Migration.test.ts b/test/jest/Migration/Migration.test.ts
index 6d78f02c8..28e9a4876 100644
--- a/test/jest/Migration/Migration.test.ts
+++ b/test/jest/Migration/Migration.test.ts
@@ -6,6 +6,7 @@ import * as db from "../../../src/db";
import * as FileUtils from "../../../src/utils/FileUtils";
import type { SaveData } from "../../../src/types";
import { calculateExp } from "../../../src/PersonObjects/formulas/skill";
+import { GetAllServers, GetServer } from "../../../src/Server/AllServers";
async function loadGameFromSaveData(saveData: SaveData) {
// Simulate loading the data in IndexedDB
@@ -132,4 +133,25 @@ describe("v3", () => {
expect(mockedDownload).not.toHaveBeenCalled();
});
});
+
+ test("Malformed hostname", async () => {
+ const saveData = new Uint8Array(fs.readFileSync("test/jest/Migration/save-files/malformed-hostname.gz"));
+ await loadGameFromSaveData(saveData);
+ for (const server of GetAllServers(true)) {
+ expect(server.hostname.isWellFormed()).toBe(true);
+ for (const script of server.scripts.values()) {
+ expect(script.server).toStrictEqual(server.hostname);
+ }
+ if (server.savedScripts) {
+ for (const script of server.savedScripts) {
+ expect(script.server).toStrictEqual(server.hostname);
+ }
+ }
+ for (const hostname of server.serversOnNetwork) {
+ expect(hostname.isWellFormed()).toBe(true);
+ expect(GetServer(hostname)).not.toBeNull();
+ }
+ }
+ expect(() => Player.getCurrentServer()).not.toThrow();
+ });
});
diff --git a/test/jest/Migration/save-files/malformed-hostname.gz b/test/jest/Migration/save-files/malformed-hostname.gz
new file mode 100644
index 000000000..a5decabb5
Binary files /dev/null and b/test/jest/Migration/save-files/malformed-hostname.gz differ
diff --git a/test/jest/Netscript/Darknet.test.ts b/test/jest/Netscript/Darknet.test.ts
index b72a87966..c1a5973f7 100644
--- a/test/jest/Netscript/Darknet.test.ts
+++ b/test/jest/Netscript/Darknet.test.ts
@@ -211,9 +211,9 @@ describe("home", () => {
const ns = getNsOnHome();
expect(ns.getServer().hostname).toStrictEqual(SpecialServers.Home);
});
- test("getServerAuthDetails", () => {
+ test("getServerDetails", () => {
const ns = getNsOnHome();
- expect(() => ns.dnet.getServerAuthDetails()).toThrow("home is not a darknet server");
+ expect(() => ns.dnet.getServerDetails()).toThrow("home is not a darknet server");
});
test("induceServerMigration", () => {
const ns = getNsOnHome();
@@ -375,9 +375,9 @@ describe("Normal NPC server", () => {
const ns = getNS(SpecialServers.CyberSecServer);
expect(ns.getServer().hostname).toStrictEqual(SpecialServers.CyberSecServer);
});
- test("getServerAuthDetails", () => {
+ test("getServerDetails", () => {
const ns = getNS(SpecialServers.CyberSecServer);
- expect(() => ns.dnet.getServerAuthDetails()).toThrow("CSEC is not a darknet server");
+ expect(() => ns.dnet.getServerDetails()).toThrow("CSEC is not a darknet server");
});
test("induceServerMigration", () => {
const ns = getNS(SpecialServers.CyberSecServer);
@@ -465,9 +465,9 @@ describe("Private server", () => {
const ns = getNS("test-server-1");
expect(ns.getServer().hostname).toStrictEqual("test-server-1");
});
- test("getServerAuthDetails", () => {
+ test("getServerDetails", () => {
const ns = getNS("test-server-1");
- expect(() => ns.dnet.getServerAuthDetails()).toThrow("test-server-1 is not a darknet server");
+ expect(() => ns.dnet.getServerDetails()).toThrow("test-server-1 is not a darknet server");
});
test("induceServerMigration", () => {
const ns = getNS("test-server-1");
@@ -555,9 +555,9 @@ describe("Hashnet server", () => {
const ns = getNS("hacknet-server-0");
expect(ns.getServer().hostname).toStrictEqual("hacknet-server-0");
});
- test("getServerAuthDetails", () => {
+ test("getServerDetails", () => {
const ns = getNS("hacknet-server-0");
- expect(() => ns.dnet.getServerAuthDetails()).toThrow("hacknet-server-0 is not a darknet server");
+ expect(() => ns.dnet.getServerDetails()).toThrow("hacknet-server-0 is not a darknet server");
});
test("induceServerMigration", () => {
const ns = getNS("hacknet-server-0");
@@ -639,9 +639,9 @@ describe("Non-existent server", () => {
ns.getServer(hostnameOfNonExistentServer);
}).toThrow(errorMessageForNonExistentServer);
});
- test("getServerAuthDetails", () => {
+ test("getServerDetails", () => {
const ns = getNsOnDarkWeb();
- expect(() => ns.dnet.getServerAuthDetails(hostnameOfNonExistentServer)).toThrow(errorMessageForNonExistentServer);
+ expect(() => ns.dnet.getServerDetails(hostnameOfNonExistentServer)).toThrow(errorMessageForNonExistentServer);
});
test("induceServerMigration", () => {
const ns = getNsOnDarkWeb();
@@ -782,9 +782,9 @@ describe("darkweb", () => {
expect(server.isOnline).toStrictEqual(true);
expect(server.ip).toStrictEqual(getDarknetServerOrThrow(SpecialServers.DarkWeb).ip);
});
- test("getServerAuthDetails", () => {
+ test("getServerDetails", () => {
const ns = getNsOnDarkWeb();
- const authDetails = ns.dnet.getServerAuthDetails();
+ const authDetails = ns.dnet.getServerDetails();
expect(authDetails.isOnline).toStrictEqual(true);
expect(authDetails.modelId).toStrictEqual(ModelIds.NoPassword);
});
@@ -935,9 +935,9 @@ describe("Non-darkweb darknet server", () => {
const server = ns.getServer();
expect(server.hostname).toStrictEqual(ns.getHostname());
});
- test("getServerAuthDetails", () => {
+ test("getServerDetails", () => {
const ns = getNsOnNonDarkwebDarknetServer();
- const authDetails = ns.dnet.getServerAuthDetails();
+ const authDetails = ns.dnet.getServerDetails();
expect(authDetails.modelId).toStrictEqual(getDarknetServerOrThrow(ns.getHostname()).modelId);
});
test("induceServerMigration targeting connected server", async () => {
@@ -1071,12 +1071,12 @@ describe("Offline darknet server", () => {
expect(server.hostname).toBe("");
expect(server.ip).toBe(ipForOfflineServer);
});
- test("getServerAuthDetails", () => {
+ test("getServerDetails", () => {
const ns = getNsOnDarkWeb();
- let authDetails = ns.dnet.getServerAuthDetails(hostnameForOfflineServer);
+ let authDetails = ns.dnet.getServerDetails(hostnameForOfflineServer);
expect(authDetails.isOnline).toStrictEqual(false);
- authDetails = ns.dnet.getServerAuthDetails(ipForOfflineServer);
+ authDetails = ns.dnet.getServerDetails(ipForOfflineServer);
expect(authDetails.isOnline).toStrictEqual(false);
});
test("induceServerMigration", async () => {
@@ -1174,9 +1174,9 @@ describe("Use IP instead of hostname", () => {
}
expect(server.ip).toStrictEqual(ip);
});
- test("getServerAuthDetails", () => {
+ test("getServerDetails", () => {
const ns = getNsOnNonDarkwebDarknetServer();
- const authDetails = ns.dnet.getServerAuthDetails(ip);
+ const authDetails = ns.dnet.getServerDetails(ip);
expect(authDetails.isOnline).toStrictEqual(true);
expect(authDetails.modelId).toStrictEqual(getDarknetServerOrThrow(ip).modelId);
});
diff --git a/test/jest/Netscript/Gang.test.ts b/test/jest/Netscript/Gang.test.ts
index 667bdf362..e2467295f 100644
--- a/test/jest/Netscript/Gang.test.ts
+++ b/test/jest/Netscript/Gang.test.ts
@@ -1,8 +1,10 @@
import { FactionName } from "@enums";
import { Player } from "@player";
-import { Gang } from "../../../src/Gang/Gang";
import { AllGangs } from "../../../src/Gang/AllGangs";
-import { getNS, initGameEnvironment, setupBasicTestingEnvironment } from "../Utilities";
+import { getNS, getWorkerScriptAndNS, initGameEnvironment, setupBasicTestingEnvironment } from "../Utilities";
+import { GangConstants } from "../../../src/Gang/data/Constants";
+import { joinFaction } from "../../../src/Faction/FactionHelpers";
+import { Factions } from "../../../src/Faction/Factions";
beforeAll(() => {
initGameEnvironment();
@@ -10,12 +12,16 @@ beforeAll(() => {
beforeEach(() => {
setupBasicTestingEnvironment();
- // Give the player a gang so gang API is accessible
- Player.gang = new Gang(FactionName.SlumSnakes, false);
+ Player.sourceFiles.set(2, 3);
});
describe("ns.gang.getAllGangInformation", () => {
- it("should return territory and power info for all gangs including the player's", () => {
+ beforeEach(() => {
+ // Give the player a gang so gang API is accessible
+ Player.startGang(FactionName.SlumSnakes, false);
+ });
+
+ test("should return territory and power info for all gangs including the player's", () => {
const ns = getNS();
const info = ns.gang.getAllGangInformation();
const gangNames = Object.keys(info);
@@ -35,7 +41,7 @@ describe("ns.gang.getAllGangInformation", () => {
}
});
- it("should return copies, not references to the original AllGangs data", () => {
+ test("should return copies, not references to the original AllGangs data", () => {
const ns = getNS();
const info = ns.gang.getAllGangInformation();
@@ -44,3 +50,55 @@ describe("ns.gang.getAllGangInformation", () => {
expect(AllGangs[FactionName.SlumSnakes].power).not.toBe(999999);
});
});
+
+describe("createGang", () => {
+ test("Success", () => {
+ const ns = getNS();
+ Player.karma = GangConstants.GangKarmaRequirement;
+ joinFaction(Factions[FactionName.SlumSnakes]);
+ expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(true);
+ });
+ describe("Failure", () => {
+ test("Already have a gang", () => {
+ const { ws, ns } = getWorkerScriptAndNS();
+ Player.karma = GangConstants.GangKarmaRequirement;
+ joinFaction(Factions[FactionName.SlumSnakes]);
+ expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(true);
+
+ expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(false);
+ expect(ws.scriptRef.logs[0]).toMatch("You already have a gang");
+ expect(ns.gang.createGang(FactionName.Tetrads)).toBe(false);
+ expect(ws.scriptRef.logs[1]).toMatch("You already have a gang");
+ });
+ test("Disabled by advanced options", () => {
+ const { ws, ns } = getWorkerScriptAndNS();
+ Player.bitNodeOptions.disableGang = true;
+ expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(false);
+ expect(ws.scriptRef.logs[0]).toMatch("Gang is disabled by advanced options");
+ });
+ test("Not have Source-File 2", () => {
+ const { ws, ns } = getWorkerScriptAndNS();
+ Player.sourceFiles.set(2, 0);
+ expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(false);
+ expect(ws.scriptRef.logs[0]).toMatch("You do not have Source-File 2");
+ });
+ test("Not enough karma", () => {
+ const { ws, ns } = getWorkerScriptAndNS();
+ expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(false);
+ expect(ws.scriptRef.logs[0]).toMatch("Your karma must be less than or equal to");
+ });
+ test("Invalid gang faction", () => {
+ const { ws, ns } = getWorkerScriptAndNS();
+ Player.karma = GangConstants.GangKarmaRequirement;
+ joinFaction(Factions[FactionName.SlumSnakes]);
+ expect(ns.gang.createGang(FactionName.Illuminati)).toBe(false);
+ expect(ws.scriptRef.logs[0]).toMatch("does not allow creating a gang");
+ });
+ test("Not a faction member", () => {
+ const { ws, ns } = getWorkerScriptAndNS();
+ Player.karma = GangConstants.GangKarmaRequirement;
+ expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(false);
+ expect(ws.scriptRef.logs[0]).toMatch("You are not a member of");
+ });
+ });
+});
diff --git a/test/jest/Terminal/tabCompletion.test.ts b/test/jest/Terminal/tabCompletion.test.ts
index 8498ef4e3..e2e6677a7 100644
--- a/test/jest/Terminal/tabCompletion.test.ts
+++ b/test/jest/Terminal/tabCompletion.test.ts
@@ -178,9 +178,9 @@ describe("getTabCompletionPossibilities", function () {
}
});
- it("completes the ls and cd commands", async () => {
+ it("completes the ls, cd and upload commands", async () => {
writeFiles();
- for (const command of ["ls", "cd"]) {
+ for (const command of ["ls", "cd", "upload"]) {
const options = await getTabCompletionPossibilities(`${command} `, root);
expect(options.sort()).toEqual(["folder1/", "anotherFolder/"].sort());
}