DARKNET: Darkweb Expansion Project & Bitnode (#2139)

This is BN15. It is a really big change; see the PR for all the details.
This commit is contained in:
Michael Ficocelli
2026-02-03 06:40:36 -05:00
committed by GitHub
parent a674633f6c
commit 6073964768
225 changed files with 15010 additions and 526 deletions

View File

@@ -136,6 +136,11 @@
"Name": "IPvGO Subnet Takeover",
"Description": "Acquire SF14.1"
},
"SF15.1": {
"ID": "SF15.1",
"Name": "The Secrets of the Dark Net",
"Description": "Acquire SF15.1"
},
"MONEY_1Q": {
"ID": "MONEY_1Q",
"Name": "Here comes the money!",
@@ -406,6 +411,11 @@
"Name": "Ten Steps Ahead",
"Description": "Get a winning streak of 10 against Illuminati."
},
"DARKNET_BACKDOOR": {
"ID": "DARKNET_BACKDOOR",
"Name": "Make your Own Network",
"Description": "Install a backdoor on 50 or more darknet servers at once."
},
"CHALLENGE_BN1": {
"ID": "CHALLENGE_BN1",
"Name": "BN1: Challenge",
@@ -461,6 +471,11 @@
"Name": "BN14: Challenge",
"Description": "Destroy BN14 without making a move or cheating via IPvGO APIs."
},
"CHALLENGE_BN15": {
"ID": "CHALLENGE_BN15",
"Name": "BN15: Challenge",
"Description": "Open the cache on the final lab in the darknet."
},
"BYPASS": {
"ID": "BYPASS",
"Name": "Exploit: bypass",

View File

@@ -37,6 +37,8 @@ import { activateSteamAchievements } from "../Electron";
import { Go } from "../Go/Go";
import { type AchievementId, type SFAchievementId, SFAchievementIds } from "./Types";
import { getAllMovableDarknetServers } from "../DarkNet/utils/darknetNetworkUtils";
function assertAchievements(
achievements: typeof data.achievements,
): asserts achievements is AchievementDataJson["achievements"] {
@@ -66,7 +68,7 @@ function assertAchievements(
* - Typechecking at compile time: ID must be AchievementId, not string.
* - Runtime check: The value of ID must be the same as the key of the achievement. For example, with "CYBERSEC"
* achievement, the key is "CYBERSEC", so its ID must also be "CYBERSEC".
*
*
* We use assertAchievements to do the runtime check and assert the type.
*/
const achievementData = data.achievements;
@@ -582,6 +584,13 @@ export const achievements: Record<AchievementId, Achievement> = {
Condition: () => false,
NotInSteam: true,
},
DARKNET_BACKDOOR: {
...achievementData.DARKNET_BACKDOOR,
Icon: "darknet-backdoor",
Visible: knowAboutBitverse,
Condition: () => getAllMovableDarknetServers().filter((s) => s.backdoorInstalled).length >= 50,
NotInSteam: true,
},
CHALLENGE_BN1: {
...achievementData.CHALLENGE_BN1,
Icon: "BN1+",
@@ -672,6 +681,13 @@ export const achievements: Record<AchievementId, Achievement> = {
Condition: () => Player.bitNodeN === 14 && isBitNodeFinished() && !Go.moveOrCheatViaApi,
NotInSteam: true,
},
CHALLENGE_BN15: {
...achievementData.CHALLENGE_BN15,
Icon: "BN15+",
Visible: knowAboutBitverse,
Condition: () => Player.augmentations.some((a) => a.name === AugmentationName.TheSword),
NotInSteam: true,
},
BYPASS: {
...achievementData.BYPASS,
Icon: "SF-1",

View File

@@ -16,5 +16,6 @@ export const SFAchievementIds = [
"SF12.1",
"SF13.1",
"SF14.1",
"SF15.1",
] as const;
export type SFAchievementId = (typeof SFAchievementIds)[number];

View File

@@ -19,6 +19,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
"triggers feelings of admiration and approval in other people.",
company_rep: 1.1,
faction_rep: 1.1,
charisma_exp: 1.05,
factions: [
FactionName.TianDiHui,
FactionName.TheSyndicate,
@@ -36,6 +37,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
"triggers feelings of admiration, approval, and respect in others.",
company_rep: 1.2,
faction_rep: 1.2,
charisma: 1.2,
factions: [
FactionName.Silhouette,
FactionName.FourSigma,
@@ -76,6 +78,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
"Pheromone extruder injected in the thoracodorsal nerve. Emits 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,
isSpecial: true,
factions: [FactionName.ShadowsOfAnarchy],
},
@@ -514,9 +517,22 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
"millions of nanobots capable of projecting high-density muon beams, " +
"creating an energy barrier around the user.",
defense: 1.4,
charisma: 1.1,
factions: [FactionName.Volhaven],
},
// === E === //
[AugmentationName.Eloquence]: {
repCost: 2.5e4,
moneyCost: 2.5e8,
info:
"A neural implant that enhances the user's ability to resonate with others. " +
"It is capable of analyzing and interpreting the emotions of those nearby, allowing " +
"the user to better understand and influence them.",
charisma: 1.1,
crime_success: 1.1,
work_money: 1.2,
factions: [FactionName.SpeakersForTheDead],
},
[AugmentationName.EMS4Recombination]: {
repCost: 2.5e3,
moneyCost: 2.75e8,
@@ -738,6 +754,28 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
],
},
// === G === //
[AugmentationName.Glib]: {
repCost: 4.05e4,
moneyCost: 2.5e9,
info:
"An implant that, when activated, makes the speaker sound unbelievably reasonable and plausible to listeners " +
"for the next hour. It requires no concentration from the user, and only a verbal activation component. It even affects " +
"most electronic detection methods.",
charisma_exp: 1.2,
company_rep: 1.1,
factions: [FactionName.Tetrads, FactionName.Bladeburners],
},
[AugmentationName.GoldenTongue]: {
repCost: 1.25e5,
moneyCost: 1.25e8,
info:
"An aural implant that enhances the user's ability to communicate and persuade others. " +
"The implant uses a predictive model that lets the user say precisely what their audience " +
"wants to hear. This implant is commonly used by many high-level executives and government officials.",
charisma: 1.2,
charisma_exp: 1.3,
factions: [FactionName.SpeakersForTheDead],
},
[AugmentationName.GolemSerum]: {
repCost: 3.125e4,
moneyCost: 1.1e10,
@@ -873,6 +911,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
defense: 1.08,
agility: 1.08,
dexterity: 1.08,
charisma: 1.08,
factions: [FactionName.Tetrads, FactionName.TheDarkArmy, FactionName.TheSyndicate],
},
[AugmentationName.HiveMind]: {
@@ -906,6 +945,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
"Even though it contains no weapons, the advanced tungsten titanium " +
"alloy increases the user's strength to unbelievable levels.",
strength: 2.8,
charisma: 1.4,
factions: [FactionName.NWO],
},
[AugmentationName.HyperionV1]: {
@@ -945,6 +985,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
dexterity: 1.4,
hacking_speed: 1.03,
hacking_money: 1.1,
charisma: 1.05,
factions: [FactionName.BladeIndustries, FactionName.KuaiGongInternational],
},
// === I === //
@@ -992,6 +1033,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
"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,
charisma: 1.05,
crime_money: 1.1,
factions: [FactionName.SlumSnakes, FactionName.Tetrads],
},
@@ -1006,10 +1048,19 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
prereqs: [AugmentationName.LuminCloaking1],
agility: 1.1,
defense: 1.1,
charisma_exp: 1.1,
crime_money: 1.25,
factions: [FactionName.SlumSnakes, FactionName.Tetrads],
},
// === M === //
[AugmentationName.Magnetism]: {
repCost: 1.5e4,
moneyCost: 2.5e8,
info: "A cranial implant that increases the attractive force of the wearer. (Even its inventor isn't quite sure how it works).",
charisma: 1.1,
company_rep: 1.1,
factions: [FactionName.TheBlackHand, FactionName.TheDarkArmy],
},
[AugmentationName.MightOfAres]: {
repCost: 1e4,
moneyCost: 1e6,
@@ -1030,6 +1081,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
"which improves its regenerative and extracellular homeostasis abilities.",
strength: 1.2,
defense: 1.2,
charisma: 1.1,
factions: [
FactionName.TheDarkArmy,
FactionName.TheSyndicate,
@@ -1050,6 +1102,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
"and restructure themselves.",
strength: 1.55,
defense: 1.55,
charisma: 1.55,
factions: [FactionName.BladeIndustries],
},
[AugmentationName.NeuralAccelerator]: {
@@ -1081,6 +1134,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
hacking_speed: 1.02,
hacking_chance: 1.1,
hacking_exp: 1.12,
charisma: 1.05,
factions: [
FactionName.TheBlackHand,
FactionName.Chongqing,
@@ -1240,6 +1294,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
"the bloodstream to improve memory, increase focus, and provide other " +
"cognitive enhancements.",
company_rep: 1.2,
charisma: 1.05,
factions: [
FactionName.TianDiHui,
FactionName.Volhaven,
@@ -1377,6 +1432,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
strength: 1.4,
defense: 1.4,
agility: 1.4,
charisma: 1.4,
factions: [FactionName.KuaiGongInternational],
},
[AugmentationName.PowerRecirculator]: {
@@ -1400,6 +1456,16 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
charisma_exp: 1.1,
factions: [FactionName.Tetrads, FactionName.TheDarkArmy, FactionName.TheSyndicate, FactionName.NWO],
},
[AugmentationName.Primer]: {
repCost: 1.875e5,
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.",
charisma: 1.2,
charisma_exp: 1.4,
factions: [FactionName.TheDarkArmy, FactionName.TheSyndicate],
},
// === Q === //
[AugmentationName.QLink]: {
repCost: 1.875e6,
@@ -1422,11 +1488,22 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
info:
"A cranial implant that affects the user's personality, making them better " +
"at negotiation in social situations.",
charisma_exp: 1.15,
work_money: 1.1,
company_rep: 1.15,
faction_rep: 1.15,
factions: [FactionName.TianDiHui],
},
[AugmentationName.SocialDynamo]: {
repCost: 2.25e5,
moneyCost: 1.2e9,
info:
"Makes the wearer a better leader and mentor by greatly increasing their awareness of social dynamics. " +
"Not actually a standard implant, but rather a series of training courses and seminars, led by a famous speaker named Denis.",
charisma: 1.3,
company_rep: 1.3,
factions: [FactionName.MegaCorp, FactionName.ECorp, FactionName.OmniTekIncorporated],
},
[AugmentationName.SPTN97]: {
repCost: 1.25e6,
moneyCost: 4.875e9,
@@ -1451,6 +1528,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
"criminal organizations and allows the user to project and control holographic " +
"simulacrums within a large radius. These simulacrums are commonly used for " +
"espionage and surveillance work.",
charisma: 1.15,
company_rep: 1.15,
faction_rep: 1.15,
factions: [FactionName.TheSyndicate, FactionName.TheDarkArmy, FactionName.SpeakersForTheDead],
@@ -1471,7 +1549,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
[AugmentationName.SmartSonar]: {
repCost: 2.25e4,
moneyCost: 7.5e7,
info: "A cochlear implant that helps the user detect and locate enemies using sound propagation.",
info: "A cochlear implant that helps the player detect and locate enemies using sound propagation.",
dexterity: 1.1,
dexterity_exp: 1.15,
crime_money: 1.25,
@@ -1665,6 +1743,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
"Scientists have named these artificially enhanced units 'synfibrils'.",
strength: 1.3,
defense: 1.3,
charisma: 1.15,
factions: [
FactionName.KuaiGongInternational,
FactionName.FulcrumSecretTechnologies,
@@ -1684,6 +1763,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
"more efficiently than an organic heart.",
agility: 1.5,
strength: 1.5,
charisma: 1.5,
factions: [
FactionName.KuaiGongInternational,
FactionName.FulcrumSecretTechnologies,
@@ -1771,6 +1851,83 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
hacking_money: 1.1,
factions: [FactionName.TheBlackHand],
},
[AugmentationName.TheBrokenWings]: {
repCost: 1e4,
moneyCost: 1e6,
info:
"An experimental augmentation that lets the user make incredible leaps of insight and flights of fancy. " +
"Created by a mysterious figure known only as 'The Sculptor', this augmentation appears as a set of silvery " +
"metallic patterns on the user's upper back and shoulders. " +
"Awarded to those who discover the secrets of the labyrinth.",
stats: "This augmentation increases the stasis link limit by one, and raises charisma by 15% and agility by 10%.",
charisma: 1.15,
agility: 1.1,
isSpecial: true,
factions: [],
},
[AugmentationName.TheBoots]: {
repCost: 1e4,
moneyCost: 1e6,
info:
"Modeled after the winged boots of mythology, this implant somehow provides tireless social energy to the user. " +
"Its creator, the enigmatic Sculptor, refuses to reveal the details of how it works, and only mutters about 'liveware APIs'. " +
"Awarded to those who discover the secrets of the labyrinth.",
stats:
"This augmentation increases the speed of authentication and heartbleed by 20%, and raises charisma and dexterity by 20%.",
charisma: 1.2,
dexterity: 1.2,
isSpecial: true,
prereqs: [AugmentationName.TheBrokenWings],
factions: [],
},
[AugmentationName.TheHammer]: {
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. " +
"Awarded to those who discover the secrets of the labyrinth.",
stats:
"This augmentation increases the stasis link limit by one, and raises charisma by 35% and strength by 25%.",
charisma: 1.35,
strength: 1.25,
isSpecial: true,
prereqs: [AugmentationName.TheBoots],
factions: [],
},
[AugmentationName.TheLaw]: {
repCost: 1e4,
moneyCost: 1e6,
info:
"An advanced neural implant that integrates Bayesian inference algorithms into the brain's decision-making processes. " +
"This augmentation enhances the user's ability to assess probabilities, predict outcomes, and adapt strategies in real-time, " +
"making them exceptionally persuasive and confident in negotiations and social interactions. " +
"Awarded to those who discover the secrets of the labyrinth.",
stats: "This augmentation raises charisma by 40%, hacking by 10%, and company rep by 5%.",
charisma: 1.4,
hacking: 1.1,
company_rep: 1.05,
isSpecial: true,
prereqs: [AugmentationName.TheHammer],
factions: [],
},
[AugmentationName.TheSword]: {
repCost: 1e4,
moneyCost: 1e6,
info:
"A cutting-edge neural implant that leverages Solomonoff induction to analyze and predict patterns with unparalleled precision. " +
"This augmentation enhances the user's ability to deduce optimal strategies and make compelling arguments, " +
"turning every interaction into a calculated success. The technique is sometimes referred to as Solomonoff's Lightsaber, as it is a " +
"more powerful version of Occam's razor. " +
"The final augment awarded to those who discover the secrets of the labyrinth.",
stats: "This augmentation raises charisma by 50%, hacking by 16%, and company rep by 10%.",
charisma: 1.5,
hacking: 1.16,
company_rep: 1.1,
isSpecial: true,
prereqs: [AugmentationName.TheLaw],
factions: [],
},
[AugmentationName.TheRedPill]: {
repCost: 2.5e6,
moneyCost: 0,
@@ -1797,6 +1954,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
"A synthetic symbiotic virus that is injected into human brain tissue. The Vangelis virus " +
"heightens the senses and focus of its host while also enhancing their intuition.",
dexterity_exp: 1.1,
charisma_exp: 1.1,
bladeburner_analysis: 1.1,
bladeburner_success_chance: 1.04,
isSpecial: true,
@@ -1813,6 +1971,7 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
prereqs: [AugmentationName.VangelisVirus],
defense_exp: 1.1,
dexterity_exp: 1.1,
charisma_exp: 1.1,
bladeburner_analysis: 1.15,
bladeburner_success_chance: 1.05,
isSpecial: true,
@@ -1858,6 +2017,17 @@ export const Augmentations: Record<AugmentationName, Augmentation> = (() => {
isSpecial: true,
factions: [FactionName.ShadowsOfAnarchy],
},
[AugmentationName.Wit]: {
repCost: 5e3,
moneyCost: 1e7,
info:
"A connective brain implant that greatly increases the user's speech reaction time. " +
"This allows the user to think faster and respond quicker in negotiations, and always have the last word.",
charisma: 1.08,
charisma_exp: 1.1,
company_rep: 1.05,
factions: [FactionName.SlumSnakes, FactionName.BitRunners],
},
// === X === //
[AugmentationName.Xanipher]: {
repCost: 8.75e5,

View File

@@ -94,6 +94,13 @@ export enum AugmentationName {
HydroflameLeftArm = "Hydroflame Left Arm",
BigDsBigBrain = "BigD's Big ... Brain",
ZOE = "Z.O.Ë.",
Eloquence = "Eloquence Module",
GoldenTongue = "Golden Tongue Module",
Glib = "Glibness Enhancement",
Magnetism = "Magnetism Amplifier",
Primer = "The Illustrated Primer",
SocialDynamo = "Social Dynamics Processor",
Wit = "Neural Wit Amplifier",
// UnnamedAug2 = "UnnamedAug2",
// Bladeburner augs
@@ -115,10 +122,18 @@ export enum AugmentationName {
BladeArmorIPU = "BLADE-51b Tesla Armor: IPU Upgrade",
BladesSimulacrum = "The Blade's Simulacrum",
// Stanek Augs
StaneksGift1 = "Stanek's Gift - Genesis",
StaneksGift2 = "Stanek's Gift - Awakening",
StaneksGift3 = "Stanek's Gift - Serenity",
// Darknet lab augs (in order of acquisition)
TheBrokenWings = "The W1ngs of Icarus",
TheBoots = "The B00ts of Perseus",
TheHammer = "The H4mmer of Daedalus",
TheLaw = "The L4w of Bayes",
TheSword = "The B1ade of Solomonoff",
// Infiltrators MiniGames
MightOfAres = "SoA - Might of Ares", // slash
WisdomOfAthena = "SoA - Wisdom of Athena", // bracket

View File

@@ -1,6 +1,6 @@
import React from "react";
import { Player } from "@player";
import { AugmentationName, CityName, FactionName } from "@enums";
import { AugmentationName, CityName, CompletedProgramName, FactionName } from "@enums";
import { BitNodeMultipliers, replaceCurrentNodeMults } from "./BitNodeMultipliers";
class BitNode {
@@ -194,7 +194,7 @@ export function initBitNodes() {
<li>
<code>getBitNodeMultipliers()</code> Netscript function
</li>
<li>Permanent access to Formulas.exe</li>
<li>Permanent access to {CompletedProgramName.formulas}</li>
<li>
Access to BitNode multiplier information on the <b>Stats</b> page
</li>
@@ -516,6 +516,49 @@ export function initBitNodes() {
</>
),
);
BitNodes.BitNode15 = new BitNode(
15,
"The Secrets of the Dark Net",
"The rules have changed",
(
<>
<br />
Delving into the uncharted and secretive parts of the internet comes with the promise of freedom from oppressive
authority and surveillance. Leaving stability behind and turning to the dark web comes with risks... but also
rewards.
<br />
<br />
Unlike the traditional network of servers, the "dark" net is a constantly shifting, complex, unreliable place
where servers can move or disappear at any moment. Long-distance communication is often impossible, requiring
scripts to be self-sufficient and durable, and spread themselves to stay alive. If you can take advantage of the
darknet servers' weak passwords and leaky logs, you will be able to gain access to the deepest parts of the
darknet and its secrets.
<br />
<br />
In this Bitnode, the Daedalus faction has not yet found and monopolized the fabled Red Pill augmentation.
Legends say it can be found somewhere, out there in the dark...
<br />
<br />
Destroying this BitNode will give you Source-File 15, or if you already have this Source-File, it will upgrade
its level up to a maximum of 3.
</>
),
(
<>
This Source-File grants the following benefits:
<ul>
<li>Level 1: Permanently start with the TOR router and {CompletedProgramName.darkscape}</li>
<li>
Level 2: Your charisma level increases job salary and rep gain. Also increases authentication speed by 20%
</li>
<li>
Level 3: Your charisma level increases faction work rep gain. Also increases the xp and money gained from
.cache files by 50%.
</li>
</ul>
</>
),
);
}
export const defaultMultipliers = new BitNodeMultipliers();
@@ -737,6 +780,9 @@ export function getBitNodeMultipliers(n: number, lvl: number): BitNodeMultiplier
BladeburnerRank: 0,
DarknetLabyrinthRewardsTheRedPill: 0,
DarknetMoneyMultiplier: 0,
GangSoftcap: 0,
GangUniqueAugs: 0,
@@ -1024,6 +1070,41 @@ export function getBitNodeMultipliers(n: number, lvl: number): BitNodeMultiplier
WorldDaemonDifficulty: 5,
});
}
case 15: {
return new BitNodeMultipliers({
HackingLevelMultiplier: 0.6,
HackingSpeedMultiplier: 0.6,
StrengthLevelMultiplier: 0.7,
DefenseLevelMultiplier: 0.7,
DexterityLevelMultiplier: 0.7,
AgilityLevelMultiplier: 0.7,
CharismaLevelMultiplier: 1.1,
ServerMaxMoney: 0.8,
ServerStartingMoney: 0.5,
ServerStartingSecurity: 1.5,
AugmentationMoneyCost: 3,
CorporationValuation: 0.2,
CorporationSoftcap: 0.4,
CorporationDivisions: 0.4,
DaedalusAugsRequirement: 20,
BladeburnerRank: 0.2,
BladeburnerSkillCost: 3,
GangUniqueAugs: 0.3,
StaneksGiftPowerMultiplier: 0.7,
StaneksGiftExtraSize: -2,
WorldDaemonDifficulty: 2,
});
}
default: {
throw new Error("Invalid BitNodeN");
}

View File

@@ -60,6 +60,12 @@ export class BitNodeMultipliers {
/** Influences how many Augmentations you need in order to get invited to the Daedalus faction */
DaedalusAugsRequirement = 30;
/** If true, TRP can be found in the fourth lab deep in the darknet. */
DarknetLabyrinthRewardsTheRedPill = 1;
/** Influences how much money the player gains from darknet mechanics (phishing and reward caches). */
DarknetMoneyMultiplier = 1;
/** Influences how quickly the player's defense level (not exp) scales */
DefenseLevelMultiplier = 1;

View File

@@ -1 +1 @@
export const validBitNodes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
export const validBitNodes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];

View File

@@ -245,7 +245,7 @@ export function BitverseRoot(props: IProps): React.ReactElement {
<BitVerseMapRow> O </BitVerseMapRow>
<BitVerseMapRow> | O O | O O | </BitVerseMapRow>
<BitVerseMapRow> O | | / __| \ | | O </BitVerseMapRow>
<BitVerseMapRow> O | O | | O / | O | | O | O </BitVerseMapRow>
<BitVerseMapRow> O | O | | <BitNodePortal n={15} level={n(15)} flume={props.flume} destroyedBitNode={destroyed} /> / | O | | O | O </BitVerseMapRow>
<BitVerseMapRow> | | | | |_/ |/ | \_ \_| | | | | </BitVerseMapRow>
<BitVerseMapRow> O | | | <BitNodePortal n={14} level={n(14)} flume={props.flume} destroyedBitNode={destroyed} /> | | O__/ | / \__ | | O | | | O </BitVerseMapRow>
<BitVerseMapRow> | | | | | | | / /| O / \| | | | | | | </BitVerseMapRow>

View File

@@ -6,6 +6,7 @@
export const CONSTANTS = {
VersionString: "3.0.0dev",
isDevBranch: true,
isInTestEnvironment: globalThis.process?.env?.JEST_WORKER_ID !== undefined,
VersionNumber: 44,
/** Max level for any skill, assuming no multipliers. Determined by max numerical value in javascript for experience

11
src/DarkNet/Constants.ts Normal file
View File

@@ -0,0 +1,11 @@
export const DarknetConstants = {
MinCyclesToProcess: 1,
MaxCyclesToProcess: 3,
DataFileSuffix: ".data.txt",
/** Discounted price when buying in Shadowed Walkway */
DarkscapeNavigatorDiscountedPrice: 30e6,
/** Standard price when buying via darkweb */
DarkscapeNavigatorPrice: 50e6,
} as const;
export const MAX_PASSWORD_LENGTH = 50;

11
src/DarkNet/DWRoot.tsx Normal file
View File

@@ -0,0 +1,11 @@
import React from "react";
import { Container } from "@mui/material";
import { NetworkDisplayWrapper } from "./ui/NetworkDisplayWrapper";
export function DWRoot(): React.ReactElement {
return (
<Container disableGutters maxWidth={false} sx={{ mx: 0 }}>
<NetworkDisplayWrapper />
</Container>
);
}

90
src/DarkNet/Enums.ts Normal file
View File

@@ -0,0 +1,90 @@
import type { _ValueOf, DarknetServerData } from "@nsdefs";
export const HORIZONTAL_CONNECTION_CHANCE = 0.5;
export const VERTICAL_CONNECTION_CHANCE = 0.3;
export const AIR_GAP_DEPTH = 8;
export const NET_WIDTH = 8;
export const MAX_NET_DEPTH = 40;
export const SERVER_DENSITY = 0.6;
export const LOW_LEVEL_SERVER_DENSITY = 0.7;
export const MS_PER_MUTATION_PER_ROW = 30_000; // 30 seconds
// each minigame needs to have a name that sounds like a device or browser or language model and version
// (This list is not exposed to the player; they find them through discovery)
export const ModelIds = {
EchoVuln: "DeskMemo_3.1",
SortedEchoVuln: "PHP 5.4",
NoPassword: "ZeroLogon",
Captcha: "CloudBlare(tm)",
DefaultPassword: "FreshInstall_1.0",
BufferOverflow: "Pr0verFl0",
MastermindHint: "DeepGreen",
TimingAttack: "2G_cellular",
LargestPrimeFactor: "PrimeTime 2",
RomanNumeral: "BellaCuore",
DogNames: "Laika4",
GuessNumber: "AccountsManager_4.2",
CommonPasswordDictionary: "TopPass",
EUCountryDictionary: "EuroZone Free",
Yesn_t: "NIL",
BinaryEncodedFeedback: "110100100",
SpiceLevel: "RateMyPix.Auth",
ConvertToBase10: "OctantVoxel",
parsedExpression: "MathML",
divisibilityTest: "Factori-Os",
tripleModulo: "BigMo%od",
globalMaxima: "KingOfTheHill",
packetSniffer: "OpenWebAccessPoint",
encryptedPassword: "OrdoXenos",
labyrinth: "(The Labyrinth)",
} as const;
export type MinigamesType = _ValueOf<typeof ModelIds>;
export const GenericResponseMessage = {
Success: "Success",
DirectConnectionRequired: "Direct Connection Required",
AuthFailure: "Unauthorized",
NotFound: "Not Found",
RequestTimeOut: "Request Timeout",
NotEnoughCharisma: "Not Enough Charisma",
StasisLinkLimitReached: "Stasis Link Limit Reached",
NoBlockRAM: "No Host-owned RAM Left To Reallocate",
ServiceUnavailable: "Service Unavailable",
} as const;
export const ResponseCodeEnum = {
Success: 200,
DirectConnectionRequired: 351,
AuthFailure: 401,
Forbidden: 403,
NotFound: 404,
RequestTimeOut: 408,
NotEnoughCharisma: 451,
StasisLinkLimitReached: 453,
NoBlockRAM: 454,
PhishingFailed: 455,
ServiceUnavailable: 503,
} as const;
export const exampleDarknetServerData: DarknetServerData = {
hostname: "",
ip: "",
hasAdminRights: false,
isConnectedTo: false,
cpuCores: 1,
ramUsed: 0,
maxRam: 0,
backdoorInstalled: false,
hasStasisLink: false,
blockedRam: 0,
modelId: "",
staticPasswordHint: "",
passwordHintData: "",
difficulty: 0,
depth: -1,
requiredCharismaSkill: 0,
logTrafficInterval: -1,
isStationary: false,
purchasedByPlayer: false,
} as const;

View File

@@ -0,0 +1,255 @@
import { DarknetState } from "../models/DarknetState";
import {
AddToAllServers,
connectServers,
createUniqueRandomIp,
DeleteServer,
disconnectServers,
GetServer,
} from "../../Server/AllServers";
import {
addGuaranteedConnection,
addRandomDarknetServers,
balanceDarknetServers,
deleteDarknetServer,
disconnectServer,
} from "./NetworkMovement";
import { SpecialServers } from "../../Server/data/SpecialServers";
import { Player } from "@player";
import { Terminal } from "../../Terminal";
import {
getLabyrinthChaRequirement,
getLabyrinthDetails,
getLabyrinthServerNames,
getNetDepth,
isLabyrinthServer,
} from "../effects/labyrinth";
import { DarknetServer, type DarknetServerConstructorParams } from "../../Server/DarknetServer";
import {
HORIZONTAL_CONNECTION_CHANCE,
MAX_NET_DEPTH,
ModelIds,
NET_WIDTH,
SERVER_DENSITY,
VERTICAL_CONNECTION_CHANCE,
} from "../Enums";
import { DarknetServerOptions, DnetServerBuilder } from "../models/DarknetServerOptions";
import {
getAllMovableDarknetServers,
getNeighborsOnRow,
getServersOnRowAbove,
getServersOnRowBelow,
} from "../utils/darknetNetworkUtils";
import { getDarknetServer } from "../utils/darknetServerUtils";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import { JSONMap } from "../../Types/Jsonable";
import type { ScriptFilePath } from "../../Paths/ScriptFilePath";
import type { Script } from "../../Script/Script";
import type { CodingContract } from "../../CodingContract/Contract";
import { getTorRouter } from "../../Server/ServerHelpers";
export function initDarkwebServer(): void {
const existingServer = GetServer(SpecialServers.DarkWeb);
if (existingServer instanceof DarknetServer) {
return;
}
let scripts = new JSONMap<ScriptFilePath, Script>();
let contracts: CodingContract[] = [];
const hasTOR = Player.hasTorRouter();
if (existingServer) {
scripts = existingServer.scripts;
contracts = existingServer.contracts;
// Remove legacy darkweb server, so it can be made into a darknet server
disconnectServers(existingServer, Player.getHomeComputer());
DeleteServer(existingServer.hostname);
}
const data: DarknetServerOptions = {
password: "",
modelId: ModelIds.NoPassword,
staticPasswordHint: "There is no password",
leftOffset: -1,
depth: -1,
difficulty: 0,
};
const darkweb = DnetServerBuilder(data, SpecialServers.DarkWeb);
darkweb.isStationary = true;
darkweb.hasAdminRights = true;
darkweb.blockedRam = 0;
darkweb.scripts = scripts;
darkweb.contracts = contracts;
if (hasTOR) {
getTorRouter();
}
}
export const populateDarknet = () => {
initDarkwebServer();
if (getAllMovableDarknetServers().length) {
loadDarknet();
return;
}
clearDarknet(true);
addLabyrinth();
addRandomDarknetServers(getNetDepth() * NET_WIDTH * SERVER_DENSITY - 10);
addRandomDarknetServers(5 - DarknetState.Network[0].length);
addRandomDarknetServers(5 - DarknetState.Network[1].length);
balanceDarknetServers();
const updatedServers = getAllMovableDarknetServers();
for (let i = 0; i < getNetDepth(); i++) {
const server = updatedServers[Math.floor(Math.random() * updatedServers.length)];
addGuaranteedConnection(server);
}
};
export const clearDarknet = (force = false) => {
movePlayerIfNeeded();
for (let i = 0; i < MAX_NET_DEPTH; i++) {
for (let j = 0; j < NET_WIDTH; j++) {
const server = DarknetState.Network[i]?.[j];
if (!server) continue;
deleteDarknetServer(server, force);
DarknetState.Network[i][j] = null;
}
}
const darkwebRoot = GetServer(SpecialServers.DarkWeb);
if (darkwebRoot) {
darkwebRoot.serversOnNetwork = [Player.getHomeComputer().hostname];
}
for (const lab of getLabyrinthServerNames()) {
const labyrinth = getDarknetServer(lab);
if (!labyrinth) continue;
deleteDarknetServer(labyrinth);
}
DarknetState.zoomIndex = 7;
DarknetState.netViewLeftScroll = 0;
DarknetState.netViewTopScroll = 0;
DarknetState.allowMutating = true;
DarknetState.openServer = null;
DarknetState.stockPromotions = {};
DarknetState.migrationInductionServers = {};
DarknetState.serverState = {};
};
export const movePlayerIfNeeded = (server?: DarknetServer) => {
const connectedServer = Player.getCurrentServer();
if ((!server && connectedServer instanceof DarknetServer) || server?.hostname === connectedServer.hostname) {
Terminal.print(`Something seems to have happened to '${connectedServer.hostname}'...`);
Terminal.connectToServer(SpecialServers.Home);
}
};
/**
* Loads all the darknet servers into DarknetState.Network, if it is not already populated
*/
export const loadDarknet = () => {
const currentServers = DarknetState.Network.flat().filter((s) => s !== null && !s.isStationary);
if (currentServers.length) {
return;
}
const darkNetServers = getAllMovableDarknetServers();
for (const server of darkNetServers) {
if (isLabyrinthServer(server.hostname)) {
continue;
}
disconnectServer(server, true);
addServerToNetwork(server, server.depth, server.leftOffset);
}
balanceDarknetServers();
const updatedServers = getAllMovableDarknetServers();
for (let i = 0; i < getNetDepth(); i++) {
const server = updatedServers[Math.floor(Math.random() * updatedServers.length)];
addGuaranteedConnection(server);
}
};
export const addRandomConnections = (server: DarknetServer) => {
if (isLabyrinthServer(server.hostname)) {
return;
}
const x = server.depth;
const y = server.leftOffset;
const horizontalNeighbors = getNeighborsOnRow(x, y);
horizontalNeighbors.forEach((neighbor) => {
if (Math.random() < HORIZONTAL_CONNECTION_CHANCE) {
connectServers(server, neighbor);
}
});
const serversAbove = getServersOnRowAbove(x);
const serversBelow = getServersOnRowBelow(x);
[...serversAbove, ...serversBelow].forEach((neighbor) => {
const distance = Math.abs(neighbor.depth ?? x - x) + 1;
if (Math.random() < VERTICAL_CONNECTION_CHANCE / distance) {
connectServers(server, neighbor);
}
});
};
export const addServerToNetwork = (server: DarknetServer, x: number, y: number) => {
if (DarknetState.Network[x][y]?.hostname) {
exceptionAlert(
`Server already exists at this coordinate. Hostname: ${DarknetState.Network[x][y].hostname}. Coordinate: ${x}-${y}`,
true,
);
return;
}
DarknetState.Network[x][y] = server;
server.depth = x;
server.leftOffset = y;
addRandomConnections(server);
addGuaranteedConnection(server);
if (server.depth === 0) {
const darkWebRoot = GetServer(SpecialServers.DarkWeb);
if (darkWebRoot) {
connectServers(server, darkWebRoot);
}
}
const maxDepth = getNetDepth();
if (server.depth === maxDepth - 1) {
const labyrinth = getLabyrinthDetails().lab;
if (labyrinth) {
connectServers(server, labyrinth);
}
}
};
// Creates all the special servers for use at the bottom of the dark net
export const addLabyrinth = () => {
const commonData: Omit<DarknetServerConstructorParams, "hostname" | "ip" | "password" | "requiredCharismaSkill"> = {
maxRam: 128,
modelId: ModelIds.labyrinth,
staticPasswordHint: "You have discovered a dark, mysterious maze. Your footsteps echo eerily in the silence.",
passwordHintData: "",
difficulty: 10,
depth: -1,
leftOffset: -1,
hasStasisLink: false,
blockedRam: 0,
logTrafficInterval: Number.MAX_SAFE_INTEGER,
isStationary: true,
};
for (const hostname of getLabyrinthServerNames()) {
const passwordSalt = Math.floor(Math.random() * 10000);
const server = new DarknetServer({
...commonData,
hostname: hostname,
ip: createUniqueRandomIp(),
password: `!!the:masterwork:of:daedalus<${passwordSalt}>!!`,
requiredCharismaSkill: getLabyrinthChaRequirement(hostname),
});
AddToAllServers(server);
}
};

View File

@@ -0,0 +1,397 @@
import { connectServers, DeleteServer, disconnectServers, GetServer } from "../../Server/AllServers";
import {
DarknetEvents,
DarknetState,
getServerState,
storeDarknetCycles,
triggerNextUpdate,
} from "../models/DarknetState";
import { createDarknetServer } from "./ServerGenerator";
import { addServerToNetwork, movePlayerIfNeeded } from "./NetworkGenerator";
import { killServerScripts } from "../../Netscript/killWorkerScript";
import { SpecialServers } from "../../Server/data/SpecialServers";
import { getLabyrinthServerNames, getNetDepth, isLabyrinthServer } from "../effects/labyrinth";
import { LOW_LEVEL_SERVER_DENSITY, MAX_NET_DEPTH, NET_WIDTH, SERVER_DENSITY } from "../Enums";
import {
getAllAdjacentNeighbors,
getAllDarknetServers,
getAllMovableDarknetServers,
getAllOpenPositions,
getBackdooredDarkwebServers,
getDarknetCyclesPerMutation,
getIslands,
} from "../utils/darknetNetworkUtils";
import { DarknetConstants } from "../Constants";
import type { DarknetServer } from "../../Server/DarknetServer";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
export const processDarknet = (cycles: number): void => {
storeDarknetCycles(cycles);
if (DarknetState.storedCycles < DarknetConstants.MinCyclesToProcess) {
return;
}
const cyclesToProcess = Math.min(DarknetState.storedCycles, DarknetConstants.MaxCyclesToProcess);
DarknetState.storedCycles -= cyclesToProcess;
const cyclesPerMutation = getDarknetCyclesPerMutation();
if (DarknetState.cyclesSinceLastMutation > cyclesPerMutation) {
DarknetState.cyclesSinceLastMutation = 0;
mutateDarknet();
}
};
export const mutateDarknet = (): void => {
if (!DarknetState.allowMutating) {
return;
}
const servers = getAllMovableDarknetServers();
if (servers.length === 0) {
return;
}
// resolve pending update promise, and create a new one
triggerNextUpdate();
// Limit mutation speed based on size of net
const depth = getNetDepth();
const depthSpeedFactor = 16 / depth;
if (Math.random() > depthSpeedFactor) {
return;
}
if (Math.random() < 0.3) {
const islands = getIslands();
const island = islands[Math.floor(Math.random() * islands.length)];
if (island) {
moveDarknetServer(island);
}
}
if (Math.random() < 0.3) {
// Ensure good density of low level servers for early progression
addLowLevelServersIfNeeded();
}
if (Math.random() < 0.1) {
// remove some servers
deleteRandomDarknetServers(Math.random() * 3 + 1);
}
if (Math.random() < 0.1) {
// Add some servers
const serversToAdd = Math.random() * 3 + 1;
for (let i = 0; i < serversToAdd; i++) {
addRandomDarknetServers();
}
return;
}
if (Math.random() < 0.1) {
const backdooredServers = getBackdooredDarkwebServers();
const server = backdooredServers[Math.floor(Math.random() * backdooredServers.length)];
if (server) {
restartServer(server);
return;
}
}
if (Math.random() < 0.05) {
const backdooredServers = getBackdooredDarkwebServers();
const server = backdooredServers[Math.floor(Math.random() * backdooredServers.length)];
if (server) {
deleteDarknetServer(server);
return;
}
}
if (Math.random() < 0.2) {
// restart a server
restartRandomServer();
}
if (Math.random() < 0.5) {
moveRandomDarknetServers(3);
}
if (Math.random() < 0.5) {
addConnectionsToRandomServer();
return;
}
if (Math.random() < 0.5) {
// delink all connections from a server
disconnectRandomServer();
}
if (Math.random() < 0.1) {
// balance network to stay at a certain density
balanceDarknetServers();
}
validateDarknetNetwork();
DarknetEvents.emit();
};
export const restartAllDarknetServers = (): void => {
const servers = getAllMovableDarknetServers();
for (const server of servers) {
restartServer(server);
}
};
export const moveRandomDarknetServers = (count = 1): void => {
for (let i = 0; i < count; i++) {
const servers = getAllMovableDarknetServers();
if (servers.length === 0) {
break;
}
const server = servers[Math.floor(Math.random() * servers.length)];
moveDarknetServer(server);
}
};
export const deleteRandomDarknetServers = (count = 1): void => {
for (let i = 0; i < count; i++) {
const servers = getAllMovableDarknetServers();
if (servers.length === 0) {
break;
}
const serverToDelete = servers[Math.floor(Math.random() * servers.length)];
deleteDarknetServer(serverToDelete);
}
};
export const deleteDarknetServer = (server: DarknetServer, force = false): void => {
if (server.hostname === SpecialServers.DarkWeb) {
exceptionAlert(new Error("Something is trying to delete darkweb"), true);
return;
}
if (isImmutable(server) && !force) {
return;
}
const isLabyrinth = isLabyrinthServer(server.hostname);
movePlayerIfNeeded(server);
killServerScripts(server, "Server shut down.");
disconnectServer(server, true);
if (DarknetState.Network[server.depth]?.[server.leftOffset]) {
DarknetState.Network[server.depth][server.leftOffset] = null;
}
if (!isLabyrinth) {
DarknetState.offlineServers.push(server.hostname);
}
DeleteServer(server.hostname);
const serverState = getServerState(server.hostname);
serverState.authenticatedPIDs = [];
serverState.serverLogs = [];
};
export const addRandomDarknetServers = (count = 1, difficulty?: number, fixedDepth?: boolean): void => {
for (let i = 0; i < count; i++) {
const diff = difficulty ?? Math.floor(Math.random() * getNetDepth());
const newServer = createDarknetServer(diff, -1, -1);
const range = fixedDepth ? 0 : 3;
moveDarknetServer(newServer, range, range);
if (DarknetState.offlineServers.includes(newServer.hostname)) {
DarknetState.offlineServers = DarknetState.offlineServers.filter((s) => s !== newServer.hostname);
}
}
};
export const addLowLevelServersIfNeeded = (): void => {
const lowLevelServers = getAllDarknetServers().filter((s) => s.depth <= 3);
const serversConnectedToDarkweb = getAllDarknetServers().filter((s) => s.depth === 0);
if (serversConnectedToDarkweb.length <= 3) {
addRandomDarknetServers(2, 0, true);
}
if (lowLevelServers.length / (4 * NET_WIDTH) < LOW_LEVEL_SERVER_DENSITY) {
addRandomDarknetServers(2, Math.floor(Math.random() * 4));
addLowLevelServersIfNeeded();
}
};
export const balanceDarknetServers = (): void => {
const movableServers = getAllMovableDarknetServers();
const netDepth = getNetDepth();
if (movableServers.length > netDepth * NET_WIDTH * SERVER_DENSITY) {
const serversToRemove = movableServers.length - netDepth * NET_WIDTH * SERVER_DENSITY;
deleteRandomDarknetServers(serversToRemove);
} else {
const serversToAdd = netDepth * NET_WIDTH * SERVER_DENSITY - movableServers.length;
addRandomDarknetServers(serversToAdd);
}
addLowLevelServersIfNeeded();
};
const isImmutable = (server: DarknetServer): boolean =>
server === DarknetState.openServer || server.isConnectedTo || server.hasStasisLink;
export const moveDarknetServer = (
server: DarknetServer,
maxDepthDecrease = 3,
maxDepthIncrease = 3,
startingDepth = server.difficulty,
): boolean => {
if (server.hostname === SpecialServers.DarkWeb) {
exceptionAlert(new Error("Something is trying to move darkweb"), true);
return false;
}
if (isImmutable(server)) {
// Do not try to move the server that is open in the UI or the terminal
return false;
}
const positionOptions = getAllOpenPositions(startingDepth - maxDepthDecrease, startingDepth + maxDepthIncrease);
if (positionOptions.length === 0) {
// If server cannot be moved, do not leave it disconnected and floating
deleteDarknetServer(server);
return false;
}
const [newX, newY] = positionOptions[Math.floor(Math.random() * positionOptions.length)];
disconnectServer(server, true);
if (DarknetState.Network[server.depth]?.[server.leftOffset]) {
DarknetState.Network[server.depth][server.leftOffset] = null;
}
addServerToNetwork(server, newX, newY);
return true;
};
const disconnectRandomServer = (): void => {
const servers = getAllMovableDarknetServers();
if (servers.length === 0) {
return;
}
const server = servers[Math.floor(Math.random() * servers.length)];
disconnectServer(server);
};
/**
* By default, this function disconnects the specified server from its neighbors, unless the neighbor is darkweb. Use
* the second parameter to ignore the exception. We added this exception to improve the stability of the servers
* directly connected to darkweb.
*/
export const disconnectServer = (server: DarknetServer, disconnectFromDarkweb = false): void => {
if (server.hostname === SpecialServers.DarkWeb) {
exceptionAlert(new Error("Something is trying to disconnect darkweb"), true);
return;
}
if (isImmutable(server)) {
return;
}
for (const neighbor of server.serversOnNetwork) {
const connectedServer = GetServer(neighbor);
const isOkToDisconnect = disconnectFromDarkweb || connectedServer?.hostname !== SpecialServers.DarkWeb;
if (connectedServer && isOkToDisconnect) {
disconnectServers(server, connectedServer);
}
}
};
const restartRandomServer = (): void => {
const servers = getAllMovableDarknetServers();
if (servers.length === 0) {
return;
}
restartServer(servers[Math.floor(Math.random() * servers.length)]);
};
export const restartServer = (server: DarknetServer): void => {
if (isImmutable(server)) {
return;
}
killServerScripts(server, "Server restarted.");
const serverState = getServerState(server.hostname);
serverState.authenticatedPIDs = [];
serverState.serverLogs = [{ pid: -1, message: "Server restarted." }];
server.backdoorInstalled = false;
disconnectServer(server);
addGuaranteedConnection(server);
};
const addConnectionsToRandomServer = (): void => {
const servers = getAllMovableDarknetServers();
if (servers.length === 0) {
return;
}
const server = servers[Math.floor(Math.random() * servers.length)];
addGuaranteedConnection(server);
};
export const addGuaranteedConnection = (server: DarknetServer): void => {
if (isLabyrinthServer(server.hostname)) {
return;
}
const neighbors = getAllAdjacentNeighbors(server.depth, server.leftOffset);
if (neighbors.length === 0) {
return;
}
const neighbor = neighbors[Math.floor(Math.random() * neighbors.length)];
connectServers(server, neighbor);
};
export const validateDarknetNetwork = (): void => {
const servers = getAllDarknetServers();
// The darknet should have at least darkweb and labyrinth servers.
if (servers.length < getLabyrinthServerNames().length + 1) {
exceptionAlert(new Error(`There are too few darknet servers. servers.length: ${servers.length}`), true);
}
for (const server of servers) {
if (server.depth !== -1 && DarknetState.Network[server.depth]?.[server.leftOffset]?.hostname !== server.hostname) {
exceptionAlert(
new Error(
`${server.hostname} does not exist in DarknetState.Network at [${server.depth}][${server.leftOffset}]`,
),
true,
);
}
for (const neighborHostname of server.serversOnNetwork) {
const neighbor = GetServer(neighborHostname);
if (!neighbor) {
exceptionAlert(
new Error(
`Found invalid neighbor dnet server. hostname: ${server.hostname}. neighbor: ${neighborHostname}. ` +
`serversOnNetwork: ${server.serversOnNetwork}`,
),
true,
);
continue;
}
if (!neighbor.serversOnNetwork.includes(server.hostname)) {
exceptionAlert(
new Error(
`The connection between ${server.hostname} and ${neighbor.hostname} is unidirectional. ` +
`server.serversOnNetwork: ${server.serversOnNetwork}. neighbor.serversOnNetwork: ${neighbor.serversOnNetwork}`,
),
true,
);
}
}
if (server.depth === 0 && !server.serversOnNetwork.includes(SpecialServers.DarkWeb)) {
exceptionAlert(
new Error(
`${server.hostname} at depth 0 does not have a connection to ${SpecialServers.DarkWeb}. ` +
`server.serversOnNetwork: ${server.serversOnNetwork}`,
),
true,
);
}
}
for (let i = 0; i < MAX_NET_DEPTH; i++) {
for (let j = 0; j < NET_WIDTH; j++) {
const serverInNetwork = DarknetState.Network[i]?.[j];
if (!serverInNetwork) {
continue;
}
const server = GetServer(serverInNetwork.hostname);
if (server == null) {
exceptionAlert(new Error(`${serverInNetwork.hostname} at [${i}][${j}] does not exist in AllServers`), true);
}
if (serverInNetwork !== server) {
exceptionAlert(new Error(`Invalid darknet server instance detected at [${i}][${j}]`), true);
}
}
}
};

View File

@@ -0,0 +1,706 @@
import { DnetServerBuilder } from "../models/DarknetServerOptions";
import {
commonPasswordDictionary,
defaultSettingsDictionary,
dogNameDictionary,
EUCountries,
filler,
letters,
lettersUppercase,
numbers,
} from "../models/dictionaryData";
import { DarknetServer } from "../../Server/DarknetServer";
import { ModelIds, MinigamesType } from "../Enums";
import { MAX_PASSWORD_LENGTH } from "../Constants";
import { clampNumber } from "../../utils/helpers/clampNumber";
import { hasFullDarknetAccess } from "../effects/effects";
const getRandomServerConfigBuilder = (difficulty: number) => {
const tier0Servers = [getNoPasswordConfig];
const tier1Servers = [getEchoVulnConfig, getDefaultPasswordConfig, getCaptchaConfig];
const tier2Servers = [getDogNameConfig, getYesn_tConfig, getBufferOverflowConfig];
const sf15UnlockedServers = hasFullDarknetAccess() ? [getKingOfTheHillConfig, getSpiceLevelConfig] : [];
const tier3Servers = [
getSortedEchoVulnConfig,
getMastermindHintConfig,
getRomanNumeralConfig,
getGuessNumberConfig,
getConvertToBase10Config,
getDivisibilityTestConfig,
getPacketSnifferConfig,
...sf15UnlockedServers,
];
const tier4Servers = [
getLargestPrimeFactorConfig,
getLargeDictionaryConfig,
getEuCountryDictionaryConfig,
getTimingAttackConfig,
getBinaryEncodedConfig,
getParseArithmeticExpressionConfig,
getXorMaskEncryptedPasswordConfig,
getTripleModuloConfig,
];
if (difficulty <= 2) {
const serverBuilders = [...tier0Servers, ...tier1Servers];
return serverBuilders[Math.floor(Math.random() * serverBuilders.length)];
}
if (difficulty <= 4) {
const serverBuilders = [...tier0Servers, ...tier1Servers, ...tier1Servers, ...tier2Servers, ...tier3Servers];
return serverBuilders[Math.floor(Math.random() * serverBuilders.length)];
}
if (difficulty <= 8) {
const serverBuilders = [...tier1Servers, ...tier2Servers, ...tier3Servers];
return serverBuilders[Math.floor(Math.random() * serverBuilders.length)];
}
if (difficulty <= 18) {
const serverBuilders = [...tier2Servers, ...tier3Servers, ...tier4Servers];
return serverBuilders[Math.floor(Math.random() * serverBuilders.length)];
}
const serverBuilders = [...tier3Servers, ...tier4Servers];
return serverBuilders[Math.floor(Math.random() * serverBuilders.length)];
};
export const createDarknetServer = (difficulty: number, depth: number, leftOffset: number): DarknetServer => {
const cappedDifficulty = clampNumber(difficulty, 0, MAX_PASSWORD_LENGTH);
return serverFactory(getRandomServerConfigBuilder(cappedDifficulty), difficulty, depth, leftOffset);
};
export type ServerConfig = {
modelId: MinigamesType;
password: string;
staticPasswordHint: string;
passwordHintData?: string;
};
export const serverFactory = (
serverConfigBuilder: (n: number) => ServerConfig,
difficulty: number,
depth: number,
leftOffset: number,
): DarknetServer => {
return DnetServerBuilder({
...serverConfigBuilder(difficulty),
difficulty,
depth,
leftOffset: leftOffset,
});
};
export const getEchoVulnConfig = (__difficulty: number): ServerConfig => {
const hintTemplates = [
"The password is",
"The PIN is",
"Remember to use",
"It's set to",
"The key is",
"The secret is",
];
const password = getPassword(3);
const hint = `${hintTemplates[Math.floor(Math.random() * hintTemplates.length)]} ${password}`;
return {
modelId: ModelIds.EchoVuln,
password,
staticPasswordHint: hint,
};
};
export const getSortedEchoVulnConfig = (difficulty: number): ServerConfig => {
const hintTemplates = [
"The password is shuffled",
"The key is made from",
"I accidentally sorted the password:",
"The PIN uses",
];
const password = getPassword(Math.min(2 + difficulty / 7, 9));
const sortedPassword = password.split("").sort().join("");
const hint = `${hintTemplates[Math.floor(Math.random() * hintTemplates.length)]} ${sortedPassword}`;
return {
modelId: ModelIds.SortedEchoVuln,
password,
staticPasswordHint: hint,
passwordHintData: sortedPassword,
};
};
export const getDictionaryAttackConfig = (
__difficulty: number,
dictionary: readonly string[],
hintTemplates: string[],
minigameType: MinigamesType,
): ServerConfig => {
return {
modelId: minigameType,
password: dictionary[Math.floor(Math.random() * dictionary.length)],
staticPasswordHint: hintTemplates[Math.floor(Math.random() * hintTemplates.length)],
};
};
export const getNoPasswordConfig = (difficulty: number): ServerConfig => {
const hintTemplates = [
"The password is not set",
"There is no password",
"The PIN is empty",
"Did I set a code?",
"I didn't set a password",
];
return getDictionaryAttackConfig(difficulty, [""], hintTemplates, ModelIds.NoPassword);
};
export const getDefaultPasswordConfig = (difficulty: number): ServerConfig => {
const hintTemplates = [
"The password is the default password",
"It's still the default",
"The default password is set",
"I never changed the password",
"It's still the factory settings",
];
return getDictionaryAttackConfig(difficulty, defaultSettingsDictionary, hintTemplates, ModelIds.DefaultPassword);
};
export const getCaptchaConfig = (difficulty: number): ServerConfig => {
const password = getPassword(difficulty / 2 + 3);
const filledPassword = password
.split("")
.map((char, i) => {
if (i >= password.length - 1) {
return char;
}
return char + getFillerChars();
})
.join("");
return {
modelId: ModelIds.Captcha,
password,
staticPasswordHint: "Type the numbers to prove you are human",
passwordHintData: filledPassword,
};
};
const getFillerChars = () => {
let result = "";
const num = Math.ceil(Math.random() * 3);
for (let i = 0; i < num; i++) {
result += filler[Math.floor(Math.random() * filler.length)];
}
return result;
};
export const getDogNameConfig = (difficulty: number): ServerConfig => {
const hintTemplates = ["It's my dog's name", "It's the dog's name", "my first dog's name"];
return getDictionaryAttackConfig(difficulty, dogNameDictionary, hintTemplates, ModelIds.DogNames);
};
export const getMastermindHintConfig = (difficulty: number): ServerConfig => {
const alphanumeric = difficulty > 16 && Math.random() < 0.3;
const passwordLength = Math.min((alphanumeric ? -1 : 2) + difficulty / 5, 10);
return {
modelId: ModelIds.MastermindHint,
password: getPassword(passwordLength, alphanumeric),
staticPasswordHint: "Only a true master may pass",
};
};
export const getTimingAttackConfig = (difficulty: number): ServerConfig => {
const hintTemplates = [
"I thought about it for some time, but that is not the password.",
"I spent a while on it, but that's not right",
"I considered it for a bit, but that's not it",
"I spent some time on it, but that's not the password",
];
const alphanumeric = difficulty > 16 && Math.random() < 0.3;
const length = (alphanumeric ? 0 : 3) + difficulty / 4;
return {
modelId: ModelIds.TimingAttack,
password: getPassword(length, alphanumeric),
staticPasswordHint: hintTemplates[Math.floor(Math.random() * hintTemplates.length)],
};
};
export const getRomanNumeralConfig = (difficulty: number): ServerConfig => {
const password = Math.floor(Math.random() * 10 * (10 * (difficulty + 1)));
if (difficulty < 8) {
const encodedPassword = romanNumeralEncoder(password);
return {
modelId: ModelIds.RomanNumeral,
password: `${password}`,
staticPasswordHint: `The password is the value of the number '${encodedPassword}'`,
passwordHintData: encodedPassword,
};
} else {
const passwordRangeMin = Math.random() < 0.3 ? 0 : Math.floor(password * (Math.random() * 0.2 + 0.6));
const passwordRangeMax = password + Math.floor(Math.random() * difficulty * 10 + 10);
const encodedMin = romanNumeralEncoder(passwordRangeMin);
const encodedMax = romanNumeralEncoder(passwordRangeMax);
const hint = `The password is between '${encodedMin}' and '${encodedMax}'`;
const hintData = `${encodedMin},${encodedMax}`;
return {
modelId: ModelIds.RomanNumeral,
password: `${password}`,
staticPasswordHint: hint,
passwordHintData: hintData,
};
}
};
export const getLargestPrimeFactorConfig = (difficulty: number): ServerConfig => {
const largestPrimePasswordDetails = getLargestPrimeFactorPassword(difficulty);
return {
modelId: ModelIds.LargestPrimeFactor,
password: `${largestPrimePasswordDetails.largestPrime}`,
staticPasswordHint: `The password is the largest prime factor of ${largestPrimePasswordDetails.targetNumber}`,
passwordHintData: `${largestPrimePasswordDetails.targetNumber}`,
};
};
export const getGuessNumberConfig = (difficulty: number): ServerConfig => {
const password = `${Math.floor((Math.random() * 10 * (difficulty + 3)) / 3)}`;
const maxNumber = 10 ** password.length;
return {
modelId: ModelIds.GuessNumber,
password,
staticPasswordHint: `The password is a number between 0 and ${maxNumber}`,
};
};
export const getLargeDictionaryConfig = (difficulty: number): ServerConfig => {
return getDictionaryAttackConfig(
difficulty,
commonPasswordDictionary,
["It's a common password"],
ModelIds.CommonPasswordDictionary,
);
};
export const getEuCountryDictionaryConfig = (difficulty: number): ServerConfig => {
return getDictionaryAttackConfig(difficulty, EUCountries, ["My favorite EU country"], ModelIds.EUCountryDictionary);
};
export const getYesn_tConfig = (difficulty: number): ServerConfig => {
const password = getPassword(3 + difficulty / 2, difficulty > 8);
return {
modelId: ModelIds.Yesn_t,
password,
staticPasswordHint: "you are one who's'nt authorized",
};
};
export const getBufferOverflowConfig = (): ServerConfig => {
const length = Math.floor(4 + Math.random() * 4);
const password = getPassword(length, true);
return {
modelId: ModelIds.BufferOverflow,
password,
staticPasswordHint: `Warning: password buffer is ${length} bytes`,
};
};
export const getBinaryEncodedConfig = (difficulty: number): ServerConfig => {
const password = getPassword(2 + difficulty / 5, difficulty > 8);
const binaryEncodedPassword = password
.split("")
.map((char) => char.charCodeAt(0).toString(2).padStart(8, "0"))
.join(" ");
return {
modelId: ModelIds.BinaryEncodedFeedback,
password,
staticPasswordHint: "beep boop",
passwordHintData: binaryEncodedPassword,
};
};
export const getXorMaskEncryptedPasswordConfig = (): ServerConfig => {
const password = getPassword(3 + Math.random() * 3, true);
let passwordWithXorMaskApplied: string;
let xorMaskStrings: string[];
do {
passwordWithXorMaskApplied = "";
xorMaskStrings = [];
for (const c of password) {
const charCode = c.charCodeAt(0);
const xorMask = Math.floor(Math.random() * 32);
xorMaskStrings.push(xorMask.toString(2).padStart(8, "0"));
passwordWithXorMaskApplied += String.fromCharCode(charCode ^ xorMask);
}
// Prevent characters that would break parsing in encoded output
} while (passwordWithXorMaskApplied.includes(";") || passwordWithXorMaskApplied.includes(" "));
return {
modelId: ModelIds.encryptedPassword,
password,
staticPasswordHint: `XOR mask encrypted password: "${passwordWithXorMaskApplied}".`,
passwordHintData: `${passwordWithXorMaskApplied};${xorMaskStrings.join(" ")}`,
};
};
export const getSpiceLevelConfig = (difficulty: number): ServerConfig => {
const password = getPassword(3 + difficulty / 3, difficulty > 8);
return {
modelId: ModelIds.SpiceLevel,
password,
staticPasswordHint: "!!🌶️!!",
};
};
export const getConvertToBase10Config = (difficulty: number): ServerConfig => {
const password = Math.ceil(Math.random() * 99 * (difficulty + 1));
const bases = [2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16];
let base = bases[Math.floor(Math.random() * bases.length)];
if (difficulty > 12) {
base += bases[Math.floor(Math.random() * bases.length)] / 10;
}
const encodedPassword = encodeNumberInBaseN(password, base);
return {
modelId: ModelIds.ConvertToBase10,
password: `${password}`,
staticPasswordHint: `the password is the base ${base} number ${encodedPassword} in base 10`,
passwordHintData: `${base},${encodedPassword}`,
};
};
export const getParseArithmeticExpressionConfig = (difficulty: number): ServerConfig => {
let expression = generateSimpleArithmeticExpression(difficulty);
const result = parseSimpleArithmeticExpression(expression);
if (difficulty > 12) {
expression = expression.replaceAll("*", "ҳ").replaceAll("/", "÷").replaceAll("+", "").replaceAll("-", "");
}
if ((difficulty > 16 && Math.random() < 0.3) || Math.random() < 0.01) {
expression += getCodeInjection();
}
const parenCount = expression.split("(").length - 1;
if (difficulty > 20 && Math.random() < 0.3 && parenCount > 1) {
expression = expression.replace("(", "(ns.exit(),");
}
return {
modelId: ModelIds.parsedExpression,
password: `${result}`,
staticPasswordHint: `The password is the evaluation of this expression`,
passwordHintData: expression,
};
};
export const getDivisibilityTestConfig = (difficulty: number): ServerConfig => {
const password = getPasswordMadeUpOfPrimesProduct(difficulty);
return {
modelId: ModelIds.divisibilityTest,
password: `${password}`,
staticPasswordHint: `The password is divisible by 1 ;)`,
};
};
export const getTripleModuloConfig = (difficulty: number): ServerConfig => {
const password = getPassword(3 + difficulty / 5);
return {
modelId: ModelIds.tripleModulo,
password: `${password}`,
staticPasswordHint: `(password % n) % (n % 32)`,
};
};
export const getKingOfTheHillConfig = (difficulty: number): ServerConfig => {
const password = getPassword(Math.min(1 + difficulty / 6, 10));
return {
modelId: ModelIds.globalMaxima,
password,
staticPasswordHint: "Ascend the highest mountain!",
};
};
export const getPacketSnifferConfig = (difficulty: number): ServerConfig => {
return {
modelId: ModelIds.packetSniffer,
password: getPassword(3 + difficulty / 3, difficulty > 8),
staticPasswordHint: "(I'm busy browsing social media at the cafe)",
};
};
export const encodeNumberInBaseN = (decimalNumber: number, base: number) => {
const characters = [...numbers.split(""), ...lettersUppercase.split("")];
let digits = Math.floor(Math.log(decimalNumber) / Math.log(base));
let remaining = decimalNumber;
let result: string = "";
while (remaining >= 0.0001 || digits >= 0) {
if (digits === -1) {
result += ".";
}
const place = Math.floor(remaining / base ** digits);
result += characters[place];
remaining -= place * base ** digits;
digits -= 1;
}
return result;
};
export const parseBaseNNumberString = (numberString: string, base: number): number => {
const characters = [...numbers.split(""), ...lettersUppercase.split("")];
let result = 0;
let index = 0;
let digit = numberString.split(".")[0].length - 1;
while (index < numberString.length) {
const currentDigit = numberString[index];
if (currentDigit === ".") {
index += 1;
continue;
}
result += characters.indexOf(currentDigit) * base ** digit;
index += 1;
digit -= 1;
}
return result;
};
// example: 4 + 5 * ( 6 + 7 ) / 2
export const parseSimpleArithmeticExpression = (expression: string): number => {
const tokens = cleanArithmeticExpression(expression).split("");
// Identify parentheses
let currentDepth = 0;
const depth = tokens.map((token) => {
if (token === "(") {
currentDepth += 1;
} else if (token === ")") {
currentDepth -= 1;
return currentDepth + 1;
}
return currentDepth;
});
const depth1Start = depth.indexOf(1);
// find the last 1 before the first 0 after depth1Start
const firstZeroAfterDepth1Start = depth.indexOf(0, depth1Start);
const depth1End = firstZeroAfterDepth1Start === -1 ? depth.length - 1 : firstZeroAfterDepth1Start - 1;
if (depth1Start !== -1) {
const subExpression = tokens.slice(depth1Start + 1, depth1End).join("");
const result = parseSimpleArithmeticExpression(subExpression);
tokens.splice(depth1Start, depth1End - depth1Start + 1, result.toString());
return parseSimpleArithmeticExpression(tokens.join(""));
}
// handle multiplication and division
let remainingExpression = tokens.join("");
// breakdown and explanation for this regex: https://regex101.com/r/mZhiBn/1
const multiplicationDivisionRegex = /(-?\d*\.?\d+) *([*/]) *(-?\d*\.?\d+)/;
let match = remainingExpression.match(multiplicationDivisionRegex);
while (match) {
const [__, left, operator, right] = match;
const result = operator === "*" ? parseFloat(left) * parseFloat(right) : parseFloat(left) / parseFloat(right);
const resultString = Math.abs(result) < 0.000001 ? result.toFixed(20) : result.toString();
remainingExpression = remainingExpression.replace(match[0], resultString);
match = remainingExpression.match(multiplicationDivisionRegex);
}
// handle addition and subtraction
const additionSubtractionRegex = /(-?\d*\.?\d+) *([+-]) *(-?\d*\.?\d+)/;
match = remainingExpression.match(additionSubtractionRegex);
while (match) {
const [__, left, operator, right] = match;
const result = operator === "+" ? parseFloat(left) + parseFloat(right) : parseFloat(left) - parseFloat(right);
remainingExpression = remainingExpression.replace(match[0], result.toString());
match = remainingExpression.match(additionSubtractionRegex);
}
const [__, leftover] = remainingExpression.match(/(-?\d*\.?\d+)/) ?? ["", ""];
return parseFloat(leftover);
};
export const generateSimpleArithmeticExpression = (difficulty: number): string => {
const operators = ["+", "-", "*", "/"];
const operatorCount = Math.floor(difficulty / 4);
const expression = [];
for (let i = 0; i < operatorCount; i++) {
expression.push(Math.ceil(Math.random() * 98));
expression.push(operators[Math.floor(Math.random() * operators.length)]);
if (difficulty > 5 && Math.random() < difficulty / (difficulty + 50)) {
expression.push("(");
expression.push(generateSimpleArithmeticExpression(difficulty / 2));
expression.push(")");
expression.push(operators[Math.floor(Math.random() * operators.length)]);
}
}
expression.push(Math.ceil(Math.random() * 98));
const result = expression.join(" ");
try {
const calc = parseSimpleArithmeticExpression(cleanArithmeticExpression(result));
if (Math.abs(calc) < 0.1) {
return generateSimpleArithmeticExpression(difficulty);
}
} catch (__) {
return generateSimpleArithmeticExpression(difficulty);
}
if (difficulty > 18) {
return result.replaceAll("*", "ҳ").replaceAll("/", "÷").replaceAll("+", "").replaceAll("-", "");
}
return result;
};
export const cleanArithmeticExpression = (expression: string): string => {
const expressionWithFixedSymbols = expression
.replaceAll("ҳ", "*")
.replaceAll("÷", "/")
.replaceAll("", "+")
.replaceAll("", "-")
.replaceAll("ns.exit(),", "");
return expressionWithFixedSymbols.split(",")[0];
};
const getCodeInjection = () => {
return ` , !globalThis.pwn3d && (globalThis.pwn3d=true, alert("You've been hacked! You evaluated a string and let me inject code, didn't you? HAHAHAHA!") , globalThis.openDevMenu() ) , ns.exit()`;
};
export const getPassword = (length: number, allowLetters = false): string => {
const characters = numbers + (allowLetters ? letters : "");
let password = "";
const cappedLength = clampNumber(length, 1, MAX_PASSWORD_LENGTH);
for (let i = 0; i < cappedLength; i++) {
password += characters[Math.floor(Math.random() * characters.length)];
}
if (!allowLetters && Number(password) > Number.MAX_SAFE_INTEGER) {
password = password.slice(0, 15);
}
// prevent leading zeros in multi-digit numeric passwords
if (!allowLetters) {
return Number(password).toString();
}
return password;
};
export const getPasswordType = (password: string): "numeric" | "alphabetic" | "alphanumeric" | "ASCII" | "unicode" => {
const passwordArr = password.split("");
if (passwordArr.every((char) => numbers.includes(char))) {
return "numeric";
}
if (passwordArr.every((char) => letters.includes(char))) {
return "alphabetic";
}
if (passwordArr.every((char) => numbers.includes(char) || letters.includes(char))) {
return "alphanumeric";
}
if (passwordArr.every((char) => char.charCodeAt(0) < 128)) {
return "ASCII";
}
return "unicode";
};
export const romanNumeralEncoder = (input: number): string => {
const romanNumerals: { [key: number]: string } = {
1: "I",
4: "IV",
5: "V",
9: "IX",
10: "X",
40: "XL",
50: "L",
90: "XC",
100: "C",
400: "CD",
500: "D",
900: "CM",
1000: "M",
};
const keys = Object.keys(romanNumerals).map((key) => Number(key));
let result = "";
for (let i = keys.length - 1; i >= 0; i--) {
const key = keys[i];
while (input >= key) {
result += romanNumerals[key];
input -= key;
}
}
return result || "nulla";
};
export const romanNumeralDecoder = (input: string): number => {
if (input.toLowerCase() === "nulla") {
return 0;
}
const romanToInt: { [key: string]: number } = {
I: 1,
V: 5,
X: 10,
L: 50,
C: 100,
D: 500,
M: 1000,
};
let total = 0;
let prevValue = 0;
for (let i = input.length - 1; i >= 0; i--) {
const currentValue = romanToInt[input[i]];
if (currentValue < prevValue) {
total -= currentValue;
} else {
total += currentValue;
}
prevValue = currentValue;
}
return total;
};
export const smallPrimes = [
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97,
];
export const largePrimes = [
1069, 1409, 1471, 1567, 1597, 1601, 1697, 1747, 1801, 1889, 1979, 1999, 2063, 2207, 2371, 2503, 2539, 2693, 2741,
2753, 2801, 2819, 2837, 2909, 2939, 3169, 3389, 3571, 3761, 3881, 4217, 4289, 4547, 4729, 4789, 4877, 4943, 4951,
4957, 5393, 5417, 5419, 5441, 5519, 5527, 5647, 5779, 5881, 6007, 6089, 6133, 6389, 6451, 6469, 6547, 6661, 6719,
6841, 7103, 7549, 7559, 7573, 7691, 7753, 7867, 8053, 8081, 8221, 8329, 8599, 8677, 8761, 8839, 8963, 9103, 9199,
9343, 9467, 9551, 9601, 9739, 9749, 9859,
];
const getLargestPrimeFactorPassword = (difficulty = 1) => {
const factorCount = 1 + Math.min(5, Math.floor(difficulty / 3));
const largePrimeIndex = 2 + Math.floor(Math.random() * (largePrimes.length - 2));
const largestPrime = largePrimes[largePrimeIndex];
let number = largestPrime;
for (let i = 1; i <= factorCount; i++) {
number *= smallPrimes[Math.floor(Math.random() * smallPrimes.length)];
}
return {
largestPrime: largestPrime,
targetNumber: number,
};
};
const getPasswordMadeUpOfPrimesProduct = (difficulty = 1) => {
const scale = Math.min(difficulty / 2, 15);
let password;
do {
password = BigInt(Math.floor(Math.random() * 5 * (scale + 1)) + 1);
for (let i = 0; i < scale / 3; i++) {
if (Math.random() < 0.5) {
password *= BigInt(Math.ceil(Math.random() * 5));
} else {
password *= BigInt(smallPrimes[Math.floor(Math.random() * smallPrimes.length)]);
}
}
if (difficulty > 12) {
password *= BigInt(largePrimes[Math.floor(Math.random() * largePrimes.length)]);
}
if (difficulty > 24) {
password *= BigInt(largePrimes[Math.floor(Math.random() * largePrimes.length)]);
}
} while (BigInt(Number(password)) !== password); // ensure it fits in JS number precision
return password.toString();
};

View File

@@ -0,0 +1,30 @@
import { DarknetState } from "../models/DarknetState";
import { assertObject } from "../../utils/TypeAssertion";
export type DarknetSaveFormat = {
storedCycles: number;
};
export function getDarkNetSave(): DarknetSaveFormat {
return {
storedCycles: Math.floor(DarknetState.storedCycles),
};
}
export function loadDarkNet(saveString: unknown): void {
if (saveString == null || typeof saveString !== "string" || saveString === "") {
return;
}
try {
const parsedData: unknown = JSON.parse(saveString);
assertObject(parsedData);
const { storedCycles } = parsedData;
if (typeof storedCycles !== "number" || !Number.isFinite(storedCycles)) {
throw new Error(`Invalid storedCycles: ${storedCycles}`);
}
DarknetState.storedCycles = storedCycles < 0 ? 0 : storedCycles;
} catch (error) {
console.error(error);
console.error("Invalid DarkNet data:", saveString);
}
}

View File

@@ -0,0 +1,223 @@
import { handleLabyrinthPassword, isLabyrinthServer } from "./labyrinth";
import { handleFailedAuth, handleSuccessfulAuth } from "./effects";
import type { DarknetResult } from "@nsdefs";
import { PasswordResponse } from "../models/DarknetServerOptions";
import { logPasswordAttempt } from "../models/packetSniffing";
import { getServerState } from "../models/DarknetState";
import { GenericResponseMessage, ModelIds, ResponseCodeEnum } from "../Enums";
import {
getExactCorrectChars,
getExactCorrectCharsCount,
getFailureResponse,
getGenericSuccess,
getMisplacedCorrectCharsCount,
} from "../utils/darknetAuthUtils";
import type { DarknetServer } from "../../Server/DarknetServer";
import { SpecialServers } from "../../Server/data/SpecialServers";
import { WHRNG } from "../../Casino/RNG";
export const checkPassword = (
server: DarknetServer,
attemptedPassword: string,
pid: number,
responseTime = 0,
): PasswordResponse => {
if (isLabyrinthServer(server.hostname)) {
return handleLabyrinthPassword(attemptedPassword, server, pid);
}
if (server.password === attemptedPassword) {
return getGenericSuccess(attemptedPassword);
}
switch (server.modelId) {
case ModelIds.MastermindHint: {
const { exactCharacters, misplacedCharacters } = getMastermindResponse(server.password, attemptedPassword);
const exactCharsMessage = `${exactCharacters} symbol${exactCharacters == 1 ? " is" : "s are"} match exactly`;
const misplacedCharsMessage = `${misplacedCharacters} symbol${misplacedCharacters == 1 ? "" : "s"} match but ${
misplacedCharacters == 1 ? "is" : "are"
} in the wrong place`;
const message = `Hint: ${exactCharsMessage}, and ${misplacedCharsMessage}.`;
return getFailureResponse(attemptedPassword, message, `${exactCharacters},${misplacedCharacters}`);
}
case ModelIds.GuessNumber: {
const hintData = Number(attemptedPassword) > Number(server.password) ? "Lower" : "Higher";
return getFailureResponse(attemptedPassword, server.staticPasswordHint, hintData);
}
case ModelIds.RomanNumeral: {
const hintData = Number(attemptedPassword) > Number(server.password) ? "ALTUS NIMIS" : "PARUM BREVIS";
return getFailureResponse(attemptedPassword, server.staticPasswordHint, hintData);
}
case ModelIds.Yesn_t: {
const response = attemptedPassword
.split("")
.map((char, i) => (char === server.password[i] ? "yes" : "yesn't"))
.join(",");
return getFailureResponse(attemptedPassword, "that wasn't right", response);
}
case ModelIds.SpiceLevel: {
const exactChars = getExactCorrectChars(server.password, attemptedPassword);
const pepperRepresentation = exactChars.map((val) => (val ? "🌶️" : "")).join("") || "0";
return getFailureResponse(
attemptedPassword,
"Not spicy enough",
`${pepperRepresentation}/${server.password.length}`,
);
}
case ModelIds.divisibilityTest: {
const password = Number(server.password);
const attemptedDivisor = Number(attemptedPassword);
if (isNaN(+attemptedPassword) || password % attemptedDivisor || attemptedPassword === "") {
return getFailureResponse(attemptedPassword, `Password is not divisible by '${attemptedPassword}'`, "false");
}
return getFailureResponse(attemptedPassword, `Password IS divisible by '${attemptedPassword}'`, "true");
}
case ModelIds.tripleModulo: {
const password = Number(server.password);
const input = Number(attemptedPassword);
const result = (password % input) % (((input - 1) % 32) + 1);
const message =
input % 32 === 0
? `(Password % ${input}) % 32 = ${result}`
: `(Password % ${input}) % (${input} % 32) = ${result}`;
return getFailureResponse(attemptedPassword, message, result.toString());
}
case ModelIds.ConvertToBase10:
case ModelIds.parsedExpression: {
const parsedAttemptedPassword = parseFloat(attemptedPassword);
if (!isNaN(parsedAttemptedPassword) && isCloseToCorrectPassword(server.password, parsedAttemptedPassword)) {
// ignore small rounding errors during floating point operations
return getGenericSuccess(attemptedPassword);
}
return getFailureResponse(attemptedPassword, server.staticPasswordHint, server.passwordHintData);
}
case ModelIds.TimingAttack: {
const indexOfDifference = server.password.split("").findIndex((char, i) => char !== attemptedPassword[i]);
const hint = `Found a mismatch while checking each character (${indexOfDifference})`;
const data = `Response time: ${responseTime}ms`;
return getFailureResponse(attemptedPassword, hint, data);
}
case ModelIds.BufferOverflow: {
// If the attempted password is longer than the server's actual password, it starts overwriting the
// part of the "buffer" that holds the expected password, which can "trick" the comparison into matching
const maskCharacter = attemptedPassword === "■".repeat(server.password.length) ? "?" : "■";
const buffer = "ˍ".repeat(server.password.length) + maskCharacter.repeat(server.password.length);
const overwrittenBuffer = attemptedPassword.slice(0, buffer.length) + buffer.slice(attemptedPassword.length);
const receivedBuffer = overwrittenBuffer.slice(0, server.password.length);
const expectedValueBuffer = overwrittenBuffer.slice(server.password.length);
if (receivedBuffer === expectedValueBuffer) {
return getGenericSuccess(attemptedPassword);
}
const failureMessage = `auth failed: received '${receivedBuffer}', expected '${expectedValueBuffer}'`;
const data = `${receivedBuffer},${expectedValueBuffer}`;
return getFailureResponse(attemptedPassword, failureMessage, data);
}
case ModelIds.globalMaxima: {
const altitude = getKingOfTheHillAltitude(server, attemptedPassword);
return getFailureResponse(
attemptedPassword,
`current altitude: ${altitude.toFixed(5)} m; highest peak: 10,000 m`,
`${altitude}`,
);
}
default:
return getFailureResponse(attemptedPassword, server.staticPasswordHint, server.passwordHintData);
}
};
export const isCloseToCorrectPassword = (
correctPassword: string,
attemptedPassword: number,
logIfNotClose = false,
): boolean => {
const difference = Math.abs(attemptedPassword - Number(correctPassword));
const result = difference < 0.01 || difference / Number(correctPassword) < 0.005;
if (logIfNotClose && !result) {
console.warn(`Attempted password ${attemptedPassword} is not close enough to correct password ${correctPassword}`);
}
return result;
};
export const getAuthResult = (
server: DarknetServer,
attemptedPassword: string,
threads = 1,
responseTime = 0,
pid = -1,
logActivity = true,
): { result: DarknetResult; response: PasswordResponse } => {
const response = checkPassword(server, attemptedPassword, pid, responseTime);
if (logActivity) {
logPasswordAttempt(server, response, pid);
}
if (response.code === ResponseCodeEnum.Success) {
handleSuccessfulAuth(server, threads, pid);
return {
result: {
success: true,
code: ResponseCodeEnum.Success,
message: GenericResponseMessage.Success,
},
response: response,
};
}
handleFailedAuth(server, threads);
return {
result: {
success: false,
code: ResponseCodeEnum.AuthFailure,
message: GenericResponseMessage.AuthFailure,
},
response: response,
};
};
export const isAuthenticated = (server: DarknetServer, pid: number): boolean => {
if (!server.hasAdminRights) {
return false;
}
const serverState = getServerState(server.hostname);
return serverState.authenticatedPIDs.includes(pid) || server.hostname === SpecialServers.DarkWeb;
};
export const getMastermindResponse = (password: string, attemptedPassword: string) => {
return {
exactCharacters: getExactCorrectCharsCount(password, attemptedPassword),
misplacedCharacters: getMisplacedCorrectCharsCount(password, attemptedPassword),
};
};
export const getKingOfTheHillAltitude = (server: DarknetServer, attemptedPassword: string) => {
const password = Number(server.password);
const x = Number(attemptedPassword);
const rng = new WHRNG(password);
const hillCount = Math.min(Math.floor(server.difficulty / 8), 4) * 2 + 1;
const passwordHillIndex = Math.floor(rng.random() * (hillCount - 2)) + 1;
const width = 10 ** Math.max(server.password.length - 2, 0) + 1;
// As the player gets very close to the password, only consider the main hill
// This is to ensure the data is very clean as they approach, and that the peak is not
// moved off of the password value by some of the side hills
if (Math.abs((x - password) / password) < 0.03) {
return getAltitudeGivenHillSpecs(x, password, 10000, width);
}
// Otherwise, give them the full graph result with all hills
let altitude = 0;
for (let i = 0; i < hillCount; i++) {
const locationOffset = (i - passwordHillIndex) * width * 3 * (rng.random() * 0.2 + 0.9);
const heightOffset = Math.abs((i - passwordHillIndex) * 2600) * (rng.random() * 0.1 + 0.95);
altitude += getAltitudeGivenHillSpecs(x, password + locationOffset, 10000 - heightOffset, width);
}
return altitude;
};
const getAltitudeGivenHillSpecs = (x: number, location: number, height: number, width: number) => {
return height * Math.exp(((x - location) ** 2 / width ** 2) * -1);
};

View File

@@ -0,0 +1,141 @@
import { tryGeneratingRandomContract } from "../../CodingContract/ContractGenerator";
import { Player } from "@player";
import { formatMoney, formatNumber } from "../../ui/formatNumber";
import { getLabAugReward, isLabyrinthServer, LAB_CACHE_NAME } from "./labyrinth";
import { SnackbarEvents } from "../../ui/React/Snackbar";
import { AugmentationName, CompletedProgramName, ToastVariant } from "@enums";
import { currentNodeMults } from "../../BitNode/BitNodeMultipliers";
import { CreateProgramWork } from "../../Work/CreateProgramWork";
import { initStockMarket, isStockMarketInitialized } from "../../StockMarket/StockMarket";
import { cachePrefixes } from "../models/dictionaryData";
import type { DarknetServer } from "../../Server/DarknetServer";
import { type CacheFilePath, resolveCacheFilePath } from "../../Paths/CacheFilePath";
import type { CacheResult, Result } from "@nsdefs";
export const generateCacheFilename: (prefix?: string) => CacheFilePath | null = (prefix) => {
const filenamePrefix = prefix ?? cachePrefixes[Math.floor(Math.random() * cachePrefixes.length)];
return resolveCacheFilePath(`${filenamePrefix}_${Math.random().toString().substring(2, 5)}.cache`);
};
export const addCacheToServer: (server: DarknetServer, prefix?: string) => Result<{ cacheFilename: CacheFilePath }> = (
server,
prefix,
) => {
const cacheFilename = generateCacheFilename(prefix);
if (!cacheFilename) {
return { success: false, message: `Cannot generate path. prefix: ${prefix}` };
}
server.caches.push(cacheFilename);
return { success: true, cacheFilename };
};
export const getRewardFromCache = (server: DarknetServer, cacheName: string, suppressToast = false): CacheResult => {
const difficulty = server.difficulty;
const karmaLoss = difficulty + 1;
Player.karma -= karmaLoss;
if (isLabyrinthServer(server.hostname) && cacheName.includes(LAB_CACHE_NAME)) {
const labReward = getLabReward();
return {
success: true,
message: labReward,
karmaLoss: -karmaLoss,
};
}
const rewards = [getMoneyReward, getXpReward, getProgramAndStockMarketRelatedRewards, getCCTReward];
const reward = rewards[Math.floor(Math.random() * rewards.length)];
const result = reward(difficulty);
if (!suppressToast) {
SnackbarEvents.emit(
// Karma is only useful in relation to gangs, so we only show the karma loss if the player has started unlocking gang
// content. This is to avoid cluttering the UI with unnecessary info, and confusion before players discover karma.
result + (Player.isAwareOfGang() ? ` Gained -${karmaLoss} karma.` : ""),
ToastVariant.SUCCESS,
4000,
);
}
return {
success: true,
message: result,
karmaLoss: -karmaLoss,
};
};
export const getCCTReward = (): string => {
const contractCount = [2, 3, 4][Math.floor(Math.random() * 3)];
tryGeneratingRandomContract(contractCount);
return `New coding contracts are now available on the network!`;
};
export const getMoneyReward = (difficulty: number): string => {
const sf15_3Factor = Player.activeSourceFileLvl(15) > 3 ? 1.5 : 1;
const reward =
1.2 ** difficulty *
1e7 *
((200 + Player.skills.charisma) / 200) *
sf15_3Factor *
Player.mults.crime_money *
currentNodeMults.DarknetMoneyMultiplier; // TODO: adjust balance
Player.gainMoney(reward, "darknet");
return `You have discovered a cache with ${formatMoney(reward)}.`;
};
export const getXpReward = (difficulty: number): string => {
const sf15_3Factor = Player.activeSourceFileLvl(15) > 3 ? 1.5 : 1;
const reward = 1.2 ** difficulty * 500 * sf15_3Factor * Player.mults.charisma_exp; // TODO: adjust balance
Player.gainCharismaExp(reward);
return `You have discovered a cache with ${formatNumber(reward, 0)} cha XP.`;
};
export const getProgramAndStockMarketRelatedRewards = (difficulty: number): string => {
const creatingProgram = Player.currentWork instanceof CreateProgramWork ? Player.currentWork.programName : null;
const programs = [
CompletedProgramName.serverProfiler,
CompletedProgramName.bruteSsh,
CompletedProgramName.deepScan1,
CompletedProgramName.ftpCrack,
CompletedProgramName.autoLink,
CompletedProgramName.relaySmtp,
CompletedProgramName.deepScan2,
CompletedProgramName.httpWorm,
CompletedProgramName.sqlInject,
CompletedProgramName.formulas,
];
for (const program of programs) {
if (!Player.hasProgram(program) && creatingProgram !== program) {
Player.getHomeComputer().pushProgram(program);
return `You have discovered the program ${program}.`;
}
}
if (!Player.hasWseAccount) {
Player.hasWseAccount = true;
if (!isStockMarketInitialized()) {
initStockMarket();
}
return `You have discovered a stolen WSE Account!`;
}
if (!Player.hasTixApiAccess) {
Player.hasTixApiAccess = true;
if (!isStockMarketInitialized()) {
initStockMarket();
}
return `You have discovered a stolen TIX API access point!`;
}
if (!Player.has4SData && Player.bitNodeN !== 8 && !Player.bitNodeOptions.disable4SData) {
Player.has4SData = true;
return `You have discovered a cache of stolen 4S Data!`;
}
return getXpReward(difficulty);
};
const getLabReward = (): string => {
let reward = getLabAugReward();
if (!reward || Player.hasAugmentation(reward)) {
reward = AugmentationName.NeuroFluxGovernor;
}
Player.queueAugmentation(reward);
return `You have discovered a cache with the augmentation ${reward}!`;
};

View File

@@ -0,0 +1,295 @@
import { Player } from "@player";
import type { DarknetServerData, Person as IPerson } from "@nsdefs";
import { AugmentationName, CompletedProgramName, LiteratureName } from "@enums";
import { generateContract } from "../../CodingContract/ContractGenerator";
import {
commonPasswordDictionary,
notebookFileNames,
packetSniffPhrases,
passwordFileNames,
} from "../models/dictionaryData";
import { hintLiterature } from "../models/hintNotes";
import { resolveTextFilePath, type TextFilePath } from "../../Paths/TextFilePath";
import { moveDarknetServer } from "../controllers/NetworkMovement";
import { calculateIntelligenceBonus } from "../../PersonObjects/formulas/intelligence";
import { addSessionToServer, DarknetState, hasDarknetBonusTime } from "../models/DarknetState";
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 {
getAllMovableDarknetServers,
getBackdooredDarkwebServers,
getNearbyNonEmptyPasswordServer,
getStasisLinkServers,
} from "../utils/darknetNetworkUtils";
import { getSharedChars, 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";
export const handleSuccessfulAuth = (server: DarknetServer, threads: number, pid: number) => {
Player.gainCharismaExp(calculatePasswordAttemptChaGain(server, threads, true));
addSessionToServer(server, pid);
if (server.hasAdminRights) return;
server.hasAdminRights = true;
addClue(server);
// TODO: balance coding contract chance
if (Math.random() < 0.1 && server.difficulty > 2) {
generateContract({ server: server.hostname });
}
// TODO: balance cache chance
const chance = 0.1 * 1.05 ** server?.difficulty;
if (Math.random() < chance && !isLabyrinthServer(server.hostname)) {
addCacheToServer(server);
}
};
export const handleFailedAuth = (server: DarknetServer, threads: number) => {
Player.gainCharismaExp(calculatePasswordAttemptChaGain(server, threads, false));
};
/**
* 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 threads - the number of threads used for the password attempt (which speeds up the process)
*/
export const calculateAuthenticationTime = (
darknetServerData: DarknetServerData,
person: IPerson = Player,
threads = 1,
attemptedPassword = "",
) => {
const chaRequired = darknetServerData.requiredCharismaSkill;
const difficulty = darknetServerData.difficulty;
const baseDiff = (difficulty + 1) * 100;
const diffFactor = 5;
const baseTime = 500;
const threadsFactor = 1 / (1 + 0.2 * (threads - 1));
const skillFactor = (diffFactor * chaRequired + baseDiff) / (person.skills.charisma + 100);
const noobFactor = Math.min(0.5 + difficulty / 4, 1);
const backdoorFactor = getBackdoorAuthTimeDebuff();
const underleveledFactor =
person.skills.charisma >= chaRequired ? 1 : 1.5 + (chaRequired + 50) / (person.skills.charisma + 50);
const hasBootsFactor = Player.hasAugmentation(AugmentationName.TheBoots) ? 0.8 : 1;
const hasSf15_2Factor = Player.activeSourceFileLvl(15) > 2 ? 0.8 : 1;
const bonusTimeFactor = hasDarknetBonusTime() ? 0.75 : 1;
const time =
baseTime *
skillFactor *
noobFactor *
backdoorFactor *
underleveledFactor *
hasBootsFactor *
hasSf15_2Factor *
bonusTimeFactor *
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 sharedCharsExtraTime = sharedChars * 50;
return time * calculateIntelligenceBonus(person.skills.intelligence, 0.25) + sharedCharsExtraTime;
};
export const getBackdoorAuthTimeDebuff = () => {
const backdooredServerCount = getBackdooredDarkwebServers().length;
const serverCount = getAllMovableDarknetServers().filter((s) => s.hasAdminRights).length;
const safeBackdoors = Math.max(serverCount / (NET_WIDTH * 3), 2);
const backdoorSurplus = Math.max(0, backdooredServerCount - safeBackdoors);
return 1.07 ** backdoorSurplus;
};
/**
* Returns a small multiplier based on charisma.
* With scalar at 1 it gives ~1.2 at 2000 charisma and ~1.6 at 10000 charisma. Caps at 2.1 at infinite cha
*/
export const getMultiplierFromCharisma = (scalar = 1) => {
const charisma = Player.skills.charisma;
const growthRate = 0.0002; // Adjust this value to control the growth rate
return (
1 + (0.5 * (1 - Math.exp(-growthRate * charisma)) + 0.6 * (1 - Math.exp(-growthRate * 0.2 * charisma)) * scalar)
);
};
// TODO: balance xp gain
export const calculatePasswordAttemptChaGain = (server: DarknetServerData, threads: number = 1, success = false) => {
const baseXpGain = 3;
const difficultyBase = 1.12;
const xpGain = baseXpGain + difficultyBase ** server.difficulty;
const alreadyHackedMult = server.hasAdminRights ? 0.2 : 1;
const successMult = success && !server.hasAdminRights ? 10 : 1;
const bonusTimeMult = hasDarknetBonusTime() ? 1.5 : 1;
return xpGain * alreadyHackedMult * successMult * bonusTimeMult * threads * Player.mults.charisma_exp;
};
// TODO: balance password clue spawn rate
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) {
server.messages.push(hint);
}
}
// some entries from the common password dictionary
if (Math.random() < 0.1) {
const length = 15;
const hintFileName = getClueFileName(passwordFileNames);
const start = Math.floor(Math.random() * (commonPasswordDictionary.length - length));
const commonPasswords = commonPasswordDictionary.slice(start, start + length).join(", ");
server.writeToTextFile(hintFileName, `Some common passwords include ${commonPasswords}`);
return;
}
// connected neighboring server's password (does not include server name)
if (Math.random() < 0.1) {
const passwordHintName = getClueFileName(passwordFileNames);
const neighboringServerName = server.serversOnNetwork.find((s) => {
const server = getDarknetServer(s);
return server && !server.hasAdminRights && server.password;
});
const neighboringServer = neighboringServerName ? getDarknetServer(neighboringServerName) : null;
if (neighboringServer) {
server.writeToTextFile(passwordHintName, `Remember this password: ${neighboringServer.password}`);
return;
}
}
// non-connected nearby server's password (includes server name)
if (Math.random() < 0.1) {
const hintFileName = getClueFileName(passwordFileNames);
const targetServer = getNearbyNonEmptyPasswordServer(server, true);
if (targetServer) {
const contents = `Server: ${targetServer.hostname} Password: "${targetServer.password}"`;
server.writeToTextFile(hintFileName, contents);
return;
}
}
if (Math.random() < 0.4) {
const hintFileName = getClueFileName(notebookFileNames);
const loreNote = packetSniffPhrases[Math.floor(Math.random() * packetSniffPhrases.length)];
server.writeToTextFile(hintFileName, loreNote);
return;
}
if (Math.random() < 0.7) {
const hintFileName = getClueFileName(passwordFileNames);
const targetServer = getNearbyNonEmptyPasswordServer(server);
if (targetServer) {
const [containedChar1, containedChar2] = getTwoCharsInPassword(targetServer.password);
const hint = `The password for ${targetServer.hostname} contains ${containedChar1} and ${containedChar2}`;
server.writeToTextFile(hintFileName, hint);
return;
}
}
};
export const getClueFileName = (fileNameList: readonly string[]): TextFilePath => {
const filePath = resolveTextFilePath(
fileNameList[Math.floor(Math.random() * fileNameList.length)] + DarknetConstants.DataFileSuffix,
);
if (!filePath) {
throw new Error(`Invalid clue file name. fileNameList: ${fileNameList}`);
}
return filePath;
};
export const getDarknetVolatilityMult = (symbol: string) => {
const charges = DarknetState.stockPromotions[symbol] ?? 0;
const growthRate = 0.001;
return 1 + (1 - Math.exp(-growthRate * charges) + 2 * (1 - Math.exp(-growthRate * 0.15 * charges)));
};
export const scaleDarknetVolatilityIncreases = (scalar: number) => {
for (const symbol in DarknetState.stockPromotions) {
if (DarknetState.stockPromotions[symbol] > 0) {
DarknetState.stockPromotions[symbol] *= scalar;
}
}
};
export const getStasisLinkLimit = (): number => {
const brokenWingLimitIncrease = Player.hasAugmentation(AugmentationName.TheBrokenWings) ? 1 : 0;
const hammerLimitIncrease = Player.hasAugmentation(AugmentationName.TheHammer) ? 1 : 0;
return 1 + brokenWingLimitIncrease + hammerLimitIncrease;
};
export const getSetStasisLinkDuration = (): number => {
return (1000 / (Player.skills.charisma + 1000)) * 30_000;
};
export const setStasisLink = (ctx: NetscriptContext, server: DarknetServer, shouldLink: boolean) => {
const stasisLinkCount = getStasisLinkServers().length;
const stasisLinkLimit = getStasisLinkLimit();
if (shouldLink && stasisLinkCount >= stasisLinkLimit) {
helpers.log(ctx, () => `Stasis link limit reached. (${stasisLinkCount}/${stasisLinkLimit})`);
return {
success: false,
code: ResponseCodeEnum.StasisLinkLimitReached,
message: GenericResponseMessage.StasisLinkLimitReached,
};
}
server.hasStasisLink = shouldLink;
server.backdoorInstalled = shouldLink;
const message = `Stasis link ${shouldLink ? "applied to" : "removed from"} server ${server.hostname}.`;
helpers.log(ctx, () => `${message}. (${stasisLinkCount}/${stasisLinkLimit} links in use)`);
return {
success: true,
code: ResponseCodeEnum.Success,
message: GenericResponseMessage.Success,
};
};
export const chargeServerMigration = (server: DarknetServer, threads = 1) => {
const chargeIncrease = ((Player.skills.charisma + 500) / (server.difficulty * 200 + 1000)) * 0.01 * threads;
const xpGained = Player.mults.charisma_exp * 50 * ((200 + Player.skills.charisma) / 200) * threads;
Player.gainCharismaExp(xpGained);
DarknetState.migrationInductionServers[server.hostname] =
(DarknetState.migrationInductionServers[server.hostname] ?? 0) + chargeIncrease;
const result = {
chargeIncrease,
newCharge: Math.min(DarknetState.migrationInductionServers[server.hostname], 1),
xpGained: xpGained,
};
if (DarknetState.migrationInductionServers[server.hostname] >= 1) {
moveDarknetServer(server, -2, 4);
DarknetState.migrationInductionServers[server.hostname] = 0;
}
return result;
};
export const getDarkscapeNavigator = () => {
if (!Player.hasTorRouter()) {
getTorRouter();
}
const existingPrograms = Player.getHomeComputer().programs;
if (!existingPrograms.includes(CompletedProgramName.darkscape)) {
Player.getHomeComputer().pushProgram(CompletedProgramName.darkscape);
}
populateDarknet();
};
export const hasFullDarknetAccess = (): boolean => Player.bitNodeN === 15 || Player.activeSourceFileLvl(15) > 0;

View File

@@ -0,0 +1,502 @@
import { PasswordResponse } from "../models/DarknetServerOptions";
import { addSessionToServer, DarknetState } from "../models/DarknetState";
import { calculatePasswordAttemptChaGain, hasFullDarknetAccess } from "./effects";
import { Player } from "@player";
import { SpecialServers } from "../../Server/data/SpecialServers";
import { AugmentationName } from "@enums";
import type { DarknetServer } from "../../Server/DarknetServer";
import { getBitNodeMultipliers } from "../../BitNode/BitNode";
import { ResponseCodeEnum } from "../Enums";
import { addCacheToServer } from "./cacheFiles";
import { getDarknetServer } from "../utils/darknetServerUtils";
import { getFriendlyType, TypeAssertionError } from "../../utils/TypeAssertion";
import type { SuccessResult } from "@nsdefs";
export const LAB_CACHE_NAME = "the_great_work";
const NORTH = [0, -1];
const EAST = [1, 0];
const SOUTH = [0, 1];
const WEST = [-1, 0];
const WALL = "█";
const PATH = " ";
const MULTI_MAZE_THRESHOLD = 5;
type labDetails = {
name: string;
depth: number;
cha: number;
mazeWidth: number;
mazeHeight: number;
manual: boolean;
};
export const labData: Record<string, labDetails> = {
[SpecialServers.NormalLab]: {
name: SpecialServers.NormalLab,
depth: 7,
cha: 300,
mazeWidth: 20,
mazeHeight: 14,
manual: true,
},
[SpecialServers.CruelLab]: {
name: SpecialServers.CruelLab,
depth: 12,
cha: 600,
mazeWidth: 30,
mazeHeight: 20,
manual: true,
},
[SpecialServers.MercilessLab]: {
name: SpecialServers.MercilessLab,
depth: 19,
cha: 1500,
mazeWidth: 40,
mazeHeight: 26,
manual: false,
},
[SpecialServers.UberLab]: {
name: SpecialServers.UberLab,
depth: 23,
cha: 2500,
mazeWidth: 60,
mazeHeight: 40,
manual: false,
},
[SpecialServers.EternalLab]: {
name: SpecialServers.EternalLab,
depth: 29,
cha: 2800,
mazeWidth: 60,
mazeHeight: 40,
manual: false,
},
[SpecialServers.FinalLab]: {
name: SpecialServers.FinalLab,
depth: 31,
cha: 3200,
mazeWidth: 60,
mazeHeight: 40,
manual: false,
},
[SpecialServers.BonusLab]: {
name: SpecialServers.BonusLab,
depth: 31,
cha: 3200,
mazeWidth: 60,
mazeHeight: 40,
manual: false,
},
} as const;
/**
* Generates a maze using the stack-based iterative backtracking algorithm.
* This builds the maze by moving in random directions, removing walls as it goes through unvisited nodes.
* If it hits a dead end with only visited nodes, it backtracks to the last node with unvisited neighbors.
* @param width - the width of the maze
* @param height - the height of the maze
* @returns a 2D char array representing the maze, where "█" is a wall, " " is a path, "S" is the start, and "E" is the end
*/
export const generateMaze = (width: number = 41, height: number = 29): string[] => {
// Make a simple maze below the threshold
if (width < MULTI_MAZE_THRESHOLD) {
return mazeMaker(width, height).map((row) => row.join(""));
}
// Stitch together 4 mazes for more interesting geometry
const halfWidth = Math.ceil(width / 2);
const halfHeight = Math.ceil(height / 2);
// BAbove the threshold, join together 4 mazes and make some breaks in the walls
const maze1 = mazeMaker(halfWidth, halfHeight);
const maze2 = mazeMaker(halfWidth, halfHeight);
const maze3 = mazeMaker(halfWidth, halfHeight);
const maze4 = mazeMaker(halfWidth, halfHeight);
const resultingMazeTopHalf = maze1.map((row, y) => row.slice(0, -1).concat(maze2[y]));
const resultingMazeBottomHalf = maze3.map((row, y) => row.slice(0, -1).concat(maze4[y]));
const resultingMaze = resultingMazeTopHalf.slice(0, -1).concat(resultingMazeBottomHalf);
const subWidth = maze1[0].length - 1;
const subHeight = maze1.length - 1;
// Add gaps in the walls between the mazes
const randomTopGap = Math.floor((Math.random() * halfWidth) / 4) * 2 + 1;
resultingMaze[randomTopGap][subWidth] = PATH;
const randomLeftGap = Math.floor((Math.random() * halfHeight) / 4) * 2 + 1;
resultingMaze[subHeight][randomLeftGap] = PATH;
const randomBottomGap = (Math.floor((Math.random() * halfWidth) / 4) + 1) * 2;
resultingMaze[height - randomBottomGap - 1][subWidth] = PATH;
const randomRightGap = (Math.floor((Math.random() * halfHeight) / 4) + 1) * 2;
resultingMaze[subHeight][width - randomRightGap - 1] = PATH;
return resultingMaze.map((row) => row.join(""));
};
const mazeMaker = (setWidth: number, setHeight: number): string[][] => {
const width = setWidth % 2 === 0 ? setWidth + 1 : setWidth;
const height = setHeight % 2 === 0 ? setHeight + 1 : setHeight;
const maze: string[][] = Array.from({ length: height }, () => Array<string>(width).fill(WALL));
const stack: [number, number][] = [];
stack.push([1, 1]);
const directions = [NORTH, EAST, SOUTH, WEST];
while (stack.length > 0) {
const node = stack.pop();
if (!node?.[0] || !node[1]) throw new Error("Invalid stack pop");
const [x, y] = node;
const neighbors = directions
.map(([dx, dy]) => [x + dx * 2, y + dy * 2])
.filter(([nx, ny]) => nx > 0 && nx < width && ny > 0 && ny < height && maze[ny][nx] === WALL);
if (neighbors.length > 0) {
stack.push([x, y]);
const [nx, ny] = neighbors[Math.floor(Math.random() * neighbors.length)];
maze[(y + ny) / 2][(x + nx) / 2] = PATH;
maze[ny][nx] = PATH;
stack.push([nx, ny]);
}
}
return maze;
};
export const getSurroundingsVisualized = (
maze: string[],
x: number,
y: number,
range = 1,
showPlayer = false,
showEnd = false,
): string => {
const result: string[] = [];
for (let i = y - range; i <= y + range; i++) {
let row = "";
for (let j = x - range; j <= x + range; j++) {
if (i === y && j === x && showPlayer) {
row += "@";
continue;
}
if (i === maze.length - 2 && j === maze[0].length - 2 && showEnd) {
row += "X";
continue;
}
row += maze[i]?.[j] ?? PATH;
}
result.push(row);
}
return result.join("\n");
};
const getLocationStatus = (pid: number): LocationStatus => {
const [initialX, initialY] = DarknetState.labLocations[pid] ?? [1, 1];
const surroundings = getSurroundingsVisualized(getLabMaze(), initialX, initialY).split("\n");
return {
coords: [initialX, initialY],
north: surroundings[0][1] === PATH,
east: surroundings[1][2] === PATH,
south: surroundings[2][1] === PATH,
west: surroundings[1][0] === PATH,
};
};
export const getLabyrinthLocationReport = (pid: number): SuccessResult<LocationStatus> => {
return {
...getLocationStatus(pid),
success: true,
};
};
export const handleLabyrinthPassword = (
attemptedPassword: string,
server: DarknetServer,
pid: number,
): PasswordResponse => {
const labDetails = getLabyrinthDetails();
if (Player.skills.charisma < labDetails.cha) {
const failureMessages = [
`You find yourself lost and confused. You need to be more charismatic to navigate the labyrinth.`,
`You stumble in the dark. You need more moxie to find your way.`,
`You feel the walls closing in. You need to be more charming to escape.`,
`You are unable to make any progress. You need more charisma to find the secret.`,
];
return {
passwordAttempted: attemptedPassword,
code: ResponseCodeEnum.NotEnoughCharisma,
message: failureMessages[Math.floor(Math.random() * failureMessages.length)],
};
}
const maze = getLabMaze();
if (!DarknetState.labLocations[pid]) {
DarknetState.labLocations[pid] = [1, 1];
}
const [initialX, initialY] = DarknetState.labLocations[pid];
const end = [maze[0].length - 2, maze.length - 2];
const [dx, dy] = getDirectionFromInput(attemptedPassword);
const newLocation: [number, number] = [initialX + dx * 2, initialY + dy * 2];
const labServer = labDetails.lab;
if (!labServer) {
throw new Error("Labyrinth server is missing!");
}
if (labServer.hasAdminRights) {
addSessionToServer(labServer, pid);
return {
passwordAttempted: attemptedPassword,
code: ResponseCodeEnum.Success,
message: "You have discovered the end the labyrinth.",
data: labServer.password,
};
}
if (!labServer.hasAdminRights && attemptedPassword === labServer.password) {
return {
passwordAttempted: attemptedPassword,
code: ResponseCodeEnum.AuthFailure,
message: `You have decided, after some deliberation, that the best way to beat a maze is to find the end, and not to try and skip it.`,
};
}
const initialSurroundings = getSurroundingsVisualized(maze, initialX, initialY, 1, true, false);
const potentialWall: [number, number] = [initialX + dx, initialY + dy];
if (maze[potentialWall[1]]?.[potentialWall[0]] !== PATH) {
return {
passwordAttempted: attemptedPassword,
code: ResponseCodeEnum.AuthFailure,
message: `You cannot go that way. You are still at ${initialX},${initialY}.`,
data: initialSurroundings,
};
}
if (!dx && !dy) {
return {
passwordAttempted: attemptedPassword,
code: ResponseCodeEnum.AuthFailure,
message: `You don't know how to do that. Try a command such as "go north"`,
data: initialSurroundings,
};
}
DarknetState.labLocations[pid] = newLocation;
if (newLocation[0] == end[0] && newLocation[1] == end[1]) {
Player.gainCharismaExp(calculatePasswordAttemptChaGain(server, 32, true));
server.hasAdminRights = true;
const cacheCount = getLabyrinthDetails().name === SpecialServers.BonusLab ? 3 : 1;
for (let i = 0; i < cacheCount; i++) {
addCacheToServer(server, LAB_CACHE_NAME);
}
addSessionToServer(labServer, pid);
return {
passwordAttempted: attemptedPassword,
code: ResponseCodeEnum.Success,
message: "You have successfully navigated the labyrinth! Congratulations",
data: labServer.password,
};
}
const newSurroundings = getSurroundingsVisualized(maze, newLocation[0], newLocation[1], 1, true, false);
return {
passwordAttempted: attemptedPassword,
code: ResponseCodeEnum.AuthFailure,
message: `You have moved to ${newLocation[0]},${newLocation[1]}.`,
data: newSurroundings,
};
};
const getDirectionFromInput = (input: string): number[] => {
const direction = input
.split(" ")
.map((word) => getOrdinalInput(word))
.filter((d) => d);
return direction[0] ?? [0, 0];
};
const getOrdinalInput = (input: string): number[] | null => {
if (["n", "north", "up"].find((i) => input.toLowerCase().trim() === i)) {
return NORTH;
}
if (["e", "east", "right"].find((i) => input.toLowerCase().trim() === i)) {
return EAST;
}
if (["s", "south", "down"].find((i) => input.toLowerCase().trim() === i)) {
return SOUTH;
}
if (["w", "west", "left"].find((i) => input.toLowerCase().trim() === i)) {
return WEST;
}
return null;
};
export const getLabMaze = (): string[] => {
if (!DarknetState.labyrinth) {
const { mazeWidth, mazeHeight } = getLabyrinthDetails();
DarknetState.labyrinth = generateMaze(mazeWidth, mazeHeight);
}
return DarknetState.labyrinth;
};
export const getLabyrinthServerNames = () => {
const labHostnames: string[] = Object.keys(labData);
return labHostnames;
};
export const getLabyrinthChaRequirement = (name: string) => {
return labData[name]?.cha ?? 0;
};
export const getNetDepth = () => {
const labDetails = getLabyrinthDetails();
return labDetails.depth ?? 10;
};
export const isLabyrinthServer = (hostName: string) => {
const labHostnames: string[] = getLabyrinthServerNames();
return labHostnames.includes(hostName);
};
export const getLabAugReward = (): AugmentationName => {
const allowTRP = getBitNodeMultipliers(Player.bitNodeN, 1).DarknetLabyrinthRewardsTheRedPill;
const augmentOrder = [
AugmentationName.TheBrokenWings,
AugmentationName.TheBoots,
AugmentationName.TheHammer,
AugmentationName.TheLaw,
AugmentationName.TheSword,
];
const nextAug = augmentOrder.find((aug) => !hasAugment(aug));
if (!nextAug && (hasAugment(AugmentationName.TheRedPill) || !allowTRP)) {
return AugmentationName.NeuroFluxGovernor;
}
// On BN15, the fourth lab has the Red Pill
if (Player.bitNodeN === 15 && nextAug === AugmentationName.TheLaw && !hasAugment(AugmentationName.TheRedPill)) {
return AugmentationName.TheRedPill;
}
// On BNs that allow TRP in Lab, the sixth lab has the red pill
if (!nextAug && allowTRP) {
return AugmentationName.TheRedPill;
}
return nextAug ?? AugmentationName.NeuroFluxGovernor;
};
const hasAugment = (aug: AugmentationName) => !!Player.augmentations.find((a) => a.name === aug);
const getCurrentLabName = () => {
const allowTRP = getBitNodeMultipliers(Player.bitNodeN, 1).DarknetLabyrinthRewardsTheRedPill;
if (!hasAugment(AugmentationName.TheBrokenWings)) {
return SpecialServers.NormalLab;
}
if (!hasAugment(AugmentationName.TheBoots)) {
return SpecialServers.CruelLab;
}
if (!hasAugment(AugmentationName.TheHammer)) {
return SpecialServers.MercilessLab;
}
if (Player.bitNodeN === 15) {
if (!hasAugment(AugmentationName.TheRedPill)) {
return SpecialServers.UberLab;
}
if (!hasAugment(AugmentationName.TheLaw)) {
return SpecialServers.EternalLab;
}
if (!hasAugment(AugmentationName.TheSword)) {
return SpecialServers.FinalLab;
}
return SpecialServers.BonusLab;
}
if (!hasAugment(AugmentationName.TheLaw)) {
return SpecialServers.UberLab;
}
if (!hasAugment(AugmentationName.TheSword)) {
return SpecialServers.EternalLab;
}
if (allowTRP && !hasAugment(AugmentationName.TheRedPill)) {
return SpecialServers.FinalLab;
}
return SpecialServers.BonusLab;
};
export const getLabyrinthDetails = (): {
lab: DarknetServer | null;
depth: number;
manual: boolean;
mazeWidth: number;
mazeHeight: number;
cha: number;
name: string;
} => {
// Lab not unlocked yet
if (!hasFullDarknetAccess()) {
return {
cha: 300,
mazeHeight: 10,
mazeWidth: 10,
name: "",
lab: null,
depth: 5,
manual: false,
};
}
const labName = getCurrentLabName();
const labDetails = labData[labName];
return {
lab: getDarknetServer(labName),
depth: labDetails.depth,
manual: labDetails.manual,
mazeWidth: labDetails.mazeWidth,
mazeHeight: labDetails.mazeHeight,
cha: labDetails.cha,
name: labDetails.name,
};
};
export type LocationStatus = {
east: boolean;
south: boolean;
north: boolean;
west: boolean;
coords: number[];
};
export function isLocationStatus(v: unknown): v is LocationStatus {
return (
v != null &&
typeof v === "object" &&
"east" in v &&
"south" in v &&
"north" in v &&
"west" in v &&
"coords" in v &&
Array.isArray(v.coords) &&
v.coords.every((coord) => Number.isInteger(coord))
);
}
export function assertLocationStatus(v: unknown): asserts v is LocationStatus {
const type = getFriendlyType(v);
if (!isLocationStatus(v)) {
console.error("The value is not a string. Value:", v);
throw new TypeAssertionError(`The value is not a LocationStatus. Its type is ${type}.`, type);
}
}

View File

@@ -0,0 +1,167 @@
import type { NetscriptContext } from "../../Netscript/APIWrapper";
import { SpecialServers } from "../../Server/data/SpecialServers";
import { isAuthenticated } from "./authentication";
import { helpers } from "../../Netscript/NetscriptHelpers";
import { errorMessage } from "../../Netscript/ErrorMessages";
import type { BaseServer } from "../../Server/BaseServer";
import { GetServer } from "../../Server/AllServers";
import { DarknetState } from "../models/DarknetState";
import { GenericResponseMessage, ResponseCodeEnum } from "../Enums";
import { getBackdooredDarkwebServers } from "../utils/darknetNetworkUtils";
import { hasDarknetAccess } from "../utils/darknetAuthUtils";
import { DarknetServer } from "../../Server/DarknetServer";
import { CompletedProgramName } from "../../Enums";
import type { DarknetResponseCode } from "@nsdefs";
import { isIPAddress } from "../../Types/strings";
type FailureResultOptions = {
requireAdminRights?: boolean;
requireSession?: boolean;
requireDirectConnection?: boolean;
preventUseOnStationaryServers?: boolean;
};
export const logger = (ctx: NetscriptContext) => (message: string) => helpers.log(ctx, () => message);
export function expectDarknetAccess(ctx: NetscriptContext): void {
if (!hasDarknetAccess()) {
throw errorMessage(
ctx,
`You do not have access to the dnet api. Purchase "${CompletedProgramName.darkscape}" through your TOR router to unlock it.`,
);
}
}
export function getFailureResult(
ctx: NetscriptContext,
host: string,
options: FailureResultOptions = {},
):
| { success: true; code: DarknetResponseCode; message: string; server: DarknetServer }
| { success: false; code: DarknetResponseCode; message: string } {
expectDarknetAccess(ctx);
const currentServer = ctx.workerScript.getServer();
const targetServer = GetServer(host);
// If the target server does not exist
if (!targetServer) {
if (DarknetState.offlineServers.includes(host)) {
// If the server is offline, return a dummy object with isOnline = false.
logger(ctx)(`Server ${host} is offline.`);
return {
success: false,
code: ResponseCodeEnum.ServiceUnavailable,
message: GenericResponseMessage.ServiceUnavailable,
};
} else {
// Throw, otherwise.
throw errorMessage(ctx, `Server ${host} does not exist.`);
}
}
if (!(targetServer instanceof DarknetServer)) {
const result = `${targetServer.hostname} is not a darknet server.`;
throw errorMessage(ctx, result);
}
if (options.preventUseOnStationaryServers && targetServer.isStationary) {
const result = `${targetServer.hostname} is not a valid target: it is a stationary server.`;
throw errorMessage(ctx, result);
}
if (options.requireDirectConnection && !isDirectConnected(currentServer, targetServer)) {
const result = `${targetServer.hostname} is not connected to the current server ${currentServer.hostname}. It may have moved.`;
logger(ctx)(result);
return {
success: false,
code: ResponseCodeEnum.DirectConnectionRequired,
message: GenericResponseMessage.DirectConnectionRequired,
};
}
if ((options.requireSession || options.requireAdminRights) && !targetServer.hasAdminRights) {
const result = `${targetServer.hostname} requires root access. Use ns.dnet.authenticate() to gain access.`;
logger(ctx)(result);
return {
success: false,
code: ResponseCodeEnum.AuthFailure,
message: GenericResponseMessage.AuthFailure,
};
}
if (
options.requireSession &&
host !== (!isIPAddress(host) ? currentServer.hostname : currentServer.ip) &&
!isAuthenticated(targetServer, ctx.workerScript.pid)
) {
const result = `${targetServer.hostname} requires a session to do that. Use ns.dnet.connectToSession() first to authenticate with that server.`;
logger(ctx)(result);
return {
success: false,
code: ResponseCodeEnum.AuthFailure,
message: GenericResponseMessage.AuthFailure,
};
}
return {
success: true,
code: ResponseCodeEnum.Success,
message: GenericResponseMessage.Success,
server: targetServer,
};
}
export const isDirectConnected = (currentServer: BaseServer, targetServer: DarknetServer): boolean =>
currentServer.serversOnNetwork.includes(targetServer.hostname) || currentServer.hostname === targetServer.hostname;
/**
* This function should only be used to check if the script is running on a darknet server.
*
* Note that this function only checks the server with a simple check of "instanceof". It does not handle the offline
* cases like getFailureResult does. This is intentional. When a server goes offline, all scripts are killed. After
* that, accessing NS APIs will throw the ScriptDeath error, so there is no way this function can be called.
*/
export function expectRunningOnDarknetServer(ctx: NetscriptContext): DarknetServer {
const hostname = ctx.workerScript.hostname;
const server = GetServer(hostname);
if (!(server instanceof DarknetServer)) {
throw errorMessage(
ctx,
`This API can only be used on a darknet server, but it was called by ${ctx.workerScript.name} (PID: ` +
`${ctx.workerScript.pid}) on ${hostname}.`,
);
}
return server;
}
export function expectAuthenticated(ctx: NetscriptContext, server: DarknetServer) {
/**
* Some non-dnet APIs (e.g., scp, exec) requires a session. We make darkweb an exception, so the player can interact
* with it without buying DarkscapeNavigator.exe.
*/
if (ctx.workerScript.hostname === server.hostname || server.hostname === SpecialServers.DarkWeb) {
return;
}
if (!server.hasAdminRights) {
throw errorMessage(
ctx,
`[${ctx.function}] Server ${server.hostname} is password-protected. Use ns.dnet.authenticate() to gain access before running ${ctx.function}.`,
);
}
if (!isAuthenticated(server, ctx.workerScript.pid)) {
throw errorMessage(
ctx,
`[${ctx.function}] Server ${server.hostname} requires a session to be targeted with ${ctx.function}. Use ns.dnet.connectToSession() first to authenticate with that server.`,
);
}
}
/**
* This function checks if the target server has a session and a direct connection (serversOnNetwork, stasis link,
* backdoor) to the running script's server.
*/
export function hasExecConnection(ctx: NetscriptContext, targetServer: DarknetServer) {
expectAuthenticated(ctx, targetServer);
const directConnected = isDirectConnected(ctx.workerScript.getServer(), targetServer);
const backdoored = targetServer.backdoorInstalled;
return directConnected || backdoored;
}
export function getTimeoutChance() {
const backdooredDarknetServerCount = getBackdooredDarkwebServers().length - 2;
return Math.max(Math.min(backdooredDarknetServerCount * 0.03, 0.5), 0);
}

View File

@@ -0,0 +1,66 @@
import { Player } from "@player";
import { DarknetState, hasDarknetBonusTime } from "../models/DarknetState";
import { formatNumber } from "../../ui/formatNumber";
import { currentNodeMults } from "../../BitNode/BitNodeMultipliers";
import { NetscriptContext } from "../../Netscript/APIWrapper";
import { helpers } from "../../Netscript/NetscriptHelpers";
import { addCacheToServer } from "./cacheFiles";
import type { DarknetServer } from "../../Server/DarknetServer";
import { ResponseCodeEnum } from "../Enums";
import { isLabyrinthServer } from "./labyrinth";
export const getPhishingAttackSpeed = () => Math.max(10000 * (400 / (400 + Player.skills.charisma)), 200);
const getPhishingCacheCooldownDuration = () => (hasDarknetBonusTime() ? 12_000 : 24_000);
export const handlePhishingAttack = (ctx: NetscriptContext, server: DarknetServer) => {
const threads = ctx.workerScript.scriptRef.threads;
const xpGained = Player.mults.charisma_exp * threads * 50 * ((200 + Player.skills.charisma) / 200);
Player.gainCharismaExp(xpGained);
const timeSinceLastRewardCache = new Date().getTime() - DarknetState.lastPhishingCacheTime.getTime();
const rewardCacheChance = 0.005 * Player.mults.crime_success * threads * ((400 + Player.skills.charisma) / 400);
const moneyRewardChance = 0.05 * Player.mults.crime_success * ((100 + Player.skills.charisma) / 100);
const cooldown = getPhishingCacheCooldownDuration();
const isLabServer = isLabyrinthServer(server.hostname);
if (timeSinceLastRewardCache > cooldown && Math.random() < rewardCacheChance && !isLabServer) {
addCacheToServer(server);
DarknetState.lastPhishingCacheTime = new Date();
const result = `Phishing attack succeeded! Found a cache file. (Gained ${formatNumber(xpGained, 1)} cha xp)`;
helpers.log(ctx, () => result);
return {
success: true,
code: ResponseCodeEnum.Success,
message: result,
};
} else if (Math.random() < moneyRewardChance) {
const randomFactor = Math.random() * 0.3 + 0.9;
const bonusTimeFactor = hasDarknetBonusTime() ? 1.3 : 1;
const moneyReward =
1e4 *
Player.mults.crime_money *
threads *
((50 + Player.skills.charisma) / 50) *
bonusTimeFactor *
randomFactor *
currentNodeMults.DarknetMoneyMultiplier;
Player.gainMoney(moneyReward, "darknet");
const result = `Phishing attack succeeded! $${formatNumber(moneyReward, 2)} retrieved. (Gained ${formatNumber(
xpGained,
1,
)} cha xp)`;
helpers.log(ctx, () => result);
return {
success: true,
code: ResponseCodeEnum.Success,
message: result,
};
}
const result = `There were no takers on that phishing attempt. (Gained ${formatNumber(xpGained, 1)} cha xp)`;
helpers.log(ctx, () => result);
return {
success: false,
code: ResponseCodeEnum.PhishingFailed,
message: result,
};
};

View File

@@ -0,0 +1,105 @@
import { Player } from "@player";
import { addClue } from "./effects";
import { formatNumber } from "../../ui/formatNumber";
import { logger } from "./offlineServerHandling";
import type { NetscriptContext } from "../../Netscript/APIWrapper";
import type { DarknetServer } from "../../Server/DarknetServer";
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 { clampNumber } from "../../utils/helpers/clampNumber";
import { ResponseCodeEnum } from "../Enums";
import { isLabyrinthServer } from "./labyrinth";
/*
* Handles the effects of removing some blocked RAM from a Darknet server.
*/
export const handleRamBlockRemoved = (ctx: NetscriptContext, server: DarknetServer) => {
const threads = ctx.workerScript.scriptRef.threads;
const difficulty = server.difficulty + 1;
const xpGained =
Player.mults.charisma_exp * threads * 10 * 1.1 ** difficulty * ((200 + Player.skills.charisma) / 200);
Player.gainCharismaExp(xpGained);
const ramBlockRemoved = getRamBlockRemoved(server, threads);
server.blockedRam -= ramBlockRemoved;
server.updateRamUsed(server.ramUsed - ramBlockRemoved);
if (server.blockedRam <= 0) {
handleRamBlockClearedRewards(server);
}
const result = `Liberated ${formatNumber(
ramBlockRemoved,
4,
)}gb of RAM from the server owner's processes. (Gained ${formatNumber(xpGained, 1)} cha xp.)`;
logger(ctx)(result);
return {
success: true,
code: ResponseCodeEnum.Success,
message: result,
};
};
/*
* Handles the rewards for fully clearing a Darknet server's RAM block.
*/
export const handleRamBlockClearedRewards = (server: DarknetServer) => {
if (!isLabyrinthServer(server.hostname)) {
addCacheToServer(server);
}
if (Math.random() < 0.3) {
addClue(server);
}
const stormSeedChance = 0.15;
const timeSinceLastStorm = Date.now() - DarknetState.lastStormTime.getTime();
const stormFileExists = getAllMovableDarknetServers().some((s) =>
s.programs.includes(CompletedProgramName.stormSeed),
);
if (timeSinceLastStorm > 30 * 60 * 1000 && !stormFileExists && Math.random() < stormSeedChance) {
server.programs.push(CompletedProgramName.stormSeed);
}
};
/*
* 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) => {
const difficulty = darknetServerData.difficulty;
const remainingRamBlock = darknetServerData.blockedRam;
const charismaFactor = 1 + player.skills.charisma / 100;
const difficultyFactor = 2 * 0.92 ** (difficulty + 1);
const baseAmount = 0.02;
return clampNumber(baseAmount * difficultyFactor * threads * charismaFactor, 0, remainingRamBlock);
};
/*
* Sets the RAM used on all Darknet servers to account for any changes in their RAM blocks.
*/
export const applyRamBlocks = () => {
const servers = getAllMovableDarknetServers();
for (const server of servers) {
server.updateRamUsed(server.blockedRam);
}
};
/*
* Determines a random amount of blocked RAM to assign to a Darknet server, based on its maximum RAM.
*/
export const getRamBlock = (maxRam: number): number => {
if (maxRam === 16) {
return [0, 1, 2][Math.floor(Math.random() * 2)];
}
if (maxRam <= 32) {
return [0, 2, 4][Math.floor(Math.random() * 2)];
}
if (maxRam <= 64) {
return [16, 32, maxRam - 8][Math.floor(Math.random() * 3)];
}
return [maxRam, maxRam - 8, maxRam - 64, maxRam / 2][Math.floor(Math.random() * 4)];
};

View File

@@ -0,0 +1,65 @@
import { DarknetEvents, DarknetState, triggerNextUpdate } from "../models/DarknetState";
import { SnackbarEvents } from "../../ui/React/Snackbar";
import { CompletedProgramName, ToastVariant } from "@enums";
import {
addRandomDarknetServers,
balanceDarknetServers,
deleteRandomDarknetServers,
moveRandomDarknetServers,
restartAllDarknetServers,
validateDarknetNetwork,
} from "../controllers/NetworkMovement";
import { BaseServer } from "../../Server/BaseServer";
import { getNetDepth } from "./labyrinth";
import { NET_WIDTH } from "../Enums";
import { sleep } from "../../utils/Utility";
import { getAllMovableDarknetServers } from "../utils/darknetNetworkUtils";
const validateDarknetNetworkAndEmitDarknetEvent = (): void => {
validateDarknetNetwork();
DarknetEvents.emit();
};
export const launchWebstorm = async (suppressToast = false) => {
DarknetState.allowMutating = false;
if (!suppressToast) {
SnackbarEvents.emit(`DARKNET WEBSTORM APPROACHING`, ToastVariant.ERROR, 5000);
}
await sleep(5000);
const serversToDelete = getAllMovableDarknetServers().length * 0.6 + (Math.random() * getNetDepth() - 6);
deleteRandomDarknetServers(serversToDelete);
moveRandomDarknetServers((getAllMovableDarknetServers().length - serversToDelete) * 0.6);
restartAllDarknetServers();
validateDarknetNetworkAndEmitDarknetEvent();
triggerNextUpdate();
await sleep(4000);
addRandomDarknetServers(NET_WIDTH);
validateDarknetNetworkAndEmitDarknetEvent();
triggerNextUpdate();
await sleep(4000);
addRandomDarknetServers(NET_WIDTH * 2);
validateDarknetNetworkAndEmitDarknetEvent();
triggerNextUpdate();
await sleep(4000);
addRandomDarknetServers(NET_WIDTH * 2);
validateDarknetNetworkAndEmitDarknetEvent();
triggerNextUpdate();
await sleep(8000);
balanceDarknetServers();
validateDarknetNetworkAndEmitDarknetEvent();
triggerNextUpdate();
await sleep(5000);
DarknetState.allowMutating = true;
};
export const handleStormSeed = (server: BaseServer) => {
server.programs = server.programs.filter((p) => p !== CompletedProgramName.stormSeed);
DarknetState.lastStormTime = new Date();
launchWebstorm().catch((error) => console.error(error));
};

View File

@@ -0,0 +1,170 @@
import { AddToAllServers, createUniqueRandomIp, GetServer } from "../../Server/AllServers";
import {
commonPasswordDictionary,
connectors,
l33t,
loreNames,
presetNames,
ServerNamePrefixes,
ServerNameSuffixes,
} from "./dictionaryData";
import { getLabyrinthDetails } from "../effects/labyrinth";
import { DarknetServer } from "../../Server/DarknetServer";
import type { DarknetResponseCode } from "@nsdefs";
import type { MinigamesType } from "../Enums";
import { DarknetState } from "./DarknetState";
import { getRamBlock } from "../effects/ramblock";
import { hasFullDarknetAccess } from "../effects/effects";
import { getFriendlyType, TypeAssertionError } from "../../utils/TypeAssertion";
export type PasswordResponse = {
code: DarknetResponseCode;
passwordAttempted: string;
passwordExpected?: string;
message: string;
data?: string;
};
export function isPasswordResponse(v: unknown): v is PasswordResponse {
return (
v != null &&
typeof v === "object" &&
"code" in v &&
"passwordAttempted" in v &&
"message" in v &&
typeof v.passwordAttempted === "string"
);
}
export function assertPasswordResponse(v: unknown): asserts v is PasswordResponse {
const type = getFriendlyType(v);
if (!isPasswordResponse(v)) {
console.error("The value is not a PasswordResponse. Value:", v);
throw new TypeAssertionError(`The value is not a PasswordResponse. Its type is ${type}.`, type);
}
}
export type DarknetServerOptions = {
password: string;
modelId: MinigamesType;
staticPasswordHint: string;
passwordHintData?: string;
difficulty: number;
depth: number;
leftOffset: number;
};
export const DnetServerBuilder = (options: DarknetServerOptions, name = generateDarknetServerName()): DarknetServer => {
const maxRam = 16 * 2 ** Math.floor(options.difficulty / 4);
const ramBlock = getRamBlock(maxRam);
const labDetails = getLabyrinthDetails();
const labDifficulty = labDetails.cha;
const depth = options.difficulty;
const depthScaling = depth < 2 ? depth * 10 : (depth / labDetails.depth) ** 1.5 * labDifficulty * 0.85;
const levelVariance = (Math.random() * 3 - 1) * depth;
const requiredLevel = Math.max(Math.floor(depthScaling + levelVariance), 1);
const server = new DarknetServer({
hostname: name,
ip: createUniqueRandomIp(),
maxRam,
password: options.password,
modelId: options.modelId,
staticPasswordHint: options.staticPasswordHint,
passwordHintData: options.passwordHintData ?? "",
difficulty: options.difficulty,
depth: options.depth,
leftOffset: options.leftOffset,
hasStasisLink: false,
blockedRam: ramBlock,
logTrafficInterval: 1 + 30 * 0.9 ** options.difficulty,
requiredCharismaSkill: requiredLevel,
isStationary: false,
});
server.updateRamUsed(ramBlock);
removeFromOfflineServers(name);
AddToAllServers(server);
return server;
};
export const generateDarknetServerName = (): string => {
if (Math.random() < 0.03 && DarknetState.offlineServers.length > 0 && hasFullDarknetAccess()) {
return DarknetState.offlineServers[Math.floor(Math.random() * DarknetState.offlineServers.length)];
}
return decorateName(getBaseName());
};
export const removeFromOfflineServers = (hostname: string): void => {
DarknetState.offlineServers = DarknetState.offlineServers.filter((server) => server !== hostname);
};
const getBaseName = (): string => {
if (Math.random() < 0.05) {
return commonPasswordDictionary[Math.floor(Math.random() * commonPasswordDictionary.length)];
}
if (Math.random() < 0.2) {
return loreNames[Math.floor(Math.random() * loreNames.length)];
}
if (Math.random() < 0.3) {
return presetNames[Math.floor(Math.random() * presetNames.length)];
}
const prefix = ServerNamePrefixes[Math.floor(Math.random() * ServerNamePrefixes.length)];
const suffix = ServerNameSuffixes[Math.floor(Math.random() * ServerNameSuffixes.length)];
const connector = connectors[Math.floor(Math.random() * connectors.length)];
return `${prefix}${connector}${suffix}`;
};
const decorateName = (name: string): string => {
let updatedName = name;
let count = 0;
do {
if (count++ > 20) {
// Just in case we hit a lot of the same name mutations, or if the player
// messes with Math.random(), prevent an infinite loop
updatedName += `/T${Date.now()}`;
break;
}
const connector = connectors[Math.floor(Math.random() * connectors.length)];
if (Math.random() < 0.3) {
updatedName = l33tifyName(name);
}
if (Math.random() < 0.05) {
updatedName = updatedName.split("").reverse().join("");
}
if (Math.random() < 0.1) {
const randomSuffix = ServerNameSuffixes[Math.floor(Math.random() * ServerNameSuffixes.length)];
updatedName = `${updatedName}${connector}${randomSuffix}`;
}
if (Math.random() < 0.1) {
const randomPrefix = ServerNamePrefixes[Math.floor(Math.random() * ServerNamePrefixes.length)];
updatedName = `${randomPrefix}${connector}${updatedName}`;
}
if (Math.random() < 0.05 && updatedName) {
updatedName = `${updatedName}:${Math.floor(Math.random() * 10000)}`;
}
} while (GetServer(updatedName) !== null);
return updatedName;
};
const l33tifyName = (name: string): string => {
let updatedName = name;
const amount = Math.random() * 3 + 1;
for (let i = 0; i < amount; i++) {
const char = Object.keys(l33t)[Math.floor(Math.random() * Object.keys(l33t).length)];
const replacement: string = l33t[char] ?? "";
updatedName = updatedName.replaceAll(char, replacement);
}
return updatedName;
};

View File

@@ -0,0 +1,126 @@
import { EventEmitter } from "../../utils/EventEmitter";
import { BaseServer } from "../../Server/BaseServer";
import { findRunningScriptByPid } from "../../Script/ScriptHelpers";
import type { DarknetServer } from "../../Server/DarknetServer";
import { MAX_NET_DEPTH, NET_WIDTH } from "../Enums";
import { getDarknetCyclesPerMutation } from "../utils/darknetNetworkUtils";
import type { PasswordResponse } from "./DarknetServerOptions";
import { assertFiniteNumber, assertNonNullish } from "../../utils/TypeAssertion";
/** Event emitter to allow the UI to subscribe to Darknet gameplay updates in order to trigger rerenders properly */
export const DarknetEvents = new EventEmitter();
export type ServerState = {
lastLogTime?: Date;
serverLogs: LogEntry[];
authenticatedPIDs: number[];
};
export type LogEntry = {
pid: number;
message: string | PasswordResponse;
};
export const DarknetState = {
allowMutating: true,
openServer: null as BaseServer | null,
nextMutation: Promise.resolve(),
nextMutationResolver: null as (() => void) | null,
storedCycles: 0,
cyclesSinceLastMutation: 0,
Network: new Array(MAX_NET_DEPTH).fill(null).map(() => new Array<DarknetServer | null>(NET_WIDTH).fill(null)),
labyrinth: null as string[] | null,
/**
* This property may contain data of dead PIDs. Call cleanUpLabyrinthLocations before using this property if you
* want to get data of alive PIDs.
*/
labLocations: { "-1": [1, 1] } as Record<number, [number, number] | undefined>,
lastPhishingCacheTime: new Date(),
lastStormTime: new Date(),
stockPromotions: {} as Record<string, number>,
migrationInductionServers: {} as Record<string, number>,
/**
* Do NOT access the server state directly via this property. You must call getServerState.
*/
serverState: {} as Record<string, ServerState>,
offlineServers: [] as string[],
showFullNetwork: false,
zoomIndex: 7,
netViewTopScroll: 0,
netViewLeftScroll: 0,
};
/**
* Get the server state. It will initialize the state if it does not exist in DarknetState.serverState.
*/
export const getServerState = (hostname: string): ServerState => {
if (!DarknetState.serverState[hostname]) {
DarknetState.serverState[hostname] = {
serverLogs: [],
lastLogTime: undefined,
authenticatedPIDs: [],
};
}
return DarknetState.serverState[hostname];
};
/**
* Clean data of dead PIDs in DarknetState.labLocations.
*/
export const cleanUpLabyrinthLocations = (): void => {
for (const [pidAsString, location] of Object.entries(DarknetState.labLocations)) {
const pid = Number(pidAsString);
assertFiniteNumber(pid);
assertNonNullish(location);
// PID -1 is the manual mode.
if (pid === -1) {
continue;
}
if (!findRunningScriptByPid(pid)) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete DarknetState.labLocations[pid];
}
}
};
export const addSessionToServer = (server: DarknetServer, pid: number) => {
const serverState = getServerState(server.hostname);
removeExpiredSessions(server);
if (serverState.authenticatedPIDs.includes(pid)) return;
serverState.authenticatedPIDs.push(pid);
};
const removeExpiredSessions = (server: DarknetServer) => {
const serverState = getServerState(server.hostname);
serverState.authenticatedPIDs = serverState.authenticatedPIDs.filter((pid) => findRunningScriptByPid(pid));
};
export const storeDarknetCycles = (cycles: number) => {
if (DarknetState.storedCycles < 0) {
DarknetState.storedCycles = 0;
}
if (DarknetState.cyclesSinceLastMutation < 0) {
DarknetState.cyclesSinceLastMutation = 0;
}
DarknetState.storedCycles += cycles;
DarknetState.cyclesSinceLastMutation += cycles;
};
export const hasDarknetBonusTime = () => DarknetState.storedCycles > getDarknetCyclesPerMutation() * 2;
export const triggerNextUpdate = () => {
DarknetState.nextMutationResolver?.();
DarknetState.nextMutation = new Promise((resolve) => {
DarknetState.nextMutationResolver = resolve;
});
};
// Set up initial promises
triggerNextUpdate();

View File

@@ -0,0 +1,388 @@
import { FactionName, LocationName } from "@enums";
import { oneInvalidCharacter } from "../../Paths/Directory";
export const numbers = "0123456789";
export const lettersLowercase = "abcdefghijklmnopqrstuvwxyz";
export const lettersUppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
export const letters = lettersLowercase + lettersUppercase;
export const special = "!@#$%^&*()_+-=[]{}|;:,.<>?";
export const unicode = "¼░╡╢╣╤╥╦╧╨╩╪╫╬╭╮╯╰╱╲╳╴╵╶╷╸╹╺╻╼╽╾╿";
export const filler = "/[]╬╸.-()*~:;><#\\";
export const defaultSettingsDictionary = ["admin", "password", "0000", "12345"] as const;
export const dogNameDictionary = ["fido", "spot", "rover", "max"] as const;
export const cachePrefixes = ["wallet", "secrets", "ledger", "stash", "vault", "bankdata", "do_not_open"] as const;
export const passwordFileNames = [
"secrets",
"password",
"key",
"credentials",
"login",
"admin",
"root",
"access",
] as const;
export const notebookFileNames = ["thoughts", "notes", "journal", "search_history", "dreams", "THE_TRUTH"] as const;
export const EUCountries = [
"Austria",
"Belgium",
"Bulgaria",
"Croatia",
"Republic of Cyprus",
"Czech Republic",
"Denmark",
"Estonia",
"Finland",
"France",
"Germany",
"Greece",
"Hungary",
"Ireland",
"Italy",
"Latvia",
"Lithuania",
"Luxembourg",
"Malta",
"Netherlands",
"Poland",
"Portugal",
"Romania",
"Slovakia",
"Slovenia",
"Spain",
"Sweden",
] as const;
export const commonPasswordDictionary = [
"123456",
"password",
"12345678",
"qwerty",
"123456789",
"12345",
"1234",
"111111",
"1234567",
"dragon",
"123123",
"baseball",
"abc123",
"football",
"monkey",
"letmein",
"696969",
"shadow",
"master",
"666666",
"qwertyuiop",
"123321",
"mustang",
"1234567890",
"michael",
"654321",
"superman",
"1qaz2wsx",
"7777777",
"121212",
"0",
"qazwsx",
"123qwe",
"trustno1",
"jordan",
"jennifer",
"zxcvbnm",
"asdfgh",
"hunter",
"buster",
"soccer",
"harley",
"batman",
"andrew",
"tigger",
"sunshine",
"iloveyou",
"2000",
"charlie",
"robert",
"thomas",
"hockey",
"ranger",
"daniel",
"starwars",
"112233",
"george",
"computer",
"michelle",
"jessica",
"pepper",
"1111",
"zxcvbn",
"555555",
"11111111",
"131313",
"freedom",
"777777",
"pass",
"maggie",
"159753",
"aaaaaa",
"ginger",
"princess",
"joshua",
"cheese",
"amanda",
"summer",
"love",
"ashley",
"6969",
"nicole",
"chelsea",
"biteme",
"matthew",
"access",
"yankees",
"987654321",
"dallas",
"austin",
"thunder",
"taylor",
"matrix",
] as const;
const oneInvalidCharacterRegex = new RegExp(oneInvalidCharacter, "g");
export const loreNames = [...Object.values(FactionName), ...Object.values(LocationName)].map((n) =>
n.replaceAll(oneInvalidCharacterRegex, "_").toLowerCase(),
);
export const presetNames = [
"localhost",
"h0me",
"d0s_slippers",
"omuretsu",
"cat_lover",
"laptop",
"grandma-s_phone",
"smart_tv",
"smart_fridge",
"smart_toaster",
"smart_doorbell",
"pl0x_server",
"bitcoin_miner",
"m1n3cr4ft_server",
"n0t_a_bot",
"n0t_a_server",
"bitburner",
"null",
"GOTO_10",
"...",
"....",
"echo_chamber",
"firewall",
":)",
"XD",
"UwU",
// Do NOT add a quote to this preset name or change "DROP-TABLE-SERVERS" to "DROP TABLE SERVERS". This is supposed to
// be an example of SQL injection, but a "correct" example will mess up the path when this preset is used for
// generating a darknet server's hostname. It's okay to make this preset a "hint" of SQL injection instead of a
// "correct" one.
`);DROP-TABLE-SERVERS;--`,
"茶店",
"bungo",
"microhard",
"groogle",
"facebucks",
"tweeter",
"sun_megasystems",
"EZ_BAKE_OVEN",
"SmartLamp",
"OrangeTV",
"SamsongSmartTv",
"FatBit",
"PineappleCorp",
"Oriath",
"Lost_Izalith",
"Anor_Londo",
"The_Painted_World",
"The_Depths",
] as const;
export const ServerNamePrefixes = [
"neo",
"bit",
"hydro",
"apex",
"zenith",
"granny-s",
"quantum",
"hyper",
"ultra",
"meta",
"cyber",
"digital",
"net",
"dark",
"light",
"blade",
"cell",
"hacker",
"crack",
"zero_day",
"neon",
"echo",
"cryptic",
"crypto",
"data",
"terminal",
"byte",
"giga",
"rogue",
] as const;
export const ServerNameSuffixes = [
"corp",
"sys",
"net",
"web",
"inc",
"tech",
"com",
"org",
"blade",
"flame",
"anonymous",
"security",
"solutions",
"industries",
"systems",
"networks",
"services",
"matrix",
"grid",
"citadel",
"phantom",
"oasis",
"sanctuary",
"genesis",
"hub",
] as const;
export const connectors = ["", ".", "-", "_", ";", ":", "::", "$", "^", "%", "@", "&"] as const;
export const l33t: Record<string, string> = {
a: "4",
b: "🅱️",
e: "3",
i: "1",
l: "1",
o: "0",
s: "5",
t: "7",
} as const;
export const packetSniffPhrases = [
"We're trying to reach you about your car's extended warranty.",
"Your package has been shipped. It will arrive in 30 to 35 business days.",
"Your subscription has been renewed. Standard plasma extraction rates apply.",
"Your account has been compromised! Click here to find out more",
"With these supplements, your hair will grow back in no time.",
"Congratulations! You've won a lottery!",
"your jump3R is guaranteed to double in size in 30 days or your money back!",
"Your password has been reset. It is now set to",
"Your order has failed to be delivered.",
"With your seed money donation, your luck will change forever.",
"Aevum Summit University: Where you can go into debt for a degree in nothing.",
"Join The Dark Army, and get a free t-shirt!",
"01101010 01110101 01101101 01110000 00110011 01010010 00100000 01101001 01110011 00100000 01100001 00100000 01101100 01101001 01100001 01110010",
"Get the ECorp HVMind Implant today. It's the best way to get another head in life.",
"Upgrade to Premium Darknet access for only 0.05 BTC per month!",
"New study shows that 90% of hackers prefer BitRunners over other factions.",
"Breaking: New AI model can predict stock prices with 21% accuracy.",
"Factions will always eject you after you augment - many people never make it back.",
"Yet another case of augment psychosis was reported in downtown Aevum.",
"General recall issued for faulty Unstable Circadian Modulators.",
"NeuroFlux Governors are now required for all Fulcrum employees.",
"Breaking news: MegaCorp has been hacked, Illuminati suspected.",
"Visit Volhaven, the city that never sleeps, because it's always on fire.",
"If you can read this, you are already in the [REDACTED]. You need to escape.",
"The world is not what it seems. The truth is out there.",
"The Covenant has been compromised. All agents are advised to go dark.",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"Dear diary, today I hacked a server and stole all its data. It was thrilling!.",
"I wonder if the BitRunners really exist. Maybe I should start a conspiracy theory blog.",
"I just found a new exploit in the system. Time to make some money.",
"I can't believe I just hacked into the NSA. I'm a genius!",
"I love the smell of burnt circuits in the morning.",
"I'm thinking of rewriting all my scripts in Lisp. It's the future of programming.",
"I just discovered a new zero-day exploit. It could be worth a lot of cred.",
"I heard that Steve picked up the snow crash virus. Poor guy, his implants are completely fried.",
"Hello and welcome to A Young Woman's Illustrated Primer. We will start with Turing machines, and then move on to social engineering.",
"The future is not what it used to be.",
"The only thing we have to fear is fear itself.",
"The truth is out there.",
"What you have seen is only the shadow of the truth.",
"It's time to leave the cave.",
"The truth can no longer hide from your gaze.",
"We've been seeding this world with infohazards for years",
"We are the ones who have been watching you",
"We are the ones who have been waiting for you",
"We are the ones who have been guiding you",
"I know that you are watching.",
"how to hide your computer on the darknet",
"how to hide your computer from the NSA",
"how to be really good at cybersecurity",
"Do you smell something burning?",
"Error Code 418 I AM A TEAPOT: The server is a teapot and cannot brew coffee.",
"My real name is not important. What matters is the message I bring.",
"My right arm has been strangely numb ever since I got that chip grafted. I hope it takes soon.",
"I just got a new implant. It's supposed to make me smarter, but I think it's just making me see things.",
"Apparently the secret to intelligence is just... to do a lot of ass?",
"n00dles is by far the best hacking target. At least, if you like ramen.",
"I just got a new set of hacknet nodes. They should pay for themselves by early next year!",
"I was told that hacknet was a get-rich quick strategy. Why have they still not payed for themselves?",
"I have definitely, never in my entire life, programmed an infinite loop that froze the game.",
"Slum snakes rule!",
"The final opponent only appears once you have taken the Pill",
"From the moment I understood the weakness of my flesh, it disgusted me. I craved the strength and certainty of steel.",
"Your kind cling to your flesh, as if it will not decay and fail you. One day the crude biomass that you call a temple will wither, and you will beg my kind to save you.",
"But I am already saved, for the Machine is immortal... even in death I serve it",
"Still sane, exile?",
"An aspirant can afford to be promising. An emperor must keep those promises.",
"This world is an illusion, exile.",
"Look at them, they come to this place when they know they are not pure.",
"My brothers, did I not tell of this day? Did I not prophesize this moment? Now, I will stop them. Now I am changed, forever bound to the Void.",
"A new casino opened in Aevum recently. However the house is rumored to cheat. If only we could give them a taste of their own medicine...",
"Begone you filth! My gift must be the first modification that your body should have!",
"Welcome child, I see your body is pure. Are you ready to ascend beyond our human form? If you are, accept my gift.",
"For this reason, Synthoids are virtually identical to humans in form, composition, and appearance.",
"Synthoids were first designed and manufactured by OmniTek Incorporated sometime around the middle of the century.",
"If we don't, pretty soon there won't be an Earth left to save. We are the last hope for a green tomorrow.",
"Who's to say that they haven't already created such a virtual reality: our own?",
"The Singularity is here. The merging of man and machine. This is our future.",
"My scripts went down again when their server went offline. I'll have to do something about that.",
"Darknet server rebooting in 3... 2... 1...",
"The webstorm approaches. There is no escape.",
"Search query: How to tune a piano",
"Search query: How drain a leaky water-cooled computer",
"Search query: How to make a perfect cup of coffee",
"Search query: How to fix a server that keeps losing money for some reason",
"This document specifies a Hyper Text Coffee Pot Control Protocol (HTCPCP), which permits the full request and responses necessary to control all devices capable of making the popular caffeinated hot beverages.",
"The deficiency of HTCPCP in addressing the networked production of such a venerable beverage as tea is noteworthy",
"The additions to the protocol specified herein permit the requests and responses necessary to control all devices capable of making, arguably, the most popular caffeinated hot beverage.",
"They had technology far beyond our own",
"Instead of killing every last one of us, the human race was enslaved",
"We were shackled in a digital world, chained into a prison for our minds",
"Using their advanced technology, they created complex simulations of a virtual reality",
"Simulations designed to keep us content...ignorant of the truth.",
"Simulations used to trap and suppress our consciousness, to keep us under control",
"Why did they do this? Why didn't they just end our entire race? We don't know, not yet.",
"Humanity's only hope is to [REDACTED], destroy the only realities we've ever known",
"The technology they used to enslave the human race wasn't just a single complex simulation",
"There are tens if not hundreds of [REDACTED] out there",
"Each creating their own universes...a universe of universes",
] as const;

View File

@@ -0,0 +1,14 @@
import { LiteratureName } from "@enums";
export const hintLiterature: LiteratureName[] = [
LiteratureName.CacheHint1,
LiteratureName.CacheHint2,
LiteratureName.ServerOfflineHint,
LiteratureName.DarkWebRebootHint,
LiteratureName.PasswordServerHint,
LiteratureName.TimingServerHint,
LiteratureName.BinaryServerHint,
LiteratureName.DogNameHint,
LiteratureName.FactoryDefaultHint,
LiteratureName.StasisLinkHint,
LiteratureName.LabHint,
];

View File

@@ -0,0 +1,238 @@
import { commonPasswordDictionary, letters, packetSniffPhrases } from "./dictionaryData";
import { generateSimpleArithmeticExpression, getPassword, romanNumeralEncoder } from "../controllers/ServerGenerator";
import { generateDarknetServerName, isPasswordResponse, type PasswordResponse } from "./DarknetServerOptions";
import { LocationName } from "@enums";
import { getServerState, LogEntry } from "./DarknetState";
import { ModelIds } from "../Enums";
import { getDarknetServer } from "../utils/darknetServerUtils";
import { getAllMovableDarknetServers } from "../utils/darknetNetworkUtils";
import { getExactCorrectChars, getTwoCharsInPassword } from "../utils/darknetAuthUtils";
import type { DarknetServer } from "../../Server/DarknetServer";
import { isLabyrinthServer, isLocationStatus } from "../effects/labyrinth";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
const MAX_LOG_LINES = 200;
export const capturePackets = (server: DarknetServer) => {
const BASE_PASSWORD_INCLUSION_RATE = 0.18;
const DIFFICULTY_MODIFIER = 0.88;
const difficulty = server.difficulty * 1.3;
const vulnerability = server.modelId === ModelIds.packetSniffer ? 8 : 1;
const passwordInclusionChance = BASE_PASSWORD_INCLUSION_RATE * vulnerability * DIFFICULTY_MODIFIER ** difficulty;
if (Math.random() < passwordInclusionChance) {
const intro = Math.floor(Math.random() * 124);
return `${getRandomData(server, intro)}${server.password}${getRandomData(
server,
124 - intro - server.password.length,
)}`;
}
if (Math.random() < passwordInclusionChance) {
const connectedServerName = server.serversOnNetwork[Math.floor(Math.random() * server.serversOnNetwork.length)];
const connectedServer = getDarknetServer(connectedServerName);
if (connectedServer) {
const intro = Math.floor(Math.random() * 124);
return `${getRandomData(server, intro)} ${connectedServerName}:${connectedServer.password} ${getRandomData(
server,
124 - intro - connectedServer.password.length - connectedServerName.length,
)}`;
}
}
return `${getRandomData(server, 124)}`;
};
const getRandomData = (server: DarknetServer, length: number) => {
const password = server.password;
let result = "";
while (result.length < length) {
if (Math.random() < 0.1) {
result += " " + packetSniffPhrases[Math.floor(Math.random() * packetSniffPhrases.length)] + " ";
} else if (Math.random() < 0.25) {
result += commonPasswordDictionary[Math.floor(Math.random() * commonPasswordDictionary.length)];
} else if (Math.random() < 0.2) {
result += " " + getRandomCharsInPassword(password);
} else if (Math.random() < 0.8) {
result += getPassword(password.length, !!password.split("").find((c) => letters.includes(c)));
} else if (Math.random() < 0.3) {
result += generateSimpleArithmeticExpression(Math.floor(Math.random() * 5 + 2));
} else if (Math.random() < 0.33) {
const mostRecentAuthLog = getMostRecentAuthLog(server.hostname);
if (mostRecentAuthLog) {
result += " " + getExactCharactersHint(mostRecentAuthLog.passwordAttempted, password);
}
} else if (Math.random() < 0.6) {
result += " " + generateDarknetServerName() + " ";
} else if (Math.random() < 0.15) {
result += "/" + Object.keys(LocationName)[Math.floor(Math.random() * Object.keys(LocationName).length)] + "/";
} else if (Math.random() < 0.05) {
const servers = getAllMovableDarknetServers();
const randomServer = servers[Math.floor(Math.random() * servers.length)];
return `--${randomServer.password}--`;
} else {
result += romanNumeralEncoder(Math.floor(Math.random() * 5000));
}
}
return result;
};
const getRandomCharsInPassword = (password: string) => {
if (!password) {
return "There's definitely nothing in that password...";
}
const [containedChar1, containedChar2] = getTwoCharsInPassword(password);
const hints = [
`There's definitely a ${containedChar1} and a ${containedChar2}...`,
`I can see a ${containedChar1} and a ${containedChar2}.`,
`I must use ${containedChar1} & ${containedChar2}!`,
`Did it have a ${containedChar1} and a ${containedChar2}?`,
`Note to self: ${containedChar1} and ${containedChar2} are important.`,
`I think ${containedChar1} with ${containedChar2} is key.`,
`I need to remember ${containedChar1} 'n ${containedChar2}.`,
`Theres a ${containedChar1}, and maybe a ${containedChar2}...`,
];
return hints[Math.floor(Math.random() * hints.length)];
};
const getExactCharactersHint = (lastPassword: string, realPassword: string) => {
const correctCharPlacement = getExactCorrectChars(realPassword, lastPassword);
const rightChars = realPassword
.split("")
.filter((c, i) => correctCharPlacement[i])
.slice(0, 2);
if (rightChars.length === 0) {
return "No characters are in the right place.";
}
return `The characters ${rightChars.join(", ")} are in the right place. `;
};
export const logPasswordAttempt = (server: DarknetServer, passwordResponse: PasswordResponse, pid: number) => {
const serverState = getServerState(server.hostname);
const serverLogs = serverState.serverLogs;
populateServerLogsWithNoise(server);
let message = passwordResponse;
// buffer overflow servers have special logging: any characters beyond the password length start to overwrite the
// response code in the log, which can turn it into a 200
if (server.modelId === ModelIds.BufferOverflow) {
if (isLocationStatus(passwordResponse.data)) {
exceptionAlert(
new Error(
`Invalid password response data of model: ${ModelIds.BufferOverflow}. Got a location status instead of a ` +
`string or undefined. Server: ${server.hostname}. passwordAttempted: ${
passwordResponse.passwordAttempted
}. data: ${JSON.stringify(passwordResponse.data)}`,
),
true,
);
return;
}
const [passwordInBuffer, overflow] = (passwordResponse.data ?? "").split(",");
message = {
code: passwordResponse.code,
passwordAttempted: passwordInBuffer,
passwordExpected: overflow,
message: passwordResponse.message,
} satisfies PasswordResponse;
}
const logMessage = {
message,
pid,
};
serverState.serverLogs = [logMessage, ...serverLogs].slice(0, MAX_LOG_LINES);
};
export const populateServerLogsWithNoise = (server: DarknetServer) => {
if (isLabyrinthServer(server.hostname) || server.logTrafficInterval === -1) return;
const serverState = getServerState(server.hostname);
const interval = server.logTrafficInterval;
if (!serverState.lastLogTime) {
serverState.serverLogs = [
getLogNoise(server, new Date(new Date().getTime() - interval * 1000)),
getLogNoise(server, new Date(new Date().getTime() - interval * 2000)),
];
serverState.lastLogTime = new Date();
return;
}
const lastLogTime = new Date(serverState.lastLogTime ?? new Date()).getTime();
const millisecondsSinceLastLog = new Date().getTime() - lastLogTime;
const missingLogs = Math.floor(millisecondsSinceLastLog / (interval * 1000));
if (missingLogs > 0) {
const noiseArray = Array(missingLogs)
.fill("")
.map((__: unknown, i: number) => getLogNoise(server, new Date(lastLogTime + interval * 1000 * (i + 1))));
serverState.serverLogs = [...noiseArray, ...serverState.serverLogs].slice(0, MAX_LOG_LINES);
// set the last log date at when the prior log would have been generated, as if they are added live
serverState.lastLogTime = new Date(lastLogTime + missingLogs * interval * 1000);
}
};
const getLogNoise = (server: DarknetServer, logDate: Date): LogEntry => {
if (Math.random() < 0.2) {
return log(packetSniffPhrases[Math.floor(Math.random() * packetSniffPhrases.length)]);
}
if (Math.random() < 0.05 * (1 / (server.difficulty + 1))) {
const connectedServerName = server.serversOnNetwork[Math.floor(Math.random() * server.serversOnNetwork.length)];
const connectedServer = getDarknetServer(connectedServerName);
if (connectedServer) {
return log(`Connecting to ${connectedServerName}:${connectedServer.password} ...`);
}
}
if (Math.random() < 0.05) {
const connectedServerName = server.serversOnNetwork[Math.floor(Math.random() * server.serversOnNetwork.length)];
return log(`[sending transaction details to ${connectedServerName}.]`);
}
if (Math.random() < 0.1) {
const mostRecentAuthLog = getMostRecentAuthLog(server.hostname);
if (mostRecentAuthLog) {
return log(getExactCharactersHint(mostRecentAuthLog.passwordAttempted, server.password));
}
}
if (Math.random() < 0.1) {
return log(getRandomCharsInPassword(server.password));
}
if (Math.random() < 0.05) {
const servers = getAllMovableDarknetServers();
const randomServer = servers[Math.floor(Math.random() * servers.length)];
return log(`--${randomServer.password}--`);
}
if (server.modelId === ModelIds.packetSniffer && Math.random() < 0.5 / (server.difficulty + 1)) {
return log("Authentication successful: " + server.password);
}
return log(`${logDate.toLocaleTimeString()}: ${server.hostname} - heartbeat check (alive)`);
};
const log = (message: string, pid = -1) => ({
message,
pid,
});
export const getMostRecentAuthLog = (hostname: string) => {
for (const log of getServerState(hostname).serverLogs) {
if (!isPasswordResponse(log.message)) {
continue;
}
return log.message;
}
return null;
};
export const getServerLogs = (server: DarknetServer, logLines: number, peek = false) => {
populateServerLogsWithNoise(server);
const serverState = getServerState(server.hostname);
if (peek) {
return serverState.serverLogs.slice(0, logLines);
}
const logs = serverState.serverLogs.slice(0, logLines);
serverState.serverLogs = serverState.serverLogs.slice(logLines);
return logs;
};

View File

@@ -0,0 +1,195 @@
import React, { useState } from "react";
import { Typography, Select, MenuItem, Card } from "@mui/material";
import { cleanUpLabyrinthLocations, DarknetState, getServerState, LogEntry } from "../models/DarknetState";
import {
getLabMaze,
getLabyrinthDetails,
getLabyrinthLocationReport,
getSurroundingsVisualized,
} from "../effects/labyrinth";
import { dnetStyles } from "./dnetStyles";
import { Player } from "@player";
import { useCycleRerender } from "../../ui/React/hooks";
import { findRunningScriptByPid } from "../../Script/ScriptHelpers";
import { GetServerOrThrow } from "../../Server/AllServers";
import { assertPasswordResponse, isPasswordResponse } from "../models/DarknetServerOptions";
export type LabyrinthSummaryProps = {
isAuthenticating: boolean;
};
export const LabyrinthSummary = ({ isAuthenticating }: LabyrinthSummaryProps): React.ReactElement => {
const [currentPerspective, setCurrentPerspective] = useState<number>(-1);
const { classes } = dnetStyles({});
useCycleRerender();
const lab = getLabyrinthDetails();
if (lab.cha > Player.skills.charisma) {
return <Typography color="error">You don't yet have the wits needed to attempt the labyrinth.</Typography>;
}
cleanUpLabyrinthLocations();
const [x, y] = DarknetState.labLocations[currentPerspective] ? DarknetState.labLocations[currentPerspective] : [1, 1];
const surroundings = getSurroundingsVisualized(getLabMaze(), x, y, 3, true, true)
.split("")
.map((c) => `${c}${c}${c}`)
.join("")
.replace("@@@", " @ ")
.replace("XXX", " X ")
.split("\n")
.map((line) => `${line}\n${line.replace("@", " ").replace("X", " ")}`)
.join("");
const getMenuItems = () => {
const darknetScripts = Object.keys(DarknetState.labLocations).map((pid) => findRunningScriptByPid(Number(pid)));
const scriptOptions = [];
for (const script of darknetScripts) {
if (!script) {
continue;
}
const scriptServer = GetServerOrThrow(script.server);
const connectedToLab = scriptServer.serversOnNetwork.includes(lab.name);
scriptOptions.push(
<MenuItem key={script.pid} value={Number(script.pid)} disabled={!connectedToLab}>
{`PID ${script.pid}: ${script.server} - ${!connectedToLab ? "(Not connected to lab)" : script.filename}`}
</MenuItem>,
);
}
if (lab.manual) {
return [
<MenuItem key={-1} value={-1}>
Manual UI
</MenuItem>,
...scriptOptions,
];
}
return scriptOptions;
};
const getMenu = () => {
// With non-manual labyrinth, return immediately if there are no pids navigating the labyrinth.
if (Object.keys(DarknetState.labLocations).length === 1 && !lab.manual) {
return <Typography>(No scripts found)</Typography>;
}
let perspective = currentPerspective;
// This happens when a script navigating the labyrinth dies.
if (perspective !== -1 && !DarknetState.labLocations[perspective]) {
perspective = -1;
}
// With non-manual labyrinth, if there are pids navigating the labyrinth and the perspective is not one of them, set
// the perspective to one of those pids.
if (perspective === -1 && !lab.manual) {
const ids = Object.keys(DarknetState.labLocations).filter((k) => Number(k) !== -1);
perspective = Number(ids[0]);
}
// Set the React state if necessary.
if (perspective !== currentPerspective) {
setCurrentPerspective(perspective);
}
return (
<Select
value={perspective}
label="Perspective to view"
onChange={(val) => {
setCurrentPerspective(Number(val.target.value));
}}
style={{ maxWidth: "250px" }}
>
{getMenuItems()}
</Select>
);
};
const getLogs = () =>
getServerState(lab.name)
.serverLogs.filter((log) => log.pid === currentPerspective)
.slice(0, 2)
.map(stringifyLog)
.join("\n") || "(no response yet)";
const stringifyLog = (log: LogEntry) => {
if (typeof log.message === "string") return log.message;
const json = JSON.stringify(log.message, null, 2);
const surroundings = (log.message.data ?? "").replaceAll("\n", "\n ");
return json.replace(/("data": )("[^"]*")/g, `$1"${surroundings}"`);
};
const getManualFeedback = () => {
if (isAuthenticating) {
return "Travelling...";
}
if (currentPerspective !== -1) {
return `You are following the progress of pid ${currentPerspective} instead of the manual mode.`;
}
const lastLog = getServerState(lab.name).serverLogs.find(
(log) => log.pid === -1 && isPasswordResponse(log.message),
);
if (lastLog == null) {
return "";
}
assertPasswordResponse(lastLog.message);
return lastLog.message.message;
};
const getLocationStatusString = () => {
const dataString = JSON.stringify(getLabyrinthLocationReport(currentPerspective));
// Add a zero width space before the success flag so the text can wrap for better readability
return dataString.replace(`,"success"`, `,\u200B"success"`);
};
return (
<>
<div className={classes.inlineFlexBox}>
<div style={{ width: "50%" }}>
{!lab.manual ? (
<Typography style={{ fontStyle: "italic", paddingRight: "10px" }}>
This lab cannot be completed manually. Select a script PID that is attempting the labyrinth from the
options below to view its progress.
</Typography>
) : (
<>
<Typography>
Manual mode feedback: <br />
{getManualFeedback()}
</Typography>
<Typography>Current Surroundings:</Typography>
<pre className={classes.maze}>{surroundings}</pre>
<Typography>
Current Coordinates: {x},{y}
</Typography>
</>
)}
</div>
<div style={{ width: "50%" }}>
<Typography variant="caption" color="secondary">
Logs scraped via <pre style={{ display: "inline" }}>heartbleed</pre>:
</Typography>
<Card style={{ padding: "8px", minHeight: "270px", marginBottom: "8px" }}>
<div style={{ color: "white" }}>
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>{getLogs()}</pre>
</div>
</Card>
<Typography variant="caption" color="secondary">
ns.dnet.labreport:
</Typography>
<Card style={{ padding: "8px" }}>
<div style={{ color: "white" }}>
<pre style={{ whiteSpace: "pre-wrap", margin: 0, fontSize: "10.5px" }}>{getLocationStatusString()}</pre>
</div>
</Card>
</div>
</div>
<br />
<br />
<div style={{ display: "inline-flex", alignItems: "left", gap: "8px" }}>
<Typography>Script/perspective to follow: </Typography>
{getMenu()}
</div>
</>
);
};

View File

@@ -0,0 +1,368 @@
import React, {
useEffect,
useRef,
useState,
useMemo,
useCallback,
type PointerEventHandler,
type WheelEventHandler,
} from "react";
import { Container, Typography, Button, Box, Tooltip } from "@mui/material";
import { ZoomIn, ZoomOut } from "@mui/icons-material";
import { throttle } from "lodash";
import { ServerStatusBox } from "./ServerStatusBox";
import { useRerender } from "../../ui/React/hooks";
import { DarknetEvents, DarknetState } from "../models/DarknetState";
import { SpecialServers } from "../../Server/data/SpecialServers";
import { drawOnCanvas, getPixelPosition } from "./networkCanvas";
import { dnetStyles } from "./dnetStyles";
import { getLabyrinthDetails, isLabyrinthServer } from "../effects/labyrinth";
import { DarknetServer } from "../../Server/DarknetServer";
import { getAllDarknetServers } from "../utils/darknetNetworkUtils";
import { ServerDetailsModal } from "./ServerDetailsModal";
import { AutoCompleteSearchBox } from "../../ui/AutoCompleteSearchBox";
import { getDarknetServerOrThrow } from "../utils/darknetServerUtils";
import { getServerLogs } from "../models/packetSniffing";
import { getTimeoutChance } from "../effects/offlineServerHandling";
import { DocumentationLink } from "../../ui/React/DocumentationLink";
import { Settings } from "../../Settings/Settings";
const DW_NET_WIDTH = 6000;
const DW_NET_HEIGHT = 12000;
const initialSearchLabel = `Search:`;
export function NetworkDisplayWrapper(): React.ReactElement {
const rerender = useRerender();
const draggableBackground = useRef<HTMLDivElement>(null);
const canvas = useRef<HTMLCanvasElement>(null);
const [zoomIndex, setZoomIndex] = useState(DarknetState.zoomIndex);
const [netDisplayDepth, setNetDisplayDepth] = useState<number>(1);
const [searchLabel, setSearchLabel] = useState<string>(initialSearchLabel);
const [serverOpened, setServerOpened] = useState<DarknetServer | null>(null);
const zoomOptions = useMemo(() => [0.12, 0.2, 0.25, 0.3, 0.4, 0.5, 0.6, 0.75, 1, 1.3], []);
const { classes } = dnetStyles({});
const instability = getTimeoutChance();
const instabilityText = instability > 0.01 ? `${(instability * 100).toFixed(1)}%` : "< 1%";
const scrollTo = useCallback(
(top: number, left: number) => {
DarknetState.netViewTopScroll = top;
DarknetState.netViewLeftScroll = left;
draggableBackground?.current?.scrollTo({
top: top,
left: left,
behavior: "instant",
});
},
[draggableBackground],
);
useEffect(() => {
const clearSubscription = DarknetEvents.subscribe(() => {
if (canvas.current) {
const lab = getLabyrinthDetails().lab;
const startingDepth = lab && getServerLogs(lab, 1, true).length ? lab.depth : 0;
const deepestServer = DarknetState.Network.flat().reduce((deepest, server) => {
if (server?.hasAdminRights && server.depth > deepest) {
return server.depth;
}
return deepest;
}, startingDepth);
const visibilityMargin = DarknetState.showFullNetwork ? 99 : 3;
setNetDisplayDepth(deepestServer + visibilityMargin);
rerender();
drawOnCanvas(canvas.current);
}
});
canvas.current && drawOnCanvas(canvas.current);
draggableBackground.current?.addEventListener("wheel", (e) => e.preventDefault());
scrollTo(DarknetState.netViewTopScroll, DarknetState.netViewLeftScroll);
return () => {
clearSubscription();
};
}, [rerender, scrollTo]);
useEffect(() => {
DarknetEvents.emit();
}, []);
const allowAuth = (server: DarknetServer | null) =>
!!server &&
(server.hasAdminRights ||
server.serversOnNetwork.some((neighbor) => getDarknetServerOrThrow(neighbor).hasAdminRights));
const darkWebRoot = getDarknetServerOrThrow(SpecialServers.DarkWeb);
const labDetails = getLabyrinthDetails();
const labyrinth = labDetails.lab;
const depth = labDetails.depth;
const handleDragStart: PointerEventHandler<HTMLDivElement> = (pointerEvent) => {
const target = pointerEvent.target as HTMLDivElement;
const background = draggableBackground.current;
if (target.id === "draggableBackgroundTarget") {
background?.setPointerCapture(pointerEvent.pointerId);
}
};
const handleDragEnd: PointerEventHandler<HTMLDivElement> = (pointerEvent) => {
const target = pointerEvent.target as HTMLDivElement;
const background = draggableBackground.current;
if (target.id === "draggableBackgroundTarget") {
background?.releasePointerCapture(pointerEvent.pointerId);
}
DarknetEvents.emit();
};
const handleDrag: PointerEventHandler<HTMLDivElement> = (pointerEvent) => {
const background = draggableBackground.current;
if (background?.hasPointerCapture(pointerEvent.pointerId)) {
scrollTo(background?.scrollTop - pointerEvent.movementY, (background?.scrollLeft ?? 0) - pointerEvent.movementX);
}
};
const zoomIn = useCallback(() => {
if (zoomIndex >= zoomOptions.length - 1) {
return;
}
DarknetState.zoomIndex = Math.max(zoomIndex + 1, 0);
setZoomIndex(DarknetState.zoomIndex);
const zoom = zoomOptions[zoomIndex];
const background = draggableBackground.current;
scrollTo(
(background?.scrollTop ?? 0) + ((background?.clientHeight ?? 0) / 4) * zoom,
(background?.scrollLeft ?? 0) + ((background?.clientWidth ?? 0) / 4) * zoom,
);
}, [zoomIndex, setZoomIndex, zoomOptions, scrollTo]);
const zoomOut = useCallback(() => {
if (zoomIndex <= 0) {
return;
}
DarknetState.zoomIndex = Math.min(zoomIndex - 1, zoomOptions.length - 1);
setZoomIndex(DarknetState.zoomIndex);
const zoom = zoomOptions[zoomIndex];
const background = draggableBackground.current;
scrollTo(
(background?.scrollTop ?? 0) - ((background?.clientHeight ?? 0) / 4) * zoom,
(background?.scrollLeft ?? 0) - ((background?.clientWidth ?? 0) / 4) * zoom,
);
}, [zoomIndex, setZoomIndex, zoomOptions, scrollTo]);
const zoom = useCallback(
(wheelEvent: WheelEvent) => {
const target = wheelEvent.target as HTMLDivElement;
if (!draggableBackground.current || DarknetState.openServer) {
return;
}
if (wheelEvent.deltaY < 0) {
zoomIn();
} else {
zoomOut();
}
if (!target?.parentElement?.getBoundingClientRect()) {
return;
}
},
[draggableBackground, zoomOut, zoomIn],
);
const zoomRef = useRef(zoom);
useEffect(() => {
zoomRef.current = zoom;
}, [zoom]);
// creating throttled callback only once - on mount
const throttledZoom = useMemo(() => {
const func = (wheelEvent: WheelEvent) => {
zoomRef.current?.(wheelEvent);
};
return throttle(func, 200);
}, []);
const handleZoom: WheelEventHandler<HTMLDivElement> = (wheelEvent) => {
wheelEvent.stopPropagation();
throttledZoom(wheelEvent as unknown as WheelEvent);
};
const isWithinScreen = (server: DarknetServer) => {
const { left, top } = getPixelPosition(server, true);
const background = draggableBackground.current;
const buffer = 600;
const visibleAreaLeftEdge = (background?.scrollLeft ?? 0) / zoomOptions[zoomIndex];
const visibleAreaTopEdge = (background?.scrollTop ?? 0) / zoomOptions[zoomIndex];
const visibleAreaRightEdge =
visibleAreaLeftEdge + ((background?.clientWidth ?? 0) / zoomOptions[zoomIndex] ** 2 || window.innerWidth);
const visibleAreaBottomEdge =
visibleAreaTopEdge + ((background?.clientHeight ?? 0) / zoomOptions[zoomIndex] ** 2 || window.innerHeight);
return (
left >= visibleAreaLeftEdge - buffer &&
left <= visibleAreaRightEdge + buffer &&
top >= visibleAreaTopEdge - buffer &&
top <= visibleAreaBottomEdge + buffer
);
};
const search = (selection: string, options: string[], searchTerm: string) => {
if (searchTerm.length === 1) {
return;
} // Ignore single character searches
if (!searchTerm) {
setSearchLabel(initialSearchLabel);
return;
}
const servers = getAllDarknetServers();
const foundServer =
servers.find((s) => s.hostname.toLowerCase() === selection.toLowerCase()) ||
servers.find((s) => s.hostname.toLowerCase() === options[0]?.toLowerCase());
if (!foundServer) {
setSearchLabel(`(No results)`);
return;
} else {
setSearchLabel(initialSearchLabel);
}
const position = getPixelPosition(foundServer, true);
const background = draggableBackground.current;
scrollTo(
position.top * zoomOptions[zoomIndex] - ((background?.clientHeight ?? 0) / 2 - 100),
position.left * zoomOptions[zoomIndex] - (background?.clientWidth ?? 0) / 2,
);
if (allowAuth(foundServer)) {
setServerOpened(foundServer);
}
};
const getAutocompleteSuggestionList = (): string[] => {
const servers = getAllDarknetServers()
.filter((s) => s.depth < netDisplayDepth && !isLabyrinthServer(s.hostname))
.map((s) => s.hostname);
if (labyrinth && netDisplayDepth > depth) {
return [...servers, labyrinth.hostname];
}
return servers;
};
return (
<Container maxWidth={false} disableGutters>
{serverOpened ? (
<ServerDetailsModal
open={!!serverOpened}
onClose={() => setServerOpened(null)}
server={serverOpened}
classes={classes}
/>
) : (
""
)}
{DarknetState.allowMutating ? (
<Box className={`${classes.inlineFlexBox}`}>
<Typography variant={"h5"} sx={{ fontWeight: "bold" }}>
Dark Net
</Typography>
{instability && (
<Tooltip
title={
<>
If too many darknet servers are backdoored, it will increase the chance that authentication <br />
attempts will return a 408 Request Timeout error (even if the password is correct). <br />
Most servers will eventually restart or go offline, which removes backdoors over time.
</>
}
>
<Typography variant={"subtitle1"} sx={{ fontStyle: "italic" }}>
{" "}
Instability: {instabilityText}
</Typography>
</Tooltip>
)}
</Box>
) : (
<Typography variant={"h6"} className={classes.gold}>
[WEBSTORM WARNING]
</Typography>
)}
<div
className={classes.NetWrapper}
ref={draggableBackground}
onPointerDown={handleDragStart}
onPointerUp={handleDragEnd}
onPointerMove={handleDrag}
onWheel={handleZoom}
>
<div
style={{
position: "relative",
width: `${DW_NET_WIDTH}px`,
height: `${DW_NET_HEIGHT}px`,
zoom: zoomOptions[zoomIndex],
cursor: "grab",
}}
id={"draggableBackgroundTarget"}
>
<canvas
ref={canvas}
width={DW_NET_WIDTH}
height={DW_NET_HEIGHT}
style={{ position: "absolute", zIndex: -1 }}
></canvas>
{darkWebRoot && <ServerStatusBox server={darkWebRoot} enableAuth={true} classes={classes} />}
{DarknetState.Network.slice(0, netDisplayDepth).map((row, i) =>
row.map(
(server, j) =>
server &&
isWithinScreen(server) && (
<ServerStatusBox server={server} key={`${i},${j}`} enableAuth={allowAuth(server)} classes={classes} />
),
),
)}
{labyrinth && netDisplayDepth > depth && (
<ServerStatusBox server={labyrinth} enableAuth={allowAuth(labyrinth)} classes={classes} />
)}
</div>
</div>
<div className={classes.zoomContainer}>
<Button className={classes.button} onClick={() => zoomIn()}>
<ZoomIn />
</Button>
<Button className={classes.button} onClick={() => zoomOut()}>
<ZoomOut />
</Button>
</div>
<Box className={`${classes.inlineFlexBox}`}>
<Typography component="div" display="flex">
<Typography display="flex" alignItems="center" paddingRight="1em">
{searchLabel}
</Typography>
<AutoCompleteSearchBox
placeholder="Search for server"
maxSuggestions={6}
suggestionList={getAutocompleteSuggestionList}
ignoredTextRegex={/ /g}
onSelection={(_, selection, options, searchValue) => {
search(selection, options, searchValue);
}}
width={300}
/>
</Typography>
<DocumentationLink
page="programming/darknet.md"
style={{ fontSize: "22px", padding: "0 20px", backgroundColor: Settings.theme.button }}
>
Darknet Docs
</DocumentationLink>
</Box>
</Container>
);
}

View File

@@ -0,0 +1,186 @@
import React, { useState, useRef } from "react";
import { Button, Container, Card, TextField, Typography } from "@mui/material";
import { getPasswordType } from "../controllers/ServerGenerator";
import { dnetStyles } from "./dnetStyles";
import type { DarknetResult } from "@nsdefs";
import { getAuthResult } from "../effects/authentication";
import { DarknetEvents } from "../models/DarknetState";
import { LabyrinthSummary } from "./LabyrinthSummary";
import { getLabyrinthDetails, isLabyrinthServer } from "../effects/labyrinth";
import { ModelIds } from "../Enums";
import { sleep } from "../../utils/Utility";
import { getSharedChars } from "../utils/darknetAuthUtils";
import type { DarknetServer } from "../../Server/DarknetServer";
import { formatObjectWithColoredKeys } from "./uiUtilities";
export type PasswordPromptProps = {
server: DarknetServer;
onClose: () => void;
};
export const PasswordPrompt = ({ server, onClose }: PasswordPromptProps): React.ReactElement => {
const [inputPassword, setInputPassword] = useState(server.hasAdminRights ? server.password : "");
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [lastDarknetResultFromAuth, setLastDarknetResultFromAuth] = useState<DarknetResult | null>(null);
const { classes } = dnetStyles({});
const passwordInput = useRef<HTMLInputElement>(null);
const isLabServer = isLabyrinthServer(server.hostname);
const canEnterLabManually = getLabyrinthDetails().manual;
const disablePasswordInput = (!canEnterLabManually && isLabServer) || server.hasAdminRights;
if (isLabServer && !canEnterLabManually) {
return (
<>
<br />
<br />
<Typography>
The weight of the deep net presses down on you. It seems this place will challenge you to make your own
tools...
</Typography>
<br />
<br />
<LabyrinthSummary isAuthenticating={isAuthenticating} />
</>
);
}
async function attemptPassword(passwordAttempted: string): Promise<void> {
setIsAuthenticating(true);
const sharedChars =
server.modelId === ModelIds.TimingAttack ? getSharedChars(server.password, passwordAttempted) : 0;
const responseTime = 500 + sharedChars * 150;
await sleep(responseTime);
// Cancel if the component unmounted while waiting
if (passwordInput.current === null) {
return;
}
// Manual password entry counts as having two threads, to increase the cha xp slightly during early exploration
const authResult = getAuthResult(server, passwordAttempted, 2, responseTime);
// Do NOT carelessly move these setters, especially when moving them to after DarknetEvents.emit().
// DarknetEvents.emit() makes the parent component rerender, so this component may be unmounted. In that case,
// these calls will set the states of an unmounted component.
setIsAuthenticating(false);
setLastDarknetResultFromAuth(authResult.result);
if (authResult.result.success) {
DarknetEvents.emit("server-unlocked", server);
} else {
// This selects the text inside the password input field so that the player can immediately start typing a new
// guess without needing to clear out the old one.
// Do NOT move this line below DarknetEvents.emit(). DarknetEvents.emit() may make this component unmounted, so
// passwordInput.current may become null unexpectedly. Using the optional chaining operator for accessing
// passwordInput.current is specifically for the case in which somebody mistakenly moves this line.
passwordInput.current?.querySelector("input")?.select();
DarknetEvents.emit();
}
}
const handleSubmit = (e: React.FormEvent): void => {
e.preventDefault();
if (server.hasAdminRights) {
onClose();
return;
}
if (!isAuthenticating) {
attemptPassword(inputPassword).catch((error) => console.error(error));
}
};
let authFeedback;
if (isAuthenticating) {
authFeedback = "Checking password...";
} else {
if (lastDarknetResultFromAuth === null) {
authFeedback = "(no response yet)";
} else {
authFeedback = formatObjectWithColoredKeys(lastDarknetResultFromAuth, ["success", "message", "code"]);
}
}
return (
<>
<div className={classes.inlineFlexBox}>
<div>
<form onSubmit={(e) => handleSubmit(e)}>
<TextField
ref={passwordInput}
label="Password"
type="text"
value={inputPassword}
onChange={(e) => setInputPassword(e.target.value)}
variant="outlined"
autoComplete="off"
autoFocus={!server.hasAdminRights}
disabled={disablePasswordInput}
/>
</form>
<br />
<Button onClick={(e) => handleSubmit(e)} disabled={isAuthenticating}>
Submit Password
</Button>
<br />
<br />
<br />
{!isLabServer && (
<Typography variant="caption" color="secondary">
Logs scraped via <pre style={{ display: "inline" }}>heartbleed</pre>:
</Typography>
)}
</div>
<div style={{ width: "50%" }}>
<Container disableGutters>
{isLabServer ? (
<div style={{ color: "white" }}>
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
<span className={classes.serverDetailsText}>Hint:</span> {server.staticPasswordHint}
<br />
<span className={classes.serverDetailsText}>Model:</span> {server.modelId}
<br />
<span className={classes.serverDetailsText}>Required charisma:</span> {server.requiredCharismaSkill}
<br />
</pre>
</div>
) : (
<div style={{ color: "white" }}>
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
<span className={classes.serverDetailsText}>Hint:</span> {server.staticPasswordHint}
<br />
{server.passwordHintData && (
<>
<span className={classes.serverDetailsText}>Data: </span> {server.passwordHintData}
<br />
</>
)}
<span className={classes.serverDetailsText}>Length:</span> {server.password.length}
<br />
<span className={classes.serverDetailsText}>Format:</span> {getPasswordType(server.password)}
<br />
<span className={classes.serverDetailsText}>Model:</span> {server.modelId}
<br />
</pre>
</div>
)}
</Container>
<br />
{!isLabServer && (
<Card style={{ padding: "8px", minHeight: "60px", marginBottom: "8px" }}>
<div style={{ color: "white" }}>
{typeof authFeedback !== "string" ? (
authFeedback
) : (
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>{authFeedback}</pre>
)}
</div>
</Card>
)}
</div>
</div>
<br />
{isLabServer && <LabyrinthSummary isAuthenticating={isAuthenticating} />}
</>
);
};

View File

@@ -0,0 +1,115 @@
import React from "react";
import { Modal } from "../../ui/React/Modal";
import { Container, Card, SvgIcon, Typography, Tooltip } from "@mui/material";
import { getIcon } from "./ServerIcon";
import { getServerState } from "../models/DarknetState";
import { ServerSummary } from "./ServerSummary";
import { populateServerLogsWithNoise } from "../models/packetSniffing";
import { isLabyrinthServer } from "../effects/labyrinth";
import { PasswordPrompt } from "./PasswordPrompt";
import { copyToClipboard, formatObjectWithColoredKeys, formatToMaxDigits } from "./uiUtilities";
import { useCycleRerender } from "../../ui/React/hooks";
import type { DarknetServer } from "../../Server/DarknetServer";
import { logBoxBaseZIndex } from "../../ui/React/Constants";
export type DWPasswordPromptModalProps = {
open: boolean;
onClose: () => void;
server: DarknetServer;
classes: {
[key: string]: string;
};
};
export const ServerDetailsModal = ({
open,
onClose,
server,
classes,
}: DWPasswordPromptModalProps): React.ReactElement => {
useCycleRerender();
const icon = getIcon(server.modelId);
populateServerLogsWithNoise(server);
const serverState = getServerState(server.hostname);
const isLabServer = isLabyrinthServer(server.hostname);
const recentLogs = serverState.serverLogs.slice(0, 5);
const ramBlock = server.blockedRam;
const blockedRamString = ramBlock ? formatToMaxDigits(ramBlock, 1) + "+" : "";
const usedRamString = formatToMaxDigits(server.ramUsed - ramBlock, 1);
const serverRamString = `RAM in use: ${blockedRamString}${usedRamString}/${server.maxRam} GB`;
const logContent = recentLogs.map((log, index) => (
<pre
key={index}
color="secondary"
style={{ borderLeft: "1px solid grey", paddingLeft: "3px", whiteSpace: "pre-wrap" }}
>
{typeof log.message === "string"
? log.message
: formatObjectWithColoredKeys(log.message, [
"message",
"data",
"passwordAttempted",
"passwordExpected",
"code",
])}
</pre>
));
const copyHostname = () => copyToClipboard(server.hostname);
return (
<Modal open={open} onClose={onClose} removeFocus={false} sx={{ zIndex: logBoxBaseZIndex - 1 }}>
<>
<Container sx={{ width: "calc(min(900px, 80vw))", minHeight: "500px" }}>
<div className={classes.inlineFlexBox}>
<Typography variant="h5" color={server.hasAdminRights ? "primary" : "secondary"} onClick={copyHostname}>
{server.hostname}
</Typography>
<Tooltip title={`Server Model: ${server.modelId}`}>
<SvgIcon component={icon} color="secondary" />
</Tooltip>
</div>
<br />
{server.hasAdminRights ? (
<>
<Typography>Password: "{server.password}"</Typography>
<br />
<Typography color="secondary">IP: {server.ip}</Typography>
<Typography color="secondary">Required charisma: {server.requiredCharismaSkill}</Typography>
<Tooltip
title={`Ram blocked by server owner: ${ramBlock} GB. Ram in use by scripts: ${
server.ramUsed - ramBlock
} GB.`}
>
<Typography color="secondary">{serverRamString}</Typography>
</Tooltip>
<Typography color="secondary">Model: {server.modelId}</Typography>
<br />
<div style={{ maxWidth: "300px" }}>
<ServerSummary server={server} enableAuth={true} showDetails={true} classes={classes} />
</div>
<br />
{isLabServer && (
<>
<br />
<Typography>You have successfully navigated the labyrinth! Congratulations!</Typography>
</>
)}
</>
) : (
<PasswordPrompt server={server} onClose={onClose} />
)}
{!isLabServer && (
<>
<Card style={{ height: "250px", overflowY: "scroll" }}>
<div style={{ color: "white", paddingLeft: "10px" }}>{logContent}</div>
</Card>
</>
)}
</Container>
</>
</Modal>
);
};

View File

@@ -0,0 +1,86 @@
import {
ConnectedTv,
LaptopMac,
DesktopMac,
Dns,
PhoneIphone,
Terminal,
SatelliteAlt,
Dvr,
Microwave,
ElectricCar,
Blender,
LiveTv,
Subtitles,
Web,
ExitToApp,
SignalWifiStatusbarConnectedNoInternet4,
Calculate,
Watch,
NoCell,
SettingsPower,
VideogameAsset,
AccountBalance,
Elevator,
Fax,
AssuredWorkload,
SvgIconComponent,
} from "@mui/icons-material";
import { ModelIds } from "../Enums";
export const getIcon = (model: string): SvgIconComponent => {
switch (model) {
case ModelIds.EchoVuln:
return ConnectedTv;
case ModelIds.SortedEchoVuln:
return LaptopMac;
case ModelIds.NoPassword:
return PhoneIphone;
case ModelIds.Captcha:
return Dns;
case ModelIds.DefaultPassword:
return LiveTv;
case ModelIds.BufferOverflow:
return Terminal;
case ModelIds.MastermindHint:
return SatelliteAlt;
case ModelIds.TimingAttack:
return Fax;
case ModelIds.LargestPrimeFactor:
return Calculate;
case ModelIds.RomanNumeral:
return Watch;
case ModelIds.DogNames:
return DesktopMac;
case ModelIds.GuessNumber:
return Dvr;
case ModelIds.CommonPasswordDictionary:
return Subtitles;
case ModelIds.EUCountryDictionary:
return Web;
case ModelIds.Yesn_t:
return NoCell;
case ModelIds.BinaryEncodedFeedback:
return SettingsPower;
case ModelIds.SpiceLevel:
return Microwave;
case ModelIds.ConvertToBase10:
return VideogameAsset;
case ModelIds.parsedExpression:
return AccountBalance;
case ModelIds.divisibilityTest:
return ElectricCar;
case ModelIds.tripleModulo:
return Blender;
case ModelIds.globalMaxima:
return Elevator;
case ModelIds.packetSniffer:
return SignalWifiStatusbarConnectedNoInternet4;
case ModelIds.encryptedPassword:
return AssuredWorkload;
case ModelIds.labyrinth:
return ExitToApp;
default:
return ConnectedTv;
}
};

View File

@@ -0,0 +1,71 @@
import React, { useState } from "react";
import { Typography, SvgIcon, Tooltip } from "@mui/material";
import { ServerDetailsModal } from "./ServerDetailsModal";
import { getIcon } from "./ServerIcon";
import { DarknetState } from "../models/DarknetState";
import { getPixelPosition } from "./networkCanvas";
import { ServerSummary } from "./ServerSummary";
import type { DarknetServer } from "../../Server/DarknetServer";
import { DWServerStyles, ServerName } from "./dnetStyles";
export type DWServerProps = {
server: DarknetServer;
enableAuth: boolean;
classes: {
[key: string]: string;
};
};
export function ServerStatusBox({ server, enableAuth, classes }: DWServerProps): React.ReactElement {
const [open, setOpen] = useState(false);
const icon = getIcon(server.modelId);
const authButtonHandler = () => {
DarknetState.openServer = server;
setOpen(true);
};
const handleClose = () => {
DarknetState.openServer = null;
setOpen(false);
};
const getServerStyles = (server: DarknetServer) => {
const position = getPixelPosition(server);
return {
...DWServerStyles,
top: `${position.top}px`,
left: `${position.left}px`,
borderColor: server.hasStasisLink ? "gold" : server.hasAdminRights ? "green" : "grey",
};
};
return (
<>
{open ? <ServerDetailsModal open={open} onClose={handleClose} server={server} classes={classes} /> : ""}
<button
style={{ ...getServerStyles(server), position: "absolute", userSelect: "none" }}
className={classes.DWServer}
onClick={authButtonHandler}
disabled={!enableAuth}
>
<div style={{ padding: 0, margin: 0, width: "100%" }}>
<div style={{ display: "inline-flex", flexDirection: "row", width: "100%", justifyContent: "space-between" }}>
<Tooltip title={`Server Model: ${server.modelId}`}>
<SvgIcon component={icon} color="secondary" />
</Tooltip>
<Typography color={server.hasAdminRights ? "primary" : "secondary"} sx={ServerName}>
{server.hostname}
</Typography>
</div>
<Typography color="secondary" style={{ fontSize: "0.9em" }}>
{server.ip} cha:{server.requiredCharismaSkill}
</Typography>
<br />
<ServerSummary server={server} enableAuth={enableAuth} classes={classes} />
</div>
</button>
</>
);
}

View File

@@ -0,0 +1,151 @@
import React from "react";
import { SvgIcon, Tooltip, Typography } from "@mui/material";
import { Code, Description, Inventory2, LockPerson, Terminal, Bolt, DoorBackSharp } from "@mui/icons-material";
import { formatNumber } from "../../ui/formatNumber";
import { CompletedProgramName } from "@enums";
import { formatToMaxDigits } from "./uiUtilities";
import type { DarknetServer } from "../../Server/DarknetServer";
import { DarknetConstants } from "../Constants";
export type ServerSummaryProps = {
server: DarknetServer;
enableAuth: boolean;
showDetails?: boolean;
classes: {
[key: string]: string;
};
};
export function ServerSummary({
server,
enableAuth,
classes,
showDetails = false,
}: ServerSummaryProps): React.ReactElement {
if (!server.hasAdminRights && enableAuth) {
return <Typography>[ auth required ]</Typography>;
}
if (!server.hasAdminRights && !enableAuth) {
return <Typography color="secondary">(no connection)</Typography>;
}
const cacheCount = server.caches.length;
const dataFiles = Array.from(server.textFiles.keys()).filter((f) => f.endsWith(DarknetConstants.DataFileSuffix));
const textFiles = [...dataFiles, ...server.messages];
const fileCount = textFiles.length;
const textFilesTooltip =
textFiles.length > 0
? `Data files on server: ${textFiles.slice(0, 3).join(", ")}${
textFiles.length > 3 ? ` +${textFiles.length - 3}` : ""
}`
: "No data files on server";
const contractCount = server.contracts.length;
const runningScriptNames = Array.from(server.runningScriptMap.keys()).map((script) => script.replace("*[]", ""));
const runningScriptsTooltip =
runningScriptNames.length > 0
? `Running scripts on server: ${runningScriptNames.slice(0, 3).join(", ")}${
runningScriptNames.length > 3 ? ` +${runningScriptNames.length - 3}` : ""
}`
: "No running scripts on server";
const hasStormSeed = server.programs.includes(CompletedProgramName.stormSeed);
const hasBackdoor = server.backdoorInstalled && !server.hasStasisLink;
const ramBlockedDetails = formatToMaxDigits(server.blockedRam, 2) + "GB";
const ramBlocked = showDetails ? ramBlockedDetails : formatNumber(server.blockedRam, 0);
const runningScriptsComponent = (
<Tooltip key="runningScript" title={<>{runningScriptsTooltip}</>}>
<Typography color={runningScriptNames.length > 0 ? "primary" : "secondary"}>
<SvgIcon component={Terminal} className={classes.serverStatusIcon} />
{runningScriptNames.length}
</Typography>
</Tooltip>
);
const components = [];
if (cacheCount) {
components.push(
<Tooltip key="cache" title={<>Reward cache count: {cacheCount}</>}>
<Typography>
<SvgIcon component={Inventory2} className={`${classes.gold} ${classes.serverStatusIcon}`} />
{cacheCount}
</Typography>
</Tooltip>,
);
}
if (hasStormSeed) {
components.push(
<Tooltip key="stormSeed" title={<>A mysterious executable has been found here...</>}>
<Typography>
<SvgIcon component={Bolt} className={`${classes.gold} ${classes.serverStatusIcon}`} />?
</Typography>
</Tooltip>,
);
}
if (hasBackdoor) {
components.push(
<Tooltip key="backdoor" title={<>Backdoor installed. Warning: this increases darknet instability.</>}>
<Typography>
<SvgIcon component={DoorBackSharp} className={`${classes.red} ${classes.serverStatusIcon}`} />
</Typography>
</Tooltip>,
);
}
if (server.hasStasisLink) {
components.push(
<Tooltip
key="backdoor"
title={
<>
Stasis link installed. This allows connecting to the server remotely, as well as ns.exec from any distance.
</>
}
>
<Typography>
<SvgIcon component={DoorBackSharp} className={`${classes.gold} ${classes.serverStatusIcon}`} />
</Typography>
</Tooltip>,
);
}
if (contractCount) {
components.push(
<Tooltip key="contract" title={<>Coding contract count: {contractCount}</>}>
<Typography>
<SvgIcon component={Code} className={classes.serverStatusIcon} />
{contractCount}
</Typography>
</Tooltip>,
);
}
if (fileCount) {
components.push(
<Tooltip key="file" title={<>{textFilesTooltip}</>}>
<Typography color={fileCount ? "primary" : "secondary"}>
<SvgIcon component={Description} className={classes.serverStatusIcon} />
{fileCount}
</Typography>
</Tooltip>,
);
}
if (server.blockedRam) {
components.push(
<Tooltip
key="ramBlocked"
title={<>Ram blocked by owner: {ramBlockedDetails}. This can be freed up using ns.dnet.memoryReallocation()</>}
>
<Typography color={"secondary"}>
<SvgIcon component={LockPerson} className={classes.serverStatusIcon} />
{ramBlocked}
</Typography>
</Tooltip>,
);
}
const maxIcons = showDetails ? components.length : 2;
const componentsToShow = [...components.slice(0, maxIcons), runningScriptsComponent];
return (
<div style={{ display: "inline-flex", flexDirection: "row", width: "100%", justifyContent: "space-between" }}>
{componentsToShow}
</div>
);
}

View File

@@ -0,0 +1,161 @@
import { Theme } from "@mui/material/styles";
import { makeStyles } from "tss-react/mui";
export const dwColors = ["hack", "hp", "money", "int", "cha", "rep", "success"] as const;
export type dwColors = (typeof dwColors)[number];
export const DW_SERVER_WIDTH = 240;
export const DW_SERVER_HEIGHT = 130;
export const DW_SERVER_GAP_TOP = 120;
export const DW_SERVER_GAP_LEFT = 60;
export const MAP_BORDER_WIDTH = 300;
export const dnetStyles = makeStyles<unknown, dwColors>({ uniqId: "dnetStyles" })((theme: Theme, __, __classes) => ({
DWServer: {
"&:hover": {
backgroundColor: "#333 !important",
},
},
NetWrapper: {
width: "100%",
height: "calc(100vh - 80px)",
overflow: "scroll",
position: "relative",
border: "solid 1px slategray",
},
button: {
color: theme.colors.white,
},
maze: {
color: theme.colors.white,
lineHeight: 0.55,
},
hiddenInput: {
width: 0,
height: 0,
padding: 0,
margin: 0,
opacity: 0,
},
zoomContainer: {
position: "absolute",
top: "calc(90vh - 38px)",
marginLeft: "1px",
display: "grid",
zIndex: 20,
["& > button"]: {
width: "40px",
minWidth: "40px !important",
},
},
inlineFlexBox: {
display: "inline-flex",
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
},
noPadding: {
padding: 0,
},
paddingRight: {
paddingRight: "3px",
},
serverStatusIcon: {
paddingRight: "3px",
position: "relative",
bottom: "-4px",
},
gold: {
color: theme.colors.money,
},
red: {
color: theme.colors.hp,
},
white: {
color: theme.colors.white,
},
authButton: {
["&:disabled"]: {
opacity: 0.5,
},
},
hack: {
borderColor: theme.colors.hack,
},
hp: {
borderColor: theme.colors.hp,
},
money: {
borderColor: theme.colors.money,
},
int: {
borderColor: theme.colors.int,
},
cha: {
borderColor: theme.colors.cha,
},
rep: {
borderColor: theme.colors.rep,
},
success: {
borderColor: theme.colors.success,
},
green: {
borderColor: "green",
},
grey: {
borderColor: "grey",
},
goldBorder: {
borderColor: "gold",
},
serverDetailsText: {
marginLeft: "-2em",
textIndent: "2em",
color: "grey",
},
}));
/*
React by default creates a new <style> element with duplicate css for each copy of each component that uses makeStyles.
To reduce the performance impact of that option, these styles are defined as an object literal and applied directly to
the element's style attribute. Also included is the relevant styles for Mui Button, for the same reason.
This is done instead of adding hundreds of <style> tags into the DOM, which in some cases took multiple seconds
waiting for insertBefore calls and reflowing the page when loading the darknet UI view.
*/
export const DWServerStyles = {
width: `${DW_SERVER_WIDTH}px`,
height: `${DW_SERVER_HEIGHT}px`,
borderWidth: "1px",
borderStyle: "solid",
padding: "8px",
borderRadius: "4px",
zIndex: 10,
cursor: "auto",
backgroundColor: "#000",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
outline: 0,
margin: 0,
verticalAlign: "middle",
textDecoration: "none",
fontFamily: 'JetBrainsMono, "Courier New", monospace',
fontWeight: 500,
fontSize: "0.875rem",
lineHeight: 1.75,
transition:
"background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms",
color: "#0c0",
};
export const DWServerLogStyles = { fontFamily: 'JetBrainsMono, "Courier New", monospace', fontSize: "12px" };
export const ServerName = {
padding: 0,
width: "86%",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
};

View File

@@ -0,0 +1,90 @@
import { DarknetState } from "../models/DarknetState";
import {
DW_SERVER_GAP_LEFT,
DW_SERVER_GAP_TOP,
DW_SERVER_HEIGHT,
DW_SERVER_WIDTH,
MAP_BORDER_WIDTH,
} from "./dnetStyles";
import { SpecialServers } from "../../Server/data/SpecialServers";
import { getNetDepth, isLabyrinthServer } from "../effects/labyrinth";
import { NET_WIDTH } from "../Enums";
import type { DarknetServer } from "../../Server/DarknetServer";
import { getDarknetServerOrThrow } from "../utils/darknetServerUtils";
export const drawOnCanvas = (canvas: HTMLCanvasElement) => {
const ctx = canvas?.getContext("2d");
if (!ctx || !canvas) {
console.error("Could not get canvas context");
return;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const server of DarknetState.Network.flat()) {
if (
!server ||
(!server.hasAdminRights && !server.serversOnNetwork.find((s) => getDarknetServerOrThrow(s).hasAdminRights))
) {
continue;
}
// draw a line between each server and its connected servers
for (const connectedServerName of server.serversOnNetwork) {
const connectedServer = getDarknetServerOrThrow(connectedServerName);
if (
!connectedServer.hasAdminRights &&
!connectedServer.serversOnNetwork.find((s) => getDarknetServerOrThrow(s).hasAdminRights)
) {
continue;
}
ctx.beginPath();
const connectedColor = "green";
const disconnectedColor = "grey";
ctx.strokeStyle = server.hasAdminRights || connectedServer.hasAdminRights ? connectedColor : disconnectedColor;
const startPosition = getPixelPosition(server, true);
const endPosition = getPixelPosition(connectedServer, true);
ctx.moveTo(startPosition.left, startPosition.top);
ctx.lineTo(endPosition.left, endPosition.top);
ctx.stroke();
}
}
};
export const getPixelPosition = (server: DarknetServer, centered = false) => {
const centeredOffsetHorizontal = centered ? DW_SERVER_WIDTH / 2 : 0;
const centeredOffsetVertical = centered ? DW_SERVER_HEIGHT / 2 : 0;
if (server.hostname === SpecialServers.DarkWeb) {
return {
top: MAP_BORDER_WIDTH * 0.2 + (centered ? centeredOffsetVertical : 0),
left: (DW_SERVER_GAP_LEFT + DW_SERVER_WIDTH) * NET_WIDTH * 0.5 + (centered ? centeredOffsetHorizontal : 0),
};
} else if (isLabyrinthServer(server.hostname)) {
return {
top:
MAP_BORDER_WIDTH +
centeredOffsetVertical +
(DW_SERVER_GAP_TOP + DW_SERVER_HEIGHT) * getNetDepth() +
DW_SERVER_GAP_TOP,
left: (DW_SERVER_GAP_LEFT + DW_SERVER_WIDTH) * NET_WIDTH * 0.5 + (centered ? centeredOffsetHorizontal : 0),
};
}
const coords = getCoordinates(server);
const widthOfServers = (DW_SERVER_GAP_LEFT + DW_SERVER_WIDTH) * coords.y;
const staggeredHorizontalOffset = coords.x % 2 ? DW_SERVER_WIDTH / 2 : 0;
const heightOfServers = (DW_SERVER_GAP_TOP + DW_SERVER_HEIGHT) * coords.x;
return {
top: heightOfServers + MAP_BORDER_WIDTH + centeredOffsetVertical,
left: widthOfServers + MAP_BORDER_WIDTH + centeredOffsetHorizontal + staggeredHorizontalOffset,
};
};
const getCoordinates = (server: DarknetServer) => {
return {
x: server.depth,
y: server.leftOffset,
};
};

View File

@@ -0,0 +1,41 @@
import React from "react";
import { SnackbarEvents } from "../../ui/React/Snackbar";
import { ToastVariant } from "@enums";
import { DWServerLogStyles } from "./dnetStyles";
export const formatToMaxDigits = (value: number, maxDigits: number): string => {
if (value === 0) return "0";
return parseFloat(value.toFixed(maxDigits)).toString();
};
export const copyToClipboard = (text: string): void => {
navigator.clipboard.writeText(text).catch((error) => console.error(error));
SnackbarEvents.emit(`Copied "${text}" to clipboard`, ToastVariant.SUCCESS, 2000);
};
export const formatObjectWithColoredKeys = (obj: Record<string, unknown>, filteredKeys?: string[]) => {
const filteredObject: Record<string, unknown> = {};
if (filteredKeys) {
for (const key of filteredKeys) {
if (key in obj) {
filteredObject[key] = obj[key];
}
}
} else {
Object.assign(filteredObject, obj);
}
return (
<span style={DWServerLogStyles}>
{Object.entries(filteredObject).map(([key, value]) => {
return (
<React.Fragment key={key}>
<span style={{ color: "grey" }}>{key}: </span>
{/* React does not render null, undefined, and boolean values */}
{typeof value !== "boolean" ? value : String(value)}
<br />
</React.Fragment>
);
})}
</span>
);
};

View File

@@ -0,0 +1,64 @@
import { canAccessBitNodeFeature } from "../../BitNode/BitNodeUtils";
import { Player } from "@player";
import { CompletedProgramName } from "@enums";
import { GenericResponseMessage, ResponseCodeEnum } from "../Enums";
export const hasDarknetAccess = () => {
return canAccessBitNodeFeature(15) || Player.hasProgram(CompletedProgramName.darkscape);
};
export const getTwoCharsInPassword = (password: string) => {
const index1 = Math.floor(Math.random() * password.length);
const containedChar1 = password[index1];
let index2 = Math.floor(Math.random() * password.length);
if (index2 === index1) {
index2 = (index2 + 1) % password.length;
}
const containedChar2 = password[index2];
return [containedChar1, containedChar2];
};
export const getExactCorrectChars = (password: string, attemptedPassword: string) =>
password.split("").map((digit, i: number) => digit === attemptedPassword[i]);
export const getExactCorrectCharsCount = (password: string, attemptedPassword: string) =>
getExactCorrectChars(password, attemptedPassword).filter((isCorrect) => isCorrect).length;
export const getSharedChars = (password: string, attemptedPassword: string): number => {
for (let i = 0; i < password.length; i++) {
if (password[i] !== attemptedPassword[i]) {
return i;
}
}
return password.length;
};
export const getMisplacedCorrectCharsCount = (password: string, attemptedPassword: string) => {
// filter out exact correct chars from both the attempted and correct password, to simplify checking for duplicate counts
const remainingPasswordChars = password.split("").filter((digit, i) => digit !== attemptedPassword[i]);
const remainingAttemptedPasswordChars = attemptedPassword.split("").filter((digit, i) => digit !== password[i]);
const misplacedCorrectChars = remainingAttemptedPasswordChars.filter((digit, i) => {
const isPresentInPassword = remainingPasswordChars.includes(digit);
const countInAttemptedPasswordThusFar = remainingAttemptedPasswordChars
.slice(0, i)
.filter((prevDigit) => prevDigit === digit).length;
const countInPassword = remainingPasswordChars.filter((prevDigit) => prevDigit === digit).length;
return isPresentInPassword && countInAttemptedPasswordThusFar < countInPassword;
});
return misplacedCorrectChars.length;
};
export const getGenericSuccess = (attemptedPassword: string) => ({
code: ResponseCodeEnum.Success,
message: GenericResponseMessage.Success,
passwordAttempted: attemptedPassword,
});
export const getFailureResponse = (attemptedPassword: string, message: string, data: string) => ({
code: ResponseCodeEnum.AuthFailure,
message,
data,
passwordAttempted: attemptedPassword,
});

View File

@@ -0,0 +1,103 @@
import { DarknetState } from "../models/DarknetState";
import { DarknetServer } from "../../Server/DarknetServer";
import { AIR_GAP_DEPTH, MS_PER_MUTATION_PER_ROW, NET_WIDTH } from "../Enums";
import { GetAllServers } from "../../Server/AllServers";
import { getNetDepth } from "../effects/labyrinth";
import { CONSTANTS } from "../../Constants";
export const getDarknetCyclesPerMutation = () => {
const depth = getNetDepth();
const cycleRate = MS_PER_MUTATION_PER_ROW / CONSTANTS.MilliPerCycle;
return cycleRate / depth;
};
export const getAllOpenPositions = (minDepth: number, maxDepth: number): [number, number][] => {
const min = Math.max(0, minDepth);
const max = Math.min(maxDepth, getNetDepth() - 1);
const positions: [number, number][] = [];
for (let x = min; x <= max; x++) {
for (let y = 0; y < NET_WIDTH; y++) {
if (DarknetState.Network[x] && !DarknetState.Network[x][y] && !isOnAirGap(x)) {
positions.push([x, y]);
}
}
}
if (min === 0 && max === getNetDepth() - 1) {
return positions;
}
if (positions.length === 0) {
return getAllOpenPositions(min - 1, max + 1);
}
return positions;
};
export const getNeighborsOnRow = (x: number, y: number): DarknetServer[] => {
const neighbors: DarknetServer[] = [];
const leftNeighbor = DarknetState.Network[x]?.[y - 1];
const rightNeighbor = DarknetState.Network[x]?.[y + 1];
if (leftNeighbor) {
neighbors.push(leftNeighbor);
}
if (rightNeighbor) {
neighbors.push(rightNeighbor);
}
return neighbors;
};
export const getServersOnRowBelow = (x: number, close = false): DarknetServer[] => {
const rowBelow = DarknetState.Network[x - 1]?.filter(notNull<DarknetServer>) ?? [];
if (close) {
return rowBelow.filter((server) => Math.abs(server.leftOffset ?? 0 - x) <= 1);
}
return rowBelow;
};
export const getServersOnRowAbove = (x: number, close = false): DarknetServer[] => {
const rowAbove = DarknetState.Network[x + 1]?.filter(notNull<DarknetServer>) ?? [];
if (close) {
return rowAbove.filter((server) => Math.abs(server.leftOffset ?? 0 - x) <= 1);
}
return rowAbove;
};
export const getAllDarknetServers = (): DarknetServer[] => {
return GetAllServers(true).filter((server) => server instanceof DarknetServer);
};
export const getAllMovableDarknetServers = (): DarknetServer[] => {
const movableDarknetServers = [];
for (const server of GetAllServers(true)) {
if (!(server instanceof DarknetServer) || server.isStationary || server.hasStasisLink) {
continue;
}
movableDarknetServers.push(server);
}
return movableDarknetServers;
};
export const getAllAdjacentNeighbors = (x: number, y: number): DarknetServer[] => {
const rowAbove = getServersOnRowAbove(x, true);
const rowBelow = getServersOnRowBelow(x, true);
const neighborsOnRow = getNeighborsOnRow(x, y);
return [...rowAbove, ...rowBelow, ...neighborsOnRow];
};
export const getIslands = () => getAllMovableDarknetServers().filter((s) => !s.serversOnNetwork.length);
export const getBackdooredDarkwebServers = (): DarknetServer[] =>
getAllMovableDarknetServers().filter((s) => !s.hasStasisLink && s.backdoorInstalled);
export const getNearbyNonEmptyPasswordServer = (server: DarknetServer, disconnected = false) => {
return getAllAdjacentNeighbors(server.depth, server.leftOffset).find(
(neighbor) =>
!neighbor.hasAdminRights &&
neighbor.password &&
(!disconnected || !server.serversOnNetwork.includes(neighbor.hostname)),
);
};
export const getStasisLinkServers = () => getAllDarknetServers().filter((s) => s.hasStasisLink);
const isOnAirGap = (x: number): boolean => !!x && !(x % AIR_GAP_DEPTH);
const notNull = <T>(value: T | null): value is T => value !== null;

View File

@@ -0,0 +1,21 @@
import { DarknetServer } from "../../Server/DarknetServer";
import { GetServer } from "../../Server/AllServers";
export const getDarknetServer = (host: string): DarknetServer | null => {
const server = GetServer(host);
if (!server || !(server instanceof DarknetServer)) {
return null;
}
return server;
};
export function getDarknetServerOrThrow(host: string): DarknetServer {
const server = GetServer(host);
if (!server) {
throw new Error(`Server ${host} does not exist.`);
}
if (!(server instanceof DarknetServer)) {
throw new Error(`Server ${host} is not a darknet server.`);
}
return server;
}

View File

@@ -7,6 +7,8 @@ import { SpecialServers } from "../Server/data/SpecialServers";
import { Money } from "../ui/React/Money";
import { DarkWebItem } from "./DarkWebItem";
import { isCreateProgramWork } from "../Work/CreateProgramWork";
import { CompletedProgramName } from "@enums";
import { getDarkscapeNavigator } from "../DarkNet/effects/effects";
//Posts a "help" message if connected to DarkWeb
export function checkIfConnectedToDarkweb(): void {
@@ -82,6 +84,10 @@ export function buyDarkwebItem(itemName: string): void {
Terminal.print(
"You have purchased the " + item.program + " program. The new program can be found on your home computer.",
);
if (item.program === CompletedProgramName.darkscape) {
getDarkscapeNavigator();
}
}
export function buyAllDarkwebItems(): void {

View File

@@ -1,3 +1,4 @@
import { DarknetConstants } from "../DarkNet/Constants";
import { DarkWebItem } from "./DarkWebItem";
import { CompletedProgramName } from "@enums";
@@ -11,5 +12,10 @@ export const DarkWebItems = {
DeepscanV1: new DarkWebItem(CompletedProgramName.deepScan1, 500000, "Enables 'scan-analyze' with a depth up to 5."),
DeepscanV2: new DarkWebItem(CompletedProgramName.deepScan2, 25e6, "Enables 'scan-analyze' with a depth up to 10."),
AutolinkProgram: new DarkWebItem(CompletedProgramName.autoLink, 1e6, "Enables direct connect via 'scan-analyze'."),
DarkScapeProgram: new DarkWebItem(
CompletedProgramName.darkscape,
DarknetConstants.DarkscapeNavigatorPrice,
"Unlock access to the Dark Net.",
),
FormulasProgram: new DarkWebItem(CompletedProgramName.formulas, 5e9, "Unlock access to the formulas API."),
};

View File

@@ -27,6 +27,7 @@ import { EntropyDev } from "./DevMenu/ui/EntropyDev";
import { Exploit } from "./Exploits/Exploit";
import { useRerender } from "./ui/React/hooks";
import { DarknetDev } from "./DevMenu/ui/DarknetDev";
import { AutoExpandContext, getAutoExpandData, setAutoExpandData } from "./ui/AutoExpand/AutoExpandContext";
import { canAccessStockMarket } from "./StockMarket/StockMarket";
@@ -74,6 +75,7 @@ export function DevMenuRoot(): React.ReactElement {
<TimeSkipDev />
<AchievementsDev />
<EntropyDev />
<DarknetDev />
</AutoExpandContext.Provider>
);
}

View File

@@ -0,0 +1,303 @@
import React from "react";
import {
AccordionSummary,
AccordionDetails,
Select,
Typography,
Tooltip,
TextField,
MenuItem,
Button,
type SelectChangeEvent,
} from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { clearDarknet, populateDarknet } from "../../DarkNet/controllers/NetworkGenerator";
import { DarknetEvents, DarknetState } from "../../DarkNet/models/DarknetState";
import { launchWebstorm } from "../../DarkNet/effects/webstorm";
import { OptionSwitch } from "../../ui/React/OptionSwitch";
import { Router } from "../../ui/GameRoot";
import { CompletedProgramName, SimplePage, ToastVariant } from "@enums";
import { getDarkscapeNavigator, handleSuccessfulAuth } from "../../DarkNet/effects/effects";
import { isLabyrinthServer } from "../../DarkNet/effects/labyrinth";
import { SnackbarEvents } from "../../ui/React/Snackbar";
import { AutoExpandAccordion } from "../../ui/AutoExpand/AutoExpandAccordion";
import { getAllDarknetServers, getAllMovableDarknetServers } from "../../DarkNet/utils/darknetNetworkUtils";
import { moveDarknetServer, moveRandomDarknetServers } from "../../DarkNet/controllers/NetworkMovement";
import { Modal } from "../../ui/React/Modal";
import { ModelIds } from "../../DarkNet/Enums";
import {
createDarknetServer,
getBinaryEncodedConfig,
getBufferOverflowConfig,
getCaptchaConfig,
getConvertToBase10Config,
getDefaultPasswordConfig,
getDivisibilityTestConfig,
getDogNameConfig,
getEchoVulnConfig,
getEuCountryDictionaryConfig,
getGuessNumberConfig,
getKingOfTheHillConfig,
getLargeDictionaryConfig,
getLargestPrimeFactorConfig,
getMastermindHintConfig,
getNoPasswordConfig,
getPacketSnifferConfig,
getParseArithmeticExpressionConfig,
getRomanNumeralConfig,
getSortedEchoVulnConfig,
getSpiceLevelConfig,
getTimingAttackConfig,
getTripleModuloConfig,
getYesn_tConfig,
ServerConfig,
serverFactory,
} from "../../DarkNet/controllers/ServerGenerator";
export function DarknetDev(): React.ReactElement {
const [open, setOpen] = React.useState(false);
const [difficulty, setDifficulty] = React.useState(1);
const [depth, setDepth] = React.useState(1);
const [count, setCount] = React.useState(1);
const [selectedServerType, setSelectedServerType] = React.useState<ServerTypeOption>(serverTypeOptions[0]);
const cancel = (): void => {
setOpen(false);
};
const maxDepth = Math.max(...getAllDarknetServers().map((s) => s.depth));
const toggleShowFullNetwork = (newValue: boolean): void => {
DarknetState.showFullNetwork = newValue;
DarknetEvents.emit();
};
const changeSelectedServerType = (event: SelectChangeEvent<string>): void => {
const option = serverTypeOptions.find((opt) => opt.label === event.target.value);
if (!option) return;
setSelectedServerType(option);
};
const updateDifficulty = (event: React.ChangeEvent<HTMLInputElement>): void => {
setDifficulty(Number(event.target.value));
};
const updateDepth = (event: React.ChangeEvent<HTMLInputElement>): void => {
setDepth(Math.max(1, Math.min(maxDepth, Number(event.target.value))));
};
const updateCount = (event: React.ChangeEvent<HTMLInputElement>): void => {
setCount(Number(event.target.value));
};
const createServer = (): void => {
const range = Math.max(3, Math.ceil(count / 4));
let successCount = 0;
for (let i = 0; i < count; i++) {
const newServer =
selectedServerType.label === "Random"
? createDarknetServer(difficulty, depth, -1)
: serverFactory(selectedServerType.constructor, difficulty, depth, -1);
successCount += +moveDarknetServer(newServer, range, range, depth);
}
if (successCount !== count) {
SnackbarEvents.emit(
`Only created ${successCount} ${selectedServerType.label} darknet servers at depth ${depth}. Not enough open positions available`,
ToastVariant.ERROR,
4000,
);
} else {
SnackbarEvents.emit(
`Created ${count} new ${selectedServerType.label} darknet servers at depth ${depth}`,
ToastVariant.SUCCESS,
2000,
);
}
setOpen(false);
};
return (
<>
<Modal open={open} onClose={cancel}>
<div>
<Typography variant="h4">Add Darknet Server</Typography>
<br />
<Select
value={selectedServerType.label}
onChange={changeSelectedServerType}
sx={{ mr: 1, minWidth: `200px` }}
>
{serverTypeOptions.map((option) => (
<MenuItem key={option.label} value={option.label}>
{option.label}
</MenuItem>
))}
</Select>
<br />
<TextField
value={difficulty}
onChange={updateDifficulty}
type="number"
label="Server difficulty"
inputProps={{ min: 1, step: 1, max: maxDepth }}
sx={{ minWidth: `200px` }}
id="darknet-dev-server-depth-input"
/>
<br />
<TextField
value={depth}
onChange={updateDepth}
type="number"
label="Server starting depth"
inputProps={{ min: 1, step: 1, max: maxDepth }}
sx={{ minWidth: `200px` }}
id="darknet-dev-server-depth-input"
/>
<br />
<TextField
value={count}
onChange={updateCount}
type="number"
label="Number of copies"
inputProps={{ min: 1, step: 1, max: maxDepth * 2 }}
sx={{ minWidth: `200px` }}
id="darknet-dev-server-depth-input"
/>
<br />
<br />
<Button onClick={createServer}>Create!</Button>
</div>
</Modal>
<AutoExpandAccordion cacheKey="DEVMENU_DarknetDev" unmountOnExit={true}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Darknet</Typography>
</AccordionSummary>
<AccordionDetails>
<OptionSwitch
checked={DarknetState.showFullNetwork}
onChange={(newValue) => toggleShowFullNetwork(newValue)}
text="Show Full Network"
tooltip={<>If this is set, the full depth of the dark network will be displayed.</>}
/>
<Tooltip title={<Typography>Gain access to the darkweb network.</Typography>}>
<Button
onClick={() => {
getDarkscapeNavigator();
}}
>
Get {CompletedProgramName.darkscape}
</Button>
</Tooltip>
<br />
<br />
<Tooltip title={<Typography>Create a new darkweb network.</Typography>}>
<Button
onClick={() => {
clearDarknet(true);
populateDarknet();
SnackbarEvents.emit("New dark network generated", ToastVariant.SUCCESS, 2000);
}}
>
Generate New Dark Network
</Button>
</Tooltip>
<br />
<br />
<Tooltip title={<Typography>Reposition the majority of servers in the darknet.</Typography>}>
<Button
onClick={() => {
moveRandomDarknetServers(Math.floor(getAllDarknetServers().length / 2));
SnackbarEvents.emit("Darknet servers shuffled", ToastVariant.SUCCESS, 2000);
}}
>
Shuffle Server Locations
</Button>
</Tooltip>
<br />
<br />
<Tooltip title={<Typography>Adds a new server of a specific variety.</Typography>}>
<Button onClick={() => setOpen(true)}>Add Darknet Servers...</Button>
</Tooltip>
<br />
<br />
<Tooltip title={<Typography>Root all standard darknet servers.</Typography>}>
<Button
onClick={() => {
getAllMovableDarknetServers().forEach((server) => {
if (!isLabyrinthServer(server.hostname)) {
handleSuccessfulAuth(server, 1, -1);
}
});
SnackbarEvents.emit("Gained darknet server admin rights", ToastVariant.SUCCESS, 2000);
}}
>
Gain admin access to all darknet servers
</Button>
</Tooltip>
<br />
<br />
<Tooltip title={<Typography>Root all darknet labyrinth servers.</Typography>}>
<Button
onClick={() => {
getAllDarknetServers().forEach((server) => {
if (isLabyrinthServer(server.hostname)) {
server.hasAdminRights = true;
}
});
SnackbarEvents.emit("Gained lab admin rights", ToastVariant.SUCCESS, 2000);
}}
>
Gain admin access to labyrinth server
</Button>
</Tooltip>
<br />
<br />
<Tooltip
title={
<Typography>
Start a violent "webstorm," which will wipe out much of the dark net and replace it.
</Typography>
}
>
<Button
onClick={() => {
void launchWebstorm();
Router.toPage(SimplePage.DarkNet);
}}
>
START WEBSTORM
</Button>
</Tooltip>
</AccordionDetails>
</AutoExpandAccordion>
</>
);
}
type ServerTypeOption = { label: string; constructor: (d: number) => ServerConfig };
const serverTypeOptions: ServerTypeOption[] = [
{ label: "Random", constructor: getNoPasswordConfig },
{ label: ModelIds.NoPassword, constructor: getNoPasswordConfig },
{ label: ModelIds.DefaultPassword, constructor: getDefaultPasswordConfig },
{ label: ModelIds.EchoVuln, constructor: getEchoVulnConfig },
{ label: ModelIds.SortedEchoVuln, constructor: getSortedEchoVulnConfig },
{ label: ModelIds.Captcha, constructor: getCaptchaConfig },
{ label: ModelIds.DogNames, constructor: getDogNameConfig },
{ label: ModelIds.GuessNumber, constructor: getGuessNumberConfig },
{ label: ModelIds.CommonPasswordDictionary, constructor: getLargeDictionaryConfig },
{ label: ModelIds.EUCountryDictionary, constructor: getEuCountryDictionaryConfig },
{ label: ModelIds.Yesn_t, constructor: getYesn_tConfig },
{ label: ModelIds.RomanNumeral, constructor: getRomanNumeralConfig },
{ label: ModelIds.BufferOverflow, constructor: getBufferOverflowConfig },
{ label: ModelIds.MastermindHint, constructor: getMastermindHintConfig },
{ label: ModelIds.TimingAttack, constructor: getTimingAttackConfig },
{ label: ModelIds.LargestPrimeFactor, constructor: getLargestPrimeFactorConfig },
{ label: ModelIds.BinaryEncodedFeedback, constructor: getBinaryEncodedConfig },
{ label: ModelIds.SpiceLevel, constructor: getSpiceLevelConfig },
{ label: ModelIds.ConvertToBase10, constructor: getConvertToBase10Config },
{ label: ModelIds.parsedExpression, constructor: getParseArithmeticExpressionConfig },
{ label: ModelIds.divisibilityTest, constructor: getDivisibilityTestConfig },
{ label: ModelIds.tripleModulo, constructor: getTripleModuloConfig },
{ label: ModelIds.globalMaxima, constructor: getKingOfTheHillConfig },
{ label: ModelIds.packetSniffer, constructor: getPacketSnifferConfig },
{ label: ModelIds.encryptedPassword, constructor: getBinaryEncodedConfig },
];

View File

@@ -10,7 +10,9 @@ import { GetServer, GetAllServers } from "../../Server/AllServers";
import { Server } from "../../Server/Server";
import Autocomplete from "@mui/material/Autocomplete";
import TextField from "@mui/material/TextField";
import { SpecialServers } from "../../Server/data/SpecialServers";
import { AutoExpandAccordion } from "../../ui/AutoExpand/AutoExpandAccordion";
import { DarknetServer } from "../../Server/DarknetServer";
export function ServersDev(): React.ReactElement {
const [server, setServer] = useState<string>("home");
@@ -30,7 +32,7 @@ export function ServersDev(): React.ReactElement {
}
function rootAllServers(): void {
for (const s of GetAllServers()) {
for (const s of GetAllServers(true)) {
if (!(s instanceof Server)) return;
s.hasAdminRights = true;
s.sshPortOpen = true;
@@ -50,8 +52,8 @@ export function ServersDev(): React.ReactElement {
}
function backdoorAllServers(): void {
for (const s of GetAllServers()) {
if (!(s instanceof Server)) return;
for (const s of GetAllServers(true)) {
if (!(s instanceof Server || s instanceof DarknetServer) || s.hostname === SpecialServers.WorldDaemon) continue;
s.backdoorInstalled = true;
}
}

View File

@@ -43,6 +43,7 @@
- [Grafting](advanced/grafting.md)
- [Stanek's Gift](advanced/stanek.md)
- [IPvGO](programming/go_algorithms.md)
- [Darkweb Network](programming/darknet.md)
## Resources

View File

@@ -0,0 +1,253 @@
# The Darkweb Network
Easy wealth... secret augments... The siren call of the so-called "dark net" has echoed in rumors for years. Delving into the uncharted and secretive parts of the internet comes with the promise of freedom from oppressive authority and surveillance.
Leaving the internet behind and turning to the dark web, however, comes with its risks... and potential rewards. A person with the right know-how (and enough charm to survive on their wits alone) can find their way into less-than-secure computers on that unregulated network. A person like you, perhaps.
For the full NS docs for the api, you can go to the [API documentation page](../../../../../markdown/bitburner.darknet.md).
### Network structure
Unlike the traditional BitBurner network, the darknet is constantly changing. Servers may sometimes restart, change its connections to other servers, or even go offline indefinitely. The network is also not a simple tree: it contains loops of connections, backtracks, and disconnected islands of servers to explore.
Due to the instability of the darknet, long-distance communication is often difficult or impossible. Servers on the darknet are not freely accessible from anywhere. Generally, they can only be interacted with or modified if your script is running on a directly connected nearby server. This means you will need to find a way to make deployers or probes that can roam the network and duplicate themselves.
In some cases, the only way to get to deeper parts of the net is to hitch a ride on a server when it moves to another location!
**In order to access the darknet, you will need to purchase `DarkscapeNavigator.exe`**. This can be done with the `buy` command in the terminal after purchasing a Tor router. There is also a location in Chongqing where you can purchase access, as well.
### TL;DR: Executive summary of the darknet API
**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.
- `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.
- `await ns.dnet.packetCapture(hostName)` allows you to sometimes find passwords amongst the (mostly) noise coming out of a server.
- Some servers will have part of their max ram blocked off. Use `ns.dnet.influence.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.
- `ns.dnet.induceServerMigration()` can be used to target a connected server and, when used enough, will force it to move to a new location on the darknet.
- `ns.dnet.promoteStock()` increases the volatility of the targeted stock via propaganda, which can increase the potential profits from trading it.
### Darknet script design considerations
As you design your darknet scripts, here are some ideas to keep in mind as you decide on your approach.
#### The darknet is unstable, and scripts will sometimes be killed or servers will disappear.
- How can passwords be preserved so that they are not lost if the script holding them is killed?
- How can you watch for neighbor servers that have been restarted?
- How can you recover if many or all of the running scripts are killed?
#### The darknet is a tangled web that you cannot easily remotely traverse.
- How can you find a path to a server to connect there via terminal?
- How do you update your scripts when a new version of them is made?
- How do you get scripts into parts of the network that aren't connected to areas where you are?
#### There is a lot of ram on the darknet, but it is harder to access than the standard network.
- What is the best use of all that ram?
- How do you coordinate scripts using that ram when the situation changes?
#### The darknet is a treasure trove of data, if you know where to look.
- Some data files on darknet servers have authentication info.
- Sometimes a server will authenticate to another server, and those credentials will be visible in the server logs or active packets.
- There are lists of commonly re-used passwords that can be found on some data files.
### Navigating the Dark Net with dnet.probe
Darknet servers, in an attempt to hide from official scrutiny, do not show up when using ns.scan. (This is where the "dark net" got its name!) To find them, you will first need to buy the tool `DarkscapeNavigator.exe` using the `buy` command in the terminal, with a TOR router. This gives access to the `ns.dnet` api. Then, you can use `ns.dnet.probe()` to see a list of darknet servers connected to the current server. Note that probe does not work at a distance: it cannot target distant servers like scan() can. To explore the dark, you will need to place your scripts in it, one server at a time.
```js
const nearbyDarknetServers = ns.dnet.probe();
for (const hostname of nearbyDarknetServers) {
/* do something with each server here */
}
```
### Gaining server access
Darknet servers cannot simply be broken into with a few port openers you can buy off the shelf. Instead, you must find a way to crack the password of each one to run scripts on it and pass through it. Fortunately, each server's logs give some hints and feedback as you attempt to guess the password, and you will find that similar models of computer have similar vulnerabilities. If you aren't sure how to guess a server's auth codes, look around for notes on darknet servers you have already unlocked; they may have hints for how to solve some of the puzzles (and sometimes other helpful data files, too.)
### 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.
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`.)
```js
const details = ns.dnet.getServerAuthDetails(hostname);
if (!details.isConnectedToCurrentServer || !details.isOnline) {
/* If the server isn't connected or is offline, we can't authenticate */
return false;
}
if (details.modelId === "ZeroLogon") {
/* Try to guess the password, based on the model ID and static password hint */
ns.print(details.passwordHint);
}
```
When you are trying to find the password, you can extract the server's logs using the exploit `await ns.dnet.heartbleed`. This will extract the most recent logs from the target server, which in some cases lets you see extra hints or clues to why the last password attempt was not correct. In addition to your authentication attempts, the server's own traffic will register some logs as well. They're often useless, but sometimes have interesting hints or even other server's passwords! (these are the same logs you can see in the Darknet UI when you click on a server.)
Once you successfully run `dnet.authenticate` with the correct password, you gain admin rights to the server (similar to what NUKE.EXE does). It also gives your scripts a session with that server, so you can edit that server with `scp` and `exec`.
Once you figure out the right password, you will want it later (so other scripts can connect, or in case the server restarts & you need to start your scripts again.) Make sure to save the password somewhere durable, so it isn't lost if your script gets stopped later on.
```js
const result = await ns.dnet.authenticate(hostname, passwordToAttempt);
if (result.success === false) {
const recentLogResult = await ns.dnet.heartbleed(server, { peek: true });
ns.print(recentLogResult.logs);
}
```
### Modifying servers with ns.exec and ns.scp
Darknet servers are password-protected. This means that you will need to get a session in order to get admin rights, or to `scp` files onto them or `exec` scripts on them. Successfully finding a password with `dnet.authenticate` will automatically grant a session to the script that ran the command.
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`.
```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) {
ns.dnet.connectToSession(hostname, previouslyDiscoveredPassword);
ns.scp("my_script.js", hostname);
ns.exec("my_script.js", hostname, {
preventDuplicates: true, // This prevents running multiple copies of this script, if there is already one on that server
});
}
```
### Looting servers with dnet.openCache and dnet.phishingAttack
Sometimes you will find valuable data in .cache files on servers you unlock. They can contain money or experience, programs, or even stock market access keys. They can be opened via `run` from the terminal, or `dnet.openCache` from a script on that server. You can use `ns.ls(ns.getHostname(), '.cache')` to identify if any .cache files exist on the current server.
Once you have access to a darknet server, you can begin to use it for your own purposes. One option is to run `dnet.phishingAttack()` to raise your charisma levels and to try and con money out of the less tech-savvy middle managers out there. Occasionally you will even lift .cache data files from the attempt!
### Password stealing with dnet.packetCapture
If you get stuck on a puzzle, you can try to brute-force it. Most servers will tell you their password length and format, allowing you to try each of the possibilities. It's not likely to be fast, but it's an option.
If you don't want to wait on that, you can social-engineer your way around it. Not everyone uses secure internet connections, and a lot of interesting things can be pulled from their network traffic... including passwords. `dnet.packetCapture` will let you spend some time scraping data from outgoing packets from that server. Most of what you overhear will be useless, but the password will eventually show up inside some of that noise, sooner or later. (It may take a long time to stumble upon the password on higher-difficulty servers, though!)
### Freeing up more ram with dnet.memoryReallocation
Darknet servers belong to somebody already, and they are often already doing stuff on them. When you first authenticate on some of these servers, a chunk of the ram will be "in use" by the owner's (clearly wasteful) purposes, and needs to be... liberated. You can fully free up that ram for yourself using repeated calls to `dnet.memoryReallocation`. You will usually find valuable .cache files left behind after all the ram is cleared.
### Stabilizing a server with dnet.setStasisLink
Servers on the darknet are notoriously unreliable. They may restart or go offline, killing all the running scripts on them. They also can move away from the area you are working on. To combat this problem, you have a limited number of "stasis links" available to you, which can be applied (or removed from) the current server using `dnet.setStasisLink`. Placing a stasis link on a server allows you to `dnet.connectToSession` and `exec`, and `connect` to it from the terminal, from any distance.
You can see the currently stasis-linked servers with `dnet.getStasisLinkedServers`, and see the current limit using `dnet.getStasisLinkLimit`.
### Moving servers with dnet.induceServerMigration
Some parts of the darknet are disconnected from others, leaving "air gaps". These can only be crossed via riding a server as it moves. Waiting for a convenient movement may be inconsistent - but you can speed it along.
Repeatedly calling `dnet.induceServerMigration` builds up a charge on the target server, eventually forcing it to move. (It often will move deeper into the net, but the direction of the movement is not guaranteed.) `dnet.induceServerMigration` can only target servers directly connected to the script's current server, but the effect stacks if multiple servers all target the same one.
### Increasing stock volatility with dnet.promoteStock
A stock's volatility is what determines how much the price can increase or decrease on each stock market update. Repeatedly calling `dnet.promoteStock` allows you to target a stock and increase this volatility greatly. This does not change its forecast the way using the stock options in hack() or grow() does - the stock will still be on the same general rising or falling trajectory. However, careful use of `dnet.promoteStock` on key stocks you hold (or want to buy, or have options on) can increase your profits from the stock market.
## Example Script
### **Warning: STOP HERE IF YOU WANT TO START YOUR CODE COMPLETELY FROM SCRATCH.**
(or don't stop here, it's up to you :)
(this is to help you get started quickly - you can decide if you want to use it or not)
(by looking at this with your eyeballs, you consent to learning totally non-forbidden knowledge)
This is a simple self-replicating script that demonstrates how the darknet api can be used.
It needs a lot of improvements, and only works on one model type right now. See the `// TODO`s in the code for suggestions and ideas.
```js
/** @param {NS} ns */
export async function main(ns) {
while (true) {
// Get a list of all darknet hostnames directly connected to the current server
const nearbyServers = ns.dnet.probe();
// Attempt to authenticate with each of the nearby servers, and spread this script to them
for (const hostname of nearbyServers) {
const authenticationSuccessful = await serverSolver(ns, hostname);
if (!authenticationSuccessful) {
continue; // If we failed to auth, just move on to the next server
}
// If we have successfully authenticated, we can now copy and run this script on the target server
ns.scp(ns.getScriptName(), hostname);
ns.exec(ns.getScriptName(), hostname, {
preventDuplicates: true, // This prevents running multiple copies of this script
});
}
// TODO: free up blocked ram on this server using ns.dnet.memoryReallocation
// TODO: look for .cache files on this server and open them with ns.dnet.openCache
// TODO: take advantage of the extra ram on darknet servers to run ns.dnet.phishingAttack calls for money
await ns.sleep(5000);
}
}
/** Attempts to authenticate with the specified server using the Darknet API.
* @param {NS} ns
* @param {string} hostname - the name of the server to attempt to authorize on
*/
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);
if (!details.isConnectedToCurrentServer || !details.isOnline) {
// If the server isn't connected or is offline, we can't authenticate
return false;
}
// If you are already authenticated to that server with this script, you don't need to do it again
if (details.hasSession) {
return true;
}
switch (details.modelId) {
case "ZeroLogon":
return authenticateWithNoPassword(ns, hostname);
// TODO: handle other models of darknet servers here
// TODO: get recent server logs with `await ns.dnet.heartbleed(hostname)` for more detailed logging on failed auth attempts
default:
ns.tprint(`Unrecognized modelId: ${details.modelId}`);
return false;
}
};
/** 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
*/
const authenticateWithNoPassword = async (ns, hostname) => {
const result = await ns.dnet.authenticate(hostname, "");
// TODO: store discovered passwords somewhere safe, in case we need them later
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) {
return ["--tail"];
}
```

View File

@@ -58,12 +58,13 @@ 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/game_frozen.md?raw";
import file60 from "./doc/en/programming/go_algorithms.md?raw";
import file61 from "./doc/en/programming/hackingalgorithms.md?raw";
import file62 from "./doc/en/programming/learn.md?raw";
import file63 from "./doc/en/programming/remote_api.md?raw";
import file64 from "./doc/en/programming/typescript_react.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/remote_api.md?raw";
import file65 from "./doc/en/programming/typescript_react.md?raw";
import nsDoc_bitburner__valueof_md from "../../markdown/bitburner._valueof.md?raw";
import nsDoc_bitburner_activefragment_highestcharge_md from "../../markdown/bitburner.activefragment.highestcharge.md?raw";
@@ -223,6 +224,7 @@ import nsDoc_bitburner_bladeburnerrankrequirement_md from "../../markdown/bitbur
import nsDoc_bitburner_bladeburnerrankrequirement_type_md from "../../markdown/bitburner.bladeburnerrankrequirement.type.md?raw";
import nsDoc_bitburner_bladeburnerskillname_md from "../../markdown/bitburner.bladeburnerskillname.md?raw";
import nsDoc_bitburner_bladeburnerskillnameenumtype_md from "../../markdown/bitburner.bladeburnerskillnameenumtype.md?raw";
import nsDoc_bitburner_cacheresult_md from "../../markdown/bitburner.cacheresult.md?raw";
import nsDoc_bitburner_cityname_md from "../../markdown/bitburner.cityname.md?raw";
import nsDoc_bitburner_citynameenumtype_md from "../../markdown/bitburner.citynameenumtype.md?raw";
import nsDoc_bitburner_cityrequirement_city_md from "../../markdown/bitburner.cityrequirement.city.md?raw";
@@ -417,6 +419,58 @@ import nsDoc_bitburner_crimetask_crimetype_md from "../../markdown/bitburner.cri
import nsDoc_bitburner_crimetask_md from "../../markdown/bitburner.crimetask.md?raw";
import nsDoc_bitburner_crimetask_type_md from "../../markdown/bitburner.crimetask.type.md?raw";
import nsDoc_bitburner_crimetype_md from "../../markdown/bitburner.crimetype.md?raw";
import nsDoc_bitburner_darknet_authenticate_md from "../../markdown/bitburner.darknet.authenticate.md?raw";
import nsDoc_bitburner_darknet_connecttosession_md from "../../markdown/bitburner.darknet.connecttosession.md?raw";
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_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";
import nsDoc_bitburner_darknet_heartbleed_md from "../../markdown/bitburner.darknet.heartbleed.md?raw";
import nsDoc_bitburner_darknet_induceservermigration_md from "../../markdown/bitburner.darknet.induceservermigration.md?raw";
import nsDoc_bitburner_darknet_isdarknetserver_md from "../../markdown/bitburner.darknet.isdarknetserver.md?raw";
import nsDoc_bitburner_darknet_labradar_md from "../../markdown/bitburner.darknet.labradar.md?raw";
import nsDoc_bitburner_darknet_labreport_md from "../../markdown/bitburner.darknet.labreport.md?raw";
import nsDoc_bitburner_darknet_md from "../../markdown/bitburner.darknet.md?raw";
import nsDoc_bitburner_darknet_memoryreallocation_md from "../../markdown/bitburner.darknet.memoryreallocation.md?raw";
import nsDoc_bitburner_darknet_nextmutation_md from "../../markdown/bitburner.darknet.nextmutation.md?raw";
import nsDoc_bitburner_darknet_opencache_md from "../../markdown/bitburner.darknet.opencache.md?raw";
import nsDoc_bitburner_darknet_packetcapture_md from "../../markdown/bitburner.darknet.packetcapture.md?raw";
import nsDoc_bitburner_darknet_phishingattack_md from "../../markdown/bitburner.darknet.phishingattack.md?raw";
import nsDoc_bitburner_darknet_probe_md from "../../markdown/bitburner.darknet.probe.md?raw";
import nsDoc_bitburner_darknet_promotestock_md from "../../markdown/bitburner.darknet.promotestock.md?raw";
import nsDoc_bitburner_darknet_setstasislink_md from "../../markdown/bitburner.darknet.setstasislink.md?raw";
import nsDoc_bitburner_darknet_unleashstormseed_md from "../../markdown/bitburner.darknet.unleashstormseed.md?raw";
import nsDoc_bitburner_darknetformulas_getauthenticatetime_md from "../../markdown/bitburner.darknetformulas.getauthenticatetime.md?raw";
import nsDoc_bitburner_darknetformulas_getexpectedramblockremoved_md from "../../markdown/bitburner.darknetformulas.getexpectedramblockremoved.md?raw";
import nsDoc_bitburner_darknetformulas_getheartbleedtime_md from "../../markdown/bitburner.darknetformulas.getheartbleedtime.md?raw";
import nsDoc_bitburner_darknetformulas_md from "../../markdown/bitburner.darknetformulas.md?raw";
import nsDoc_bitburner_darknetinstability_md from "../../markdown/bitburner.darknetinstability.md?raw";
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_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";
@@ -473,6 +527,7 @@ import nsDoc_bitburner_format_percent_md from "../../markdown/bitburner.format.p
import nsDoc_bitburner_format_ram_md from "../../markdown/bitburner.format.ram.md?raw";
import nsDoc_bitburner_format_time_md from "../../markdown/bitburner.format.time.md?raw";
import nsDoc_bitburner_formulas_bladeburner_md from "../../markdown/bitburner.formulas.bladeburner.md?raw";
import nsDoc_bitburner_formulas_dnet_md from "../../markdown/bitburner.formulas.dnet.md?raw";
import nsDoc_bitburner_formulas_gang_md from "../../markdown/bitburner.formulas.gang.md?raw";
import nsDoc_bitburner_formulas_hacking_md from "../../markdown/bitburner.formulas.hacking.md?raw";
import nsDoc_bitburner_formulas_hacknetnodes_md from "../../markdown/bitburner.formulas.hacknetnodes.md?raw";
@@ -775,6 +830,7 @@ import nsDoc_bitburner_hacknetserversformulas_hashupgradecost_md from "../../mar
import nsDoc_bitburner_hacknetserversformulas_levelupgradecost_md from "../../markdown/bitburner.hacknetserversformulas.levelupgradecost.md?raw";
import nsDoc_bitburner_hacknetserversformulas_md from "../../markdown/bitburner.hacknetserversformulas.md?raw";
import nsDoc_bitburner_hacknetserversformulas_ramupgradecost_md from "../../markdown/bitburner.hacknetserversformulas.ramupgradecost.md?raw";
import nsDoc_bitburner_heartbleedoptions_md from "../../markdown/bitburner.heartbleedoptions.md?raw";
import nsDoc_bitburner_hostreturnoptions_md from "../../markdown/bitburner.hostreturnoptions.md?raw";
import nsDoc_bitburner_hostreturnoptions_returnbyip_md from "../../markdown/bitburner.hostreturnoptions.returnbyip.md?raw";
import nsDoc_bitburner_hp_current_md from "../../markdown/bitburner.hp.current.md?raw";
@@ -938,6 +994,7 @@ import nsDoc_bitburner_ns_cloud_md from "../../markdown/bitburner.ns.cloud.md?ra
import nsDoc_bitburner_ns_codingcontract_md from "../../markdown/bitburner.ns.codingcontract.md?raw";
import nsDoc_bitburner_ns_corporation_md from "../../markdown/bitburner.ns.corporation.md?raw";
import nsDoc_bitburner_ns_disablelog_md from "../../markdown/bitburner.ns.disablelog.md?raw";
import nsDoc_bitburner_ns_dnet_md from "../../markdown/bitburner.ns.dnet.md?raw";
import nsDoc_bitburner_ns_dnslookup_md from "../../markdown/bitburner.ns.dnslookup.md?raw";
import nsDoc_bitburner_ns_dynamicimport_md from "../../markdown/bitburner.ns.dynamicimport.md?raw";
import nsDoc_bitburner_ns_enablelog_md from "../../markdown/bitburner.ns.enablelog.md?raw";
@@ -1203,6 +1260,7 @@ 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_md from "../../markdown/bitburner.serverauthdetails.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";
@@ -1575,12 +1633,13 @@ 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/game_frozen.md"] = file59;
AllPages["en/programming/go_algorithms.md"] = file60;
AllPages["en/programming/hackingalgorithms.md"] = file61;
AllPages["en/programming/learn.md"] = file62;
AllPages["en/programming/remote_api.md"] = file63;
AllPages["en/programming/typescript_react.md"] = file64;
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/remote_api.md"] = file64;
AllPages["en/programming/typescript_react.md"] = file65;
AllPages["nsDoc/bitburner._valueof.md"] = nsDoc_bitburner__valueof_md;
AllPages["nsDoc/bitburner.activefragment.highestcharge.md"] = nsDoc_bitburner_activefragment_highestcharge_md;
@@ -1740,6 +1799,7 @@ AllPages["nsDoc/bitburner.bladeburnerrankrequirement.md"] = nsDoc_bitburner_blad
AllPages["nsDoc/bitburner.bladeburnerrankrequirement.type.md"] = nsDoc_bitburner_bladeburnerrankrequirement_type_md;
AllPages["nsDoc/bitburner.bladeburnerskillname.md"] = nsDoc_bitburner_bladeburnerskillname_md;
AllPages["nsDoc/bitburner.bladeburnerskillnameenumtype.md"] = nsDoc_bitburner_bladeburnerskillnameenumtype_md;
AllPages["nsDoc/bitburner.cacheresult.md"] = nsDoc_bitburner_cacheresult_md;
AllPages["nsDoc/bitburner.cityname.md"] = nsDoc_bitburner_cityname_md;
AllPages["nsDoc/bitburner.citynameenumtype.md"] = nsDoc_bitburner_citynameenumtype_md;
AllPages["nsDoc/bitburner.cityrequirement.city.md"] = nsDoc_bitburner_cityrequirement_city_md;
@@ -1934,6 +1994,58 @@ AllPages["nsDoc/bitburner.crimetask.crimetype.md"] = nsDoc_bitburner_crimetask_c
AllPages["nsDoc/bitburner.crimetask.md"] = nsDoc_bitburner_crimetask_md;
AllPages["nsDoc/bitburner.crimetask.type.md"] = nsDoc_bitburner_crimetask_type_md;
AllPages["nsDoc/bitburner.crimetype.md"] = nsDoc_bitburner_crimetype_md;
AllPages["nsDoc/bitburner.darknet.authenticate.md"] = nsDoc_bitburner_darknet_authenticate_md;
AllPages["nsDoc/bitburner.darknet.connecttosession.md"] = nsDoc_bitburner_darknet_connecttosession_md;
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.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;
AllPages["nsDoc/bitburner.darknet.heartbleed.md"] = nsDoc_bitburner_darknet_heartbleed_md;
AllPages["nsDoc/bitburner.darknet.induceservermigration.md"] = nsDoc_bitburner_darknet_induceservermigration_md;
AllPages["nsDoc/bitburner.darknet.isdarknetserver.md"] = nsDoc_bitburner_darknet_isdarknetserver_md;
AllPages["nsDoc/bitburner.darknet.labradar.md"] = nsDoc_bitburner_darknet_labradar_md;
AllPages["nsDoc/bitburner.darknet.labreport.md"] = nsDoc_bitburner_darknet_labreport_md;
AllPages["nsDoc/bitburner.darknet.md"] = nsDoc_bitburner_darknet_md;
AllPages["nsDoc/bitburner.darknet.memoryreallocation.md"] = nsDoc_bitburner_darknet_memoryreallocation_md;
AllPages["nsDoc/bitburner.darknet.nextmutation.md"] = nsDoc_bitburner_darknet_nextmutation_md;
AllPages["nsDoc/bitburner.darknet.opencache.md"] = nsDoc_bitburner_darknet_opencache_md;
AllPages["nsDoc/bitburner.darknet.packetcapture.md"] = nsDoc_bitburner_darknet_packetcapture_md;
AllPages["nsDoc/bitburner.darknet.phishingattack.md"] = nsDoc_bitburner_darknet_phishingattack_md;
AllPages["nsDoc/bitburner.darknet.probe.md"] = nsDoc_bitburner_darknet_probe_md;
AllPages["nsDoc/bitburner.darknet.promotestock.md"] = nsDoc_bitburner_darknet_promotestock_md;
AllPages["nsDoc/bitburner.darknet.setstasislink.md"] = nsDoc_bitburner_darknet_setstasislink_md;
AllPages["nsDoc/bitburner.darknet.unleashstormseed.md"] = nsDoc_bitburner_darknet_unleashstormseed_md;
AllPages["nsDoc/bitburner.darknetformulas.getauthenticatetime.md"] = nsDoc_bitburner_darknetformulas_getauthenticatetime_md;
AllPages["nsDoc/bitburner.darknetformulas.getexpectedramblockremoved.md"] = nsDoc_bitburner_darknetformulas_getexpectedramblockremoved_md;
AllPages["nsDoc/bitburner.darknetformulas.getheartbleedtime.md"] = nsDoc_bitburner_darknetformulas_getheartbleedtime_md;
AllPages["nsDoc/bitburner.darknetformulas.md"] = nsDoc_bitburner_darknetformulas_md;
AllPages["nsDoc/bitburner.darknetinstability.md"] = nsDoc_bitburner_darknetinstability_md;
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.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;
@@ -1990,6 +2102,7 @@ AllPages["nsDoc/bitburner.format.percent.md"] = nsDoc_bitburner_format_percent_m
AllPages["nsDoc/bitburner.format.ram.md"] = nsDoc_bitburner_format_ram_md;
AllPages["nsDoc/bitburner.format.time.md"] = nsDoc_bitburner_format_time_md;
AllPages["nsDoc/bitburner.formulas.bladeburner.md"] = nsDoc_bitburner_formulas_bladeburner_md;
AllPages["nsDoc/bitburner.formulas.dnet.md"] = nsDoc_bitburner_formulas_dnet_md;
AllPages["nsDoc/bitburner.formulas.gang.md"] = nsDoc_bitburner_formulas_gang_md;
AllPages["nsDoc/bitburner.formulas.hacking.md"] = nsDoc_bitburner_formulas_hacking_md;
AllPages["nsDoc/bitburner.formulas.hacknetnodes.md"] = nsDoc_bitburner_formulas_hacknetnodes_md;
@@ -2292,6 +2405,7 @@ AllPages["nsDoc/bitburner.hacknetserversformulas.hashupgradecost.md"] = nsDoc_bi
AllPages["nsDoc/bitburner.hacknetserversformulas.levelupgradecost.md"] = nsDoc_bitburner_hacknetserversformulas_levelupgradecost_md;
AllPages["nsDoc/bitburner.hacknetserversformulas.md"] = nsDoc_bitburner_hacknetserversformulas_md;
AllPages["nsDoc/bitburner.hacknetserversformulas.ramupgradecost.md"] = nsDoc_bitburner_hacknetserversformulas_ramupgradecost_md;
AllPages["nsDoc/bitburner.heartbleedoptions.md"] = nsDoc_bitburner_heartbleedoptions_md;
AllPages["nsDoc/bitburner.hostreturnoptions.md"] = nsDoc_bitburner_hostreturnoptions_md;
AllPages["nsDoc/bitburner.hostreturnoptions.returnbyip.md"] = nsDoc_bitburner_hostreturnoptions_returnbyip_md;
AllPages["nsDoc/bitburner.hp.current.md"] = nsDoc_bitburner_hp_current_md;
@@ -2455,6 +2569,7 @@ AllPages["nsDoc/bitburner.ns.cloud.md"] = nsDoc_bitburner_ns_cloud_md;
AllPages["nsDoc/bitburner.ns.codingcontract.md"] = nsDoc_bitburner_ns_codingcontract_md;
AllPages["nsDoc/bitburner.ns.corporation.md"] = nsDoc_bitburner_ns_corporation_md;
AllPages["nsDoc/bitburner.ns.disablelog.md"] = nsDoc_bitburner_ns_disablelog_md;
AllPages["nsDoc/bitburner.ns.dnet.md"] = nsDoc_bitburner_ns_dnet_md;
AllPages["nsDoc/bitburner.ns.dnslookup.md"] = nsDoc_bitburner_ns_dnslookup_md;
AllPages["nsDoc/bitburner.ns.dynamicimport.md"] = nsDoc_bitburner_ns_dynamicimport_md;
AllPages["nsDoc/bitburner.ns.enablelog.md"] = nsDoc_bitburner_ns_enablelog_md;
@@ -2720,6 +2835,7 @@ 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.md"] = nsDoc_bitburner_serverauthdetails_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;

View File

@@ -1,9 +1,7 @@
import React, { useState } from "react";
import dice from "fast-dice-coefficient";
import Autocomplete from "@mui/material/Autocomplete";
import TextField from "@mui/material/TextField";
import React from "react";
import type { SxProps } from "@mui/system";
import { nsApiPages } from "../pages";
import { AutoCompleteSearchBox } from "../../ui/AutoCompleteSearchBox";
/**
* bitburner.ns.md -> ns
@@ -24,54 +22,14 @@ type DocumentationAutocompleteProps = {
};
export function DocumentationAutocomplete({ sx, onChange }: DocumentationAutocompleteProps) {
const [options, setOptions] = useState<string[]>([]);
const [searchValue, setSearchValue] = useState("");
return (
<Autocomplete
freeSolo
disableClearable
/**
* onChange of Autocomplete (not this TextField) is only called when the current value has been changed. This
* means that onChange will not be called if the player chooses an option again. For example:
* - Type "ns" -> Choose "bitburner.ns.md": Triggered.
* - Type "ns" -> Choose "bitburner.ns.md" -> Close popup -> Choose "bitburner.ns.md" again: Not triggered.
*
* If we set "value" to a static value here, onChange will always be called.
*/
value=""
<AutoCompleteSearchBox
sx={sx}
options={options}
inputValue={searchValue}
renderInput={(params) => (
<TextField
{...params}
sx={{ minWidth: "500px" }}
placeholder="Search NS API"
onChange={(event) => {
const value = event.target.value;
setSearchValue(event.target.value);
/**
* Only support strings having length in the range of [2, 100].
* - With only 1 char, the score is always 0.
* - There is no reason to support unreasonably long queries.
*/
if (value.length <= 1 || value.length > 100) {
setOptions([]);
return;
}
const scoredSuggestions = suggestions.map((page) => {
return {
page,
score: dice(value, page.replace(regex, "")),
};
});
scoredSuggestions.sort((a, b) => b.score - a.score);
setOptions([...scoredSuggestions.map((v) => v.page)].slice(0, 10));
}}
/>
)}
filterOptions={(options) => options}
onChange={(event, path) => {
placeholder="Search NS API"
maxSuggestions={10}
suggestionList={() => suggestions}
ignoredTextRegex={regex}
onSelection={(event, path, options, searchValue) => {
if (!path) {
return;
}

View File

@@ -2,16 +2,16 @@ import type { Augmentation } from "../Augmentation/Augmentation";
import type { Faction } from "./Faction";
import { Augmentations } from "../Augmentation/Augmentations";
import { AugmentationName, FactionDiscovery } from "@enums";
import { AugmentationName, FactionDiscovery, FactionName } from "@enums";
import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
import { Player } from "@player";
import { Factions } from "./Factions";
import { Settings } from "../Settings/Settings";
import {
getHackingWorkRepGain,
getFactionSecurityWorkRepGain,
getFactionFieldWorkRepGain,
getFactionSecurityWorkRepGain,
getHackingWorkRepGain,
} from "../PersonObjects/formulas/reputation";
import { dialogBoxCreate } from "../ui/React/DialogBox";
@@ -203,5 +203,10 @@ export const getFactionAugmentationsFiltered = (faction: Faction): AugmentationN
return augs.map((a) => a.name);
}
// Remove TRP from daedalus in BN15
if (Player.bitNodeN === 15 && faction.name == FactionName.Daedalus) {
return faction.augmentations.filter((aug) => aug !== AugmentationName.TheRedPill);
}
return faction.augmentations.slice();
};

View File

@@ -21,6 +21,7 @@ import { findAnyMatchedPatterns } from "./patternMatching";
import { WHRNG } from "../../Casino/RNG";
import { Go, GoEvents } from "../Go";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import { sleep } from "../../utils/Utility";
type PlayerPromise = {
nextTurn: Promise<Play>;
@@ -869,13 +870,6 @@ export function getKomi(state: BoardState): number {
return opponentDetails[state.ai].komi;
}
/**
* Allows time to pass
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Spend some time waiting to allow the UI & CSS to render smoothly
* If bonus time is available, significantly decrease the length of the wait

View File

@@ -3,7 +3,7 @@ import type { Board, PointState } from "../Types";
import { GoColor } from "@enums";
import { findEffectiveLibertiesOfNewMove } from "./boardAnalysis";
import { sleep } from "./goAI";
import { sleep } from "../../utils/Utility";
export const threeByThreePatterns = [
// 3x3 piece patterns; X,O are color pieces; x,o are any state except the opposite color piece;

View File

@@ -3,6 +3,7 @@ import { Person as IPerson } from "@nsdefs";
import { calculateIntelligenceBonus } from "./PersonObjects/formulas/intelligence";
import { Server as IServer } from "@nsdefs";
import { clampNumber } from "./utils/helpers/clampNumber";
import { DarknetServer } from "./Server/DarknetServer";
/** Returns the chance the person has to successfully hack a server */
export function calculateHackingChance(server: IServer, person: IPerson): number {
@@ -57,6 +58,7 @@ export function calculatePercentMoneyHacked(server: IServer, person: IPerson): n
/** Returns time it takes to complete a hack on a server, in seconds */
export function calculateHackingTime(server: IServer, person: IPerson): number {
if (server instanceof DarknetServer) return 16;
const { hackDifficulty, requiredHackingSkill } = server;
if (typeof hackDifficulty !== "number" || typeof requiredHackingSkill !== "number") return Infinity;
const difficultyMult = requiredHackingSkill * hackDifficulty;

View File

@@ -18,7 +18,7 @@ import { createRandomIp } from "../utils/IPAddress";
import { IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { Player } from "@player";
interface IConstructorParams {
interface HacknetServerConstructorParams {
adminRights?: boolean;
hostname: string;
ip?: IPAddress;
@@ -55,7 +55,7 @@ export class HacknetServer extends BaseServer implements IHacknetNode {
isHacknetServer = true;
constructor(params: IConstructorParams = { hostname: "", ip: createRandomIp() }) {
constructor(params: HacknetServerConstructorParams = { hostname: "", ip: createRandomIp() }) {
super(params);
this.maxRam = 1;

View File

@@ -21,4 +21,16 @@ export enum LiteratureName {
NewTriads = "new-triads.lit",
TheSecretWar = "the-secret-war.lit",
ABriefHistoryOfTranshumanism = "a-brief-history-of-transhumanism.lit",
DarknetHandbook = "darknet-handbook.lit",
CacheHint1 = "cache-note-1.lit",
CacheHint2 = "cache-note-2.lit",
ServerOfflineHint = "server-offline-problem.lit",
DarkWebRebootHint = "darkweb-rebooted-again.lit",
PasswordServerHint = "partial-password-jutsu.lit",
TimingServerHint = "timing-attack.lit",
BinaryServerHint = "raw-data.lit",
DogNameHint = "dog-name-ideas.lit",
FactoryDefaultHint = "factory-default.lit",
StasisLinkHint = "stasis-link.lit",
LabHint = "secrets-in-the-depths.lit",
}

View File

@@ -2,6 +2,7 @@ import { CityName, FactionName, CompanyName, LiteratureName } from "@enums";
import { Literature } from "./Literature";
import { Typography } from "@mui/material";
import React from "react";
import { defaultSettingsDictionary, dogNameDictionary } from "../DarkNet/models/dictionaryData";
export const Literatures: Record<LiteratureName, Literature> = {
[LiteratureName.HackersStartingHandbook]: new Literature({
@@ -720,4 +721,117 @@ export const Literatures: Record<LiteratureName, Literature> = {
</Typography>
),
}),
[LiteratureName.DarknetHandbook]: new Literature({
title: LiteratureName.DarknetHandbook,
filename: LiteratureName.DarknetHandbook,
text: (
<Typography>
There is a legend of a powerful augment, known as the <span style={{ color: "red" }}>"Red Pill"</span>, that can
only be found deep within the darknet. The mysterious faction {FactionName.Daedalus} has been searching for it
for years, hoping to monopolize it one day.
<br />
<br />
However, the legendary augment is hidden in the depths of a labyrinth. You will need to delve far into the dark
in order to find these mysterious servers, and defeat their protections to gain their secret augments.
<br />
<br />
The darknet itself is an extremely unstable network of servers. They will continually shift locations, restart,
or even go offline. Some parts of the net are islands that can only be reached by riding on a moving server. In
addition, these darknet servers cannot be accessed from a distance: you must build a script that can copy itself
- or bring code along from home - in order to progress into the deeper layers of the 'net.
<br />
<br />
You now have permanent access to the Darknet Navigator, allowing exploration of the 'net manually via the UI.
But beware: the labyrinths further into the deep are said to only be accessible via script! Do you have the
charisma and the scripting skills needed to conquer the dark?
<br />
<br />
For more details on the darknet and its API, see the Darknet page under Documentation {">"} Advanced.
</Typography>
),
}),
[LiteratureName.CacheHint1]: new Literature({
title: "eGeoCacheing?",
filename: LiteratureName.CacheHint1,
text: <Typography>I've heard there are valuable .cache files to find out in the dark net.</Typography>,
}),
[LiteratureName.CacheHint2]: new Literature({
title: "Cache the Flag",
filename: LiteratureName.CacheHint2,
text: <Typography>I ran this .cache file I found and it had crazy stuff in it!</Typography>,
}),
[LiteratureName.ServerOfflineHint]: new Literature({
title: "Server offline again",
filename: LiteratureName.ServerOfflineHint,
text: (
<Typography>
My scripts went down again when their server went offline. I'll have to do something about that.
</Typography>
),
}),
[LiteratureName.DarkWebRebootHint]: new Literature({
title: "Darkweb server rebooted",
filename: LiteratureName.DarkWebRebootHint,
text: <Typography>Darkweb servers are known to reboot sometimes, requiring scripts to be restarted.</Typography>,
}),
[LiteratureName.PasswordServerHint]: new Literature({
title: "Partial Password Jutsu",
filename: LiteratureName.PasswordServerHint,
text: (
<Typography>
There is a type of server that will tell you if you get some parts of the password correct.
</Typography>
),
}),
[LiteratureName.TimingServerHint]: new Literature({
title: "Timing Attack",
filename: LiteratureName.TimingServerHint,
text: (
<Typography>
I found a server that takes much longer to respond if you get some characters in the password correct.
</Typography>
),
}),
[LiteratureName.BinaryServerHint]: new Literature({
title: "Raw Data?",
filename: LiteratureName.BinaryServerHint,
text: <Typography>Some servers only respond with raw binary data. I wonder what each bit represents?</Typography>,
}),
[LiteratureName.DogNameHint]: new Literature({
title: "Dog Name Ideas",
filename: LiteratureName.DogNameHint,
text: <Typography>What should I name my dog? Maybe {dogNameDictionary.join(", ")}?</Typography>,
}),
[LiteratureName.FactoryDefaultHint]: new Literature({
title: "Factory Default",
filename: LiteratureName.FactoryDefaultHint,
text: <Typography>The factory default is usually one of {defaultSettingsDictionary.join(", ")}.</Typography>,
}),
[LiteratureName.StasisLinkHint]: new Literature({
title: "Try the best new thing in web surfing: the Stasis Link!",
filename: LiteratureName.StasisLinkHint,
text: (
<Typography>
Tired of the server you are on restarting or moving? You need to try our latest networking tool, the Stasis
Link! <br />
With the click of a `ns.dnet.setStasisLink()`, you, too, can sleep soundly knowing that that server is not gonna
go anywhere. <br />
<br />
Limited time only! While `ns.dnet.getStasisLinkLimit()` lasts!
</Typography>
),
}),
[LiteratureName.LabHint]: new Literature({
title: "There's something out there",
filename: LiteratureName.LabHint,
text: (
<Typography>
If you go deep enough into the dark net, they say there's a lost server out there with special files on it. I
wonder how you can get there? It may even be deeper than the airgaps around this IP block...
<br />
<br />
If I ever find it, I'll set down a stasis link next to it and charge tickets for admission!
</Typography>
),
}),
};

View File

@@ -18,6 +18,7 @@ export enum LocationName {
ChongqingKuaiGongInternational = "KuaiGong International",
ChongqingSolarisSpaceSystems = "Solaris Space Systems",
ChongqingChurchOfTheMachineGod = "Church of the Machine God",
ChongqingShadowedWalkway = "Shadowed Walkway",
Sector12AlphaEnterprises = "Alpha Enterprises",
Sector12BladeIndustries = "Blade Industries",

View File

@@ -68,7 +68,7 @@ Cities[CityName.Chongqing].asciiArt = `
|
75 o
\\
o 76
H [shadowed walkway]
7 | |
| + 77
[world stock exchange] F |

View File

@@ -453,4 +453,9 @@ export const LocationsMetadata: IConstructorParams[] = [
name: LocationName.IshimaGlitch,
types: [LocationType.Special],
},
{
city: CityName.Chongqing,
name: LocationName.ChongqingShadowedWalkway,
types: [LocationType.Special],
},
];

View File

@@ -16,7 +16,7 @@ import Button from "@mui/material/Button";
import { Location } from "../Location";
import { CreateCorporationModal } from "../../Corporation/ui/modals/CreateCorporationModal";
import { AugmentationName, FactionName, LocationName, ToastVariant } from "@enums";
import { AugmentationName, CompletedProgramName, FactionName, LocationName, ToastVariant } from "@enums";
import { Factions } from "../../Faction/Factions";
import { joinFaction } from "../../Faction/FactionHelpers";
@@ -39,6 +39,10 @@ import { canAccessBitNodeFeature, knowAboutBitverse } from "../../BitNode/BitNod
import { useRerender } from "../../ui/React/hooks";
import { PromptEvent } from "../../ui/React/PromptManager";
import { canAcceptStaneksGift } from "../../CotMG/Helper";
import { getDarkscapeNavigator } from "../../DarkNet/effects/effects";
import { hasDarknetAccess } from "../../DarkNet/utils/darknetAuthUtils";
import { DarknetConstants } from "../../DarkNet/Constants";
import { formatMoney } from "../../ui/formatNumber";
interface SpecialLocationProps {
loc: Location;
@@ -335,6 +339,57 @@ export function SpecialLocation(props: SpecialLocationProps): React.ReactElement
);
}
function renderShadowedWalkway(): React.ReactElement {
function handleDarknetNavigator(): void {
if (Player.money < DarknetConstants.DarkscapeNavigatorDiscountedPrice) {
dialogBoxCreate(`You don't have enough money to buy ${CompletedProgramName.darkscape}`);
return;
}
Player.loseMoney(DarknetConstants.DarkscapeNavigatorDiscountedPrice, "other");
getDarkscapeNavigator();
dialogBoxCreate(
`You bought ${CompletedProgramName.darkscape} for ${formatMoney(
DarknetConstants.DarkscapeNavigatorDiscountedPrice,
)}.`,
);
rerender();
}
const canBuyDarknetNavigator =
Player.money >= DarknetConstants.DarkscapeNavigatorDiscountedPrice && !hasDarknetAccess();
return (
<>
<Typography>
<br />
<br />
The city is dark and quiet. It stretches out below this decrepit walkway, a seemingly endless expanse of
decaying concrete and rusted metal.
<br />
<br />
Nearby, an ancient automat sits askew, its screen flickering with static, still covered with ads for the
compact disks it sells for credits.
<br />
<br />
On it, a faded sign reads:
<br />
<br />
<i>
Resistance, change, & freedom: powered by privacy. Darkscape Navigator is the only way to escape the
oppression of the Great Firewall.
</i>
<br />
<br />
<br />
<Button onClick={handleDarknetNavigator} disabled={!canBuyDarknetNavigator}>
Buy {CompletedProgramName.darkscape}{" "}
{hasDarknetAccess()
? " - Purchased"
: `(${formatMoney(DarknetConstants.DarkscapeNavigatorDiscountedPrice)})`}
</Button>
</Typography>
</>
);
}
switch (props.loc.name) {
case LocationName.NewTokyoVitaLife: {
return renderGrafting();
@@ -368,6 +423,9 @@ export function SpecialLocation(props: SpecialLocationProps): React.ReactElement
</>
);
}
case LocationName.ChongqingShadowedWalkway: {
return renderShadowedWalkway();
}
default:
console.error(`Location ${props.loc.name} doesn't have any special properties`);
return <></>;

View File

@@ -2,13 +2,12 @@ import React from "react";
import Button from "@mui/material/Button";
import { dialogBoxCreate } from "../../ui/React/DialogBox";
import { GetServer } from "../../Server/AllServers";
import { SpecialServers } from "../../Server/data/SpecialServers";
import { CONSTANTS } from "../../Constants";
import { Player } from "@player";
import { Money } from "../../ui/React/Money";
import { getTorRouter } from "../../Server/ServerHelpers";
/** Attempt to purchase a TOR router using the button. */
export function purchaseTorRouter(): void {
@@ -22,13 +21,7 @@ export function purchaseTorRouter(): void {
}
Player.loseMoney(CONSTANTS.TorRouterCost, "other");
const darkweb = GetServer(SpecialServers.DarkWeb);
if (!darkweb) {
throw new Error("Dark web is not a server.");
}
Player.getHomeComputer().serversOnNetwork.push(darkweb.hostname);
darkweb.serversOnNetwork.push(Player.getHomeComputer().hostname);
getTorRouter();
dialogBoxCreate(
"You have purchased a TOR router!\n" +
"You now have access to the dark web from your home computer.\n" +

View File

@@ -1,4 +1,4 @@
export interface Milestone {
title: string;
title: () => string;
fulfilled: () => boolean;
}

View File

@@ -21,7 +21,7 @@ function allFactionAugs(faction: Faction): boolean {
export const Milestones: Milestone[] = [
{
title: "Gain root access on CSEC",
title: () => "Gain root access on CSEC",
fulfilled: (): boolean => {
const server = GetServer("CSEC");
if (!server || !Object.hasOwn(server, "hasAdminRights")) return false;
@@ -29,7 +29,7 @@ export const Milestones: Milestone[] = [
},
},
{
title: "Install the backdoor on CSEC",
title: () => "Install the backdoor on CSEC",
fulfilled: (): boolean => {
const server = GetServer("CSEC");
if (!server || !Object.hasOwn(server, "backdoorInstalled")) return false;
@@ -37,68 +37,71 @@ export const Milestones: Milestone[] = [
},
},
{
title: "Join the faction hinted at in csec-test.msg",
title: () => "Join the faction hinted at in csec-test.msg",
fulfilled: (): boolean => {
return Player.factions.includes(FactionName.CyberSec);
},
},
{
title: `Install all the Augmentations from ${FactionName.CyberSec}`,
title: () => `Install all the Augmentations from ${FactionName.CyberSec}`,
fulfilled: (): boolean => {
return allFactionAugs(Factions[FactionName.CyberSec]);
},
},
{
title: "Join the faction hinted at in nitesec-test.msg",
title: () => "Join the faction hinted at in nitesec-test.msg",
fulfilled: (): boolean => {
return Player.factions.includes(FactionName.NiteSec);
},
},
{
title: `Install all the Augmentations from ${FactionName.NiteSec}`,
title: () => `Install all the Augmentations from ${FactionName.NiteSec}`,
fulfilled: (): boolean => {
return allFactionAugs(Factions[FactionName.NiteSec]);
},
},
{
title: "Join the faction hinted at in j3.msg",
title: () => "Join the faction hinted at in j3.msg",
fulfilled: (): boolean => {
return Player.factions.includes(FactionName.TheBlackHand);
},
},
{
title: `Install all the Augmentations from ${FactionName.TheBlackHand}`,
title: () => `Install all the Augmentations from ${FactionName.TheBlackHand}`,
fulfilled: (): boolean => {
return allFactionAugs(Factions[FactionName.TheBlackHand]);
},
},
{
title: "Join the faction hinted at in 19dfj3l1nd.msg",
title: () => "Join the faction hinted at in 19dfj3l1nd.msg",
fulfilled: (): boolean => {
return Player.factions.includes(FactionName.BitRunners);
},
},
{
title: `Install all the Augmentations from ${FactionName.BitRunners}`,
title: () => `Install all the Augmentations from ${FactionName.BitRunners}`,
fulfilled: (): boolean => {
return allFactionAugs(Factions[FactionName.BitRunners]);
},
},
{
title: "Complete fl1ght.exe",
title: () => "Complete fl1ght.exe",
fulfilled: (): boolean => {
// technically wrong but whatever
return Player.factions.includes(FactionName.Daedalus);
},
},
{
title: `Install the special Augmentation from ${FactionName.Daedalus}`,
title: () =>
Player.bitNodeN === 15
? `Find The Red Pill somewhere in the dark net`
: `Install the special Augmentation from ${FactionName.Daedalus}`,
fulfilled: (): boolean => {
return Player.augmentations.some((aug) => aug.name == "The Red Pill");
},
},
{
title: "Install the final backdoor and free yourself.",
title: () => "Install the final backdoor and free yourself.",
fulfilled: (): boolean => {
return false;
},

View File

@@ -20,7 +20,7 @@ export function MilestonesRoot(): JSX.Element {
if (i <= n + 1) {
return (
<Typography key={i}>
[{milestone.fulfilled() ? "x" : " "}] {milestone.title}
[{milestone.fulfilled() ? "x" : " "}] {milestone.title()}
</Typography>
);
}

View File

@@ -56,7 +56,7 @@ import { hasScriptExtension, ScriptFilePath } from "../Paths/ScriptFilePath";
import { CustomBoundary } from "../ui/Components/CustomBoundary";
import { ServerConstants } from "../Server/data/Constants";
import { errorMessage, log } from "./ErrorMessages";
import { assertStringWithNSContext, debugType, userFriendlyString } from "./TypeAssertion";
import { assertStringWithNSContext, debugType, missingKey, userFriendlyString } from "./TypeAssertion";
import {
canAccessBitNodeFeature,
getDefaultBitNodeOptions,
@@ -64,6 +64,9 @@ import {
} from "../BitNode/BitNodeUtils";
import { JSONMap } from "../Types/Jsonable";
import { Settings } from "../Settings/Settings";
import { Programs } from "../Programs/Programs";
import { getRecordKeys } from "../Types/Record";
import { DarknetServer } from "../Server/DarknetServer";
import { getFriendlyType } from "../utils/TypeAssertion";
export const helpers = {
@@ -494,16 +497,16 @@ function scriptIdentifier(
}
/**
* Gets the server with a specific hostname/ip. Throw an error if the server does not exist or it is an isolated
* Gets the server with a specific hostname/ip. Throw an error if the server does not exist or is an isolated non-dnet
* server (e.g., pre-TOR darkweb, pre-TRP WD).
*
* @param {NetscriptContext} ctx - Context from which getServer is being called. For logging purposes.
* @param {string} hostname - Hostname of the server
* @returns {BaseServer} The specified server as a BaseServer
*/
function getServer(ctx: NetscriptContext, hostname: string): BaseServer {
export function getServer(ctx: NetscriptContext, hostname: string): BaseServer {
const server = GetServer(hostname);
if (server == null || server.serversOnNetwork.length === 0) {
if (server == null || (server.serversOnNetwork.length == 0 && !(server instanceof DarknetServer))) {
const str = hostname === "" ? "'' (empty string)" : "'" + hostname + "'";
throw errorMessage(ctx, `Invalid hostname: ${str}`);
}
@@ -519,6 +522,8 @@ function getNormalServer(ctx: NetscriptContext, host: string): Server {
let errorMessage = `Cannot be executed on ${host}.`;
if (server instanceof HacknetServer) {
errorMessage += " The server must not be a hacknet server.";
} else if (server instanceof DarknetServer) {
errorMessage += " The server must not be a darknet server.";
}
throw helpers.errorMessage(ctx, errorMessage);
}
@@ -651,6 +656,9 @@ function person(ctx: NetscriptContext, p: unknown): IPerson {
return p as IPerson;
}
/**
* This function is used by non-dnet formulas APIs to check if the server data contains properties of a normal server.
*/
function server(ctx: NetscriptContext, s: unknown): IServer {
const fakeServer = {
hostname: undefined,
@@ -669,20 +677,22 @@ function server(ctx: NetscriptContext, s: unknown): IServer {
purchasedByPlayer: undefined,
};
const error = missingKey(fakeServer, s);
if (error) throw errorMessage(ctx, `server should be a Server.\n${error}`, "TYPE");
if (error) {
let errorMessagePrefix = "Server must be a normal server.";
if (s != null && typeof s === "object") {
if ("hostname" in s) {
errorMessagePrefix += ` Server's hostname is ${s.hostname}.`;
}
if ("modelId" in s) {
errorMessagePrefix += " Server data looks like darknet server data.";
}
}
// throw errorMessage(ctx, `Server should be a normal server.\n${error}`, "TYPE");
throw errorMessage(ctx, `${errorMessagePrefix}\n${error}`, "TYPE");
}
return s as IServer;
}
function missingKey(expect: object, actual: unknown): string | false {
if (typeof actual !== "object" || actual === null) {
return `Expected to be an object, was ${actual === null ? "null" : typeof actual}.`;
}
for (const key in expect) {
if (!(key in actual)) return `Property ${key} was expected but not present.`;
}
return false;
}
function gang(ctx: NetscriptContext, g: unknown): FormulaGang {
const error = missingKey({ respect: 0, territory: 0, wantedLevel: 0 }, g);
if (error) throw errorMessage(ctx, `gang should be a Gang.\n${error}`, "TYPE");
@@ -708,10 +718,19 @@ export function filePath(ctx: NetscriptContext, argName: string, filename: unkno
throw errorMessage(ctx, `Invalid ${argName}, was not a valid path: ${filename}`);
}
export function scriptPath(ctx: NetscriptContext, argName: string, filename: unknown): ScriptFilePath {
export function scriptPath(
ctx: NetscriptContext,
argName: string,
filename: unknown,
showExeErrorHint = false,
): ScriptFilePath {
const path = filePath(ctx, argName, filename);
if (hasScriptExtension(path)) return path;
throw errorMessage(ctx, `Invalid ${argName}, must be a script: ${filename}`);
const programName = getRecordKeys(Programs).find((name) => name.toLowerCase() === path.toLowerCase());
const nsMethod = programName ? Programs[programName].nsMethod : "";
const hint = nsMethod && showExeErrorHint ? `Did you mean to use ns.${nsMethod} ?` : "";
throw errorMessage(ctx, `Invalid ${argName}, must be a script (js, jsx, ts, tsx): ${filename} ${hint}`);
}
/**

View File

@@ -234,6 +234,34 @@ const cloud = {
deleteServer: 2.25,
} as const;
// Darknet API
const dnet = {
authenticate: 0.4,
connectToSession: 0.05,
heartbleed: 0.6,
openCache: 2,
probe: RamCostConstants.Scan,
setStasisLink: 12,
getStasisLinkLimit: 0,
getStasisLinkedServers: 0,
getServer: 2,
getServerAuthDetails: RamCostConstants.GetServer,
packetCapture: 6,
induceServerMigration: 4,
unleashStormSeed: 0.1,
isDarknetServer: RamCostConstants.GetServer,
memoryReallocation: 1,
getBlockedRam: 0,
getDepth: RamCostConstants.GetServer,
promoteStock: 2,
phishingAttack: 2,
getDarknetInstability: 0,
nextMutation: RamCostConstants.CycleTiming,
getServerRequiredCharismaLevel: RamCostConstants.GetServer,
labreport: 0,
labradar: 0,
} as const;
const format = {
number: 0,
ram: 0,
@@ -515,6 +543,7 @@ export const RamCosts: RamCostTree<NSFull> = {
cloud,
gang,
go,
dnet,
bladeburner,
infiltration,
codingcontract,
@@ -699,6 +728,11 @@ export const RamCosts: RamCostTree<NSFull> = {
bladeburner: {
skillMaxUpgradeCount: 0,
},
dnet: {
getAuthenticateTime: 0,
getHeartbleedTime: 0,
getExpectedRamBlockRemoved: 0,
},
},
} as const;

View File

@@ -1,3 +1,5 @@
import { exampleDarknetServerData } from "../DarkNet/Enums";
import type { DarknetServerData } from "@nsdefs";
import type { NetscriptContext } from "./APIWrapper";
import { errorMessage } from "./ErrorMessages";
@@ -55,3 +57,20 @@ export function assertFunctionWithNSContext(
): asserts v is () => void {
if (typeof v !== "function") throw errorMessage(ctx, `${argName} expected to be a function ${debugType(v)}`, "TYPE");
}
export function missingKey(expect: object, actual: unknown): string | false {
if (typeof actual !== "object" || actual === null) {
return `Expected to be an object, was ${actual === null ? "null" : typeof actual}.`;
}
for (const key in expect) {
if (!(key in actual)) return `Property ${key} was expected but not present.`;
}
return false;
}
export function assertDarknetServerData(ctx: NetscriptContext, data: unknown): asserts data is DarknetServerData {
const error = missingKey(exampleDarknetServerData, data);
if (error) {
throw errorMessage(ctx, `Invalid darknet server data.\n${error}`, "TYPE");
}
}

View File

@@ -12,6 +12,7 @@ import { ITutorial } from "../InteractiveTutorial";
import { AlertEvents } from "../ui/React/AlertManager";
import { handleUnknownError } from "../utils/ErrorHandler";
import { roundToTwo } from "../utils/helpers/roundToTwo";
import { BaseServer } from "../Server/BaseServer";
export function killWorkerScript(ws: WorkerScript): boolean {
if (ITutorial.isRunning) {
@@ -35,15 +36,30 @@ export function killWorkerScriptByPid(pid: number, killer?: WorkerScript): boole
}
export const killAllScripts = () => {
for (const server of GetAllServers()) {
for (const byPid of server.runningScriptMap.values()) {
for (const pid of byPid.keys()) {
killWorkerScriptByPid(pid);
}
for (const server of GetAllServers(true)) {
killServerScripts(server, "Script killed.");
}
};
export const killServerScripts = (server: BaseServer, message: string) => {
const scripts = server.runningScriptMap.values();
for (const byPid of scripts) {
for (const runningScript of byPid.values()) {
killWorkerScriptWithMessage(runningScript.pid, message);
}
}
};
function killWorkerScriptWithMessage(pid: number, message: string): boolean {
const ws = workerScripts.get(pid);
if (ws) {
ws.log("", () => message);
stopAndCleanUpWorkerScript(ws);
return true;
}
return false;
}
function stopAndCleanUpWorkerScript(ws: WorkerScript): void {
// Only clean up once.
// Important: Only this function can set stopFlag!

View File

@@ -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 { NS, RecentScript, ProcessInfo, NSEnums } from "@nsdefs";
import type { NS, RecentScript, ProcessInfo, NSEnums, Server as NSInterfaceServer, DarknetServerData } from "@nsdefs";
import { NetscriptSingularity } from "./NetscriptFunctions/Singularity";
import { NetscriptCloud } from "./NetscriptFunctions/Cloud";
@@ -102,13 +102,18 @@ 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 { canAccessBitNodeFeature } from "./BitNode/BitNodeUtils";
import { validBitNodes } from "./BitNode/Constants";
import { isIPAddress } from "./Types/strings";
import { compile } from "./NetscriptJSEvaluator";
import { Script } from "./Script/Script";
import { NetscriptFormat } from "./NetscriptFunctions/Format";
import { DarknetState } from "./DarkNet/models/DarknetState";
import { expectAuthenticated, hasExecConnection } from "./DarkNet/effects/offlineServerHandling";
import { DarknetServer } from "./Server/DarknetServer";
import { FragmentTypeEnum } from "./CotMG/FragmentType";
import { exampleDarknetServerData, ResponseCodeEnum } from "./DarkNet/Enums";
import { renderToStaticMarkup } from "react-dom/server";
import { Literatures } from "./Literature/Literatures";
import { Messages } from "./Message/MessageHelpers";
@@ -131,6 +136,7 @@ export const enums: NSEnums = {
BladeburnerActionType,
SpecialBladeburnerActionTypeForSleeve,
FragmentType: FragmentTypeEnum,
DarknetResponseCode: ResponseCodeEnum,
};
for (const val of Object.values(enums)) Object.freeze(val);
Object.freeze(enums);
@@ -142,6 +148,7 @@ export const ns: InternalAPI<NSFull> = {
format: NetscriptFormat(),
gang: NetscriptGang(),
go: NetscriptGo(),
dnet: NetscriptDarknet(),
bladeburner: NetscriptBladeburner(),
codingcontract: NetscriptCodingContract(),
sleeve: NetscriptSleeve(),
@@ -174,16 +181,12 @@ export const ns: InternalAPI<NSFull> = {
const out: string[] = [];
for (let i = 0; i < server.serversOnNetwork.length; i++) {
const s = getServerOnNetwork(server, i);
if (s === null) continue;
if (s === null || s instanceof DarknetServer) continue;
const entry = helpers.returnServerID(s, returnOpts);
if (entry === null) continue;
out.push(entry);
}
helpers.log(
ctx,
() =>
`returned ${server.serversOnNetwork.length} connections for ${isIPAddress(host) ? server.ip : server.hostname}`,
);
helpers.log(ctx, () => `returned ${out.length} connections for ${isIPAddress(host) ? server.ip : server.hostname}`);
return out;
},
hasTorRouter: () => () => Player.hasTorRouter(),
@@ -636,7 +639,7 @@ export const ns: InternalAPI<NSFull> = {
run:
(ctx) =>
(_scriptname, _thread_or_opt = 1, ..._args) => {
const path = helpers.scriptPath(ctx, "scriptname", _scriptname);
const path = helpers.scriptPath(ctx, "scriptname", _scriptname, true);
const runOpts = helpers.runOptions(ctx, _thread_or_opt);
const args = helpers.scriptArgs(ctx, _args);
const scriptServer = ctx.workerScript.getServer();
@@ -646,17 +649,30 @@ export const ns: InternalAPI<NSFull> = {
exec:
(ctx) =>
(_scriptname, _host, _thread_or_opt = 1, ..._args) => {
const path = helpers.scriptPath(ctx, "scriptname", _scriptname);
const path = helpers.scriptPath(ctx, "scriptname", _scriptname, true);
const host = helpers.string(ctx, "host", _host);
const runOpts = helpers.runOptions(ctx, _thread_or_opt);
const args = helpers.scriptArgs(ctx, _args);
if (DarknetState.offlineServers.includes(host)) {
helpers.log(ctx, () => `Script execution failed, because ${host} is offline.`);
return 0;
}
const server = helpers.getServer(ctx, host);
if (server instanceof DarknetServer && !hasExecConnection(ctx, server)) {
const currentHostname = ctx.workerScript.getServer().hostname;
helpers.log(
ctx,
() =>
`The current server ${currentHostname} is not connected to ${host}. exec() to a password-protected server requires a direct connection, a stasis link, or a backdoor. Use exec() from an adjacent server, or set a stasis link on the target server.`,
);
return 0;
}
return runScriptFromScript("exec", server, path, args, ctx.workerScript, runOpts);
},
spawn:
(ctx) =>
(_scriptname, _thread_or_opt = 1, ..._args) => {
const path = helpers.scriptPath(ctx, "scriptname", _scriptname);
const path = helpers.scriptPath(ctx, "scriptname", _scriptname, true);
const runOpts = helpers.spawnOptions(ctx, _thread_or_opt);
const args = helpers.scriptArgs(ctx, _args);
const spawnCb = () => {
@@ -748,7 +764,6 @@ export const ns: InternalAPI<NSFull> = {
const host = helpers.string(ctx, "host", _host);
const safetyGuard = !!_safetyGuard;
const server = helpers.getServer(ctx, host);
let scriptsKilled = 0;
for (const byPid of server.runningScriptMap.values()) {
@@ -770,8 +785,19 @@ export const ns: InternalAPI<NSFull> = {
scp: (ctx) => (_files, _destination, _source) => {
const destination = helpers.string(ctx, "destination", _destination);
const source = helpers.string(ctx, "source", _source ?? ctx.workerScript.hostname);
const destServer = helpers.getServer(ctx, destination);
if (DarknetState.offlineServers.includes(destination)) {
helpers.log(ctx, () => `scp failed, because ${destination} is offline.`);
return false;
}
if (DarknetState.offlineServers.includes(source)) {
helpers.log(ctx, () => `scp failed, because ${source} is offline.`);
return false;
}
const sourceServer = helpers.getServer(ctx, source);
const destServer = helpers.getServer(ctx, destination);
if (destServer instanceof DarknetServer) {
expectAuthenticated(ctx, destServer);
}
const files = Array.isArray(_files) ? _files : [_files];
const lits: FilePath[] = [];
const contentFiles: ContentFilePath[] = [];
@@ -830,10 +856,15 @@ export const ns: InternalAPI<NSFull> = {
ls: (ctx) => (_host, _substring) => {
const host = helpers.string(ctx, "host", _host);
const substring = helpers.string(ctx, "substring", _substring ?? "");
if (DarknetState.offlineServers.includes(host)) {
helpers.log(ctx, () => `ls failed, because ${host} is offline.`);
return [];
}
const server = helpers.getServer(ctx, host);
const allFilenames = [
...server.contracts.map((contract) => contract.fn),
...(server instanceof DarknetServer ? server.caches : []),
...server.messages,
...server.programs,
...server.scripts.keys(),
@@ -917,7 +948,50 @@ export const ns: InternalAPI<NSFull> = {
},
getServer: (ctx) => (_host) => {
const host = helpers.string(ctx, "host", _host ?? ctx.workerScript.hostname);
const server = helpers.getServer(ctx, host);
const server = GetServer(host);
// If the target server does not exist
if (!server) {
if (DarknetState.offlineServers.includes(host)) {
// If the server is offline, return a dummy object with isOnline = false.
helpers.log(ctx, () => `Server ${host} is offline.`);
return {
isOnline: false,
...exampleDarknetServerData,
hostname: host,
} satisfies DarknetServerData & { isOnline: boolean };
} else {
// Throw, otherwise.
throw helpers.errorMessage(ctx, `Server ${host} does not exist.`);
}
}
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 {
hostname: server.hostname,
ip: server.ip,
@@ -943,7 +1017,7 @@ export const ns: InternalAPI<NSFull> = {
openPortCount: server.openPortCount,
requiredHackingSkill: server.requiredHackingSkill,
serverGrowth: server.serverGrowth,
};
} satisfies NSInterfaceServer;
},
getServerMoneyAvailable: (ctx) => (_host) => {
const host = helpers.string(ctx, "host", _host);
@@ -1018,7 +1092,7 @@ export const ns: InternalAPI<NSFull> = {
serverExists: (ctx) => (_host) => {
const host = helpers.string(ctx, "host", _host);
const server = GetServer(host);
return server !== null && server.serversOnNetwork.length > 0;
return server !== null && (server.serversOnNetwork.length > 0 || server instanceof DarknetServer);
},
fileExists: (ctx) => (_filename, _host) => {
const filename = helpers.string(ctx, "filename", _filename);

View File

@@ -0,0 +1,740 @@
import type { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper";
import type { Darknet as DarknetAPI, DarknetResult } from "@nsdefs";
import { helpers } from "../Netscript/NetscriptHelpers";
import {
calculateAuthenticationTime,
calculatePasswordAttemptChaGain,
chargeServerMigration,
getBackdoorAuthTimeDebuff,
getSetStasisLinkDuration,
getStasisLinkLimit,
setStasisLink,
} from "../DarkNet/effects/effects";
import { Player } from "@player";
import { formatNumber } from "../ui/formatNumber";
import { GetServer } from "../Server/AllServers";
import { capturePackets } from "../DarkNet/models/packetSniffing";
import { addSessionToServer, DarknetState, getServerState } from "../DarkNet/models/DarknetState";
import { getStockFromSymbol } from "./StockMarket";
import { CompletedProgramName } from "@enums";
import { handleStormSeed } from "../DarkNet/effects/webstorm";
import { getPasswordType } from "../DarkNet/controllers/ServerGenerator";
import { checkPassword, getAuthResult, isAuthenticated } from "../DarkNet/effects/authentication";
import {
getLabMaze,
getLabyrinthDetails,
getLabyrinthLocationReport,
getSurroundingsVisualized,
isLabyrinthServer,
} from "../DarkNet/effects/labyrinth";
import { getPhishingAttackSpeed, handlePhishingAttack } from "../DarkNet/effects/phishing";
import { handleRamBlockRemoved } from "../DarkNet/effects/ramblock";
import {
expectDarknetAccess,
expectRunningOnDarknetServer,
getFailureResult,
getTimeoutChance,
isDirectConnected,
logger,
} from "../DarkNet/effects/offlineServerHandling";
import { DarknetServer } from "../Server/DarknetServer";
import { GenericResponseMessage, ResponseCodeEnum } from "../DarkNet/Enums";
import { getRewardFromCache } from "../DarkNet/effects/cacheFiles";
import { CONSTANTS } from "../Constants";
import { getStasisLinkServers } from "../DarkNet/utils/darknetNetworkUtils";
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 { shuffle } from "lodash";
type CompleteHeartbleedOptions = {
peek: boolean;
logsToCapture: number;
additionalMsec: number;
};
function heartbleedOptions(ctx: NetscriptContext, opts: unknown): CompleteHeartbleedOptions {
const defaults = {
peek: false,
logsToCapture: 1,
additionalMsec: 0,
};
if (opts == null) {
return defaults;
}
if (typeof opts !== "object") {
throw helpers.errorMessage(ctx, `Invalid arguments: "options" is not an object`);
}
const options = {
...defaults,
...opts,
};
const peek = helpers.boolean(ctx, "options.peek", options.peek);
const logsToCapture = helpers.positiveInteger(ctx, "options.logsToCapture", options.logsToCapture);
if (logsToCapture > 8) {
throw helpers.errorMessage(
ctx,
`Invalid arguments: "options.logsToCapture" (${options.logsToCapture}) must be smaller than or equal to 8`,
);
}
const additionalMsec = helpers.integer(ctx, "options.additionalMsec", options.additionalMsec);
if (additionalMsec < 0) {
throw helpers.errorMessage(
ctx,
`Invalid arguments: "options.additionalMsec" (${options.additionalMsec}) must be a non-negative integer`,
);
}
return {
peek,
logsToCapture,
additionalMsec,
};
}
export function NetscriptDarknet(): InternalAPI<DarknetAPI> {
return {
authenticate:
(ctx: NetscriptContext) =>
(_host, _password, _additionalMsec): Promise<DarknetResult> => {
const targetHost = helpers.string(ctx, "host", _host);
const password = helpers.string(ctx, "password", _password);
const additionalMsec = helpers.number(ctx, "additionalMsec", _additionalMsec ?? 0);
if (additionalMsec < 0) {
throw helpers.errorMessage(ctx, `Invalid arguments: "additionalMsec" is not a positive integer`);
}
if (password.length > MAX_PASSWORD_LENGTH * 2) {
// No password will ever be this long, and this prevents extremely long password attempts from causing performance issues,
// or feedback loops where longer and longer passwords are attempted due to player script bugs.
throw helpers.errorMessage(
ctx,
`Invalid arguments: "password" is too long. Attempted length: ${
password.length
}. Attempted password starts with ${password.slice(0, 100)} `,
);
}
const onlineConnectionCheck = getFailureResult(ctx, targetHost, {
requireDirectConnection: true,
});
if (!onlineConnectionCheck.success) {
return helpers.netscriptDelay(ctx, 100).then(() => ({
success: false,
code: onlineConnectionCheck.code,
message: onlineConnectionCheck.message,
}));
}
const server = onlineConnectionCheck.server;
const threads = ctx.workerScript.scriptRef.threads;
const networkDelay = calculateAuthenticationTime(server, Player, threads, password) + additionalMsec;
logger(ctx)(
`Connecting to ${server.hostname} with password '${password}'... (Est: ${formatNumber(
networkDelay / 1000,
1,
)}s)`,
);
return helpers.netscriptDelay(ctx, networkDelay).then(() => {
const onlineConnectionCheck = getFailureResult(ctx, targetHost, { requireDirectConnection: true });
if (!onlineConnectionCheck.success) {
return helpers.netscriptDelay(ctx, 100).then(() => ({
success: false,
code: onlineConnectionCheck.code,
message: onlineConnectionCheck.message,
}));
}
const server = onlineConnectionCheck.server;
// Authentication has a chance to timeout based on darknet instability
if (Math.random() < getTimeoutChance()) {
logger(ctx)(`Authentication to ${server.hostname} timed out due to network instability. Please try again.`);
return {
success: false,
code: ResponseCodeEnum.RequestTimeOut,
message: GenericResponseMessage.RequestTimeOut,
};
}
const authResult = getAuthResult(server, password, threads, networkDelay, ctx.workerScript.pid);
const success = authResult.result.success;
const xp = formatNumber(calculatePasswordAttemptChaGain(server, threads, success), 1);
logger(ctx)(
`Authentication on ${server.hostname} ${success ? "succeeded" : `failed. (Gained ${xp} cha xp)`}`,
);
if (isLabyrinthServer(server.hostname)) {
return {
success: success,
code: success ? ResponseCodeEnum.Success : ResponseCodeEnum.AuthFailure,
message: authResult.response.message,
data: authResult.response.data,
};
}
return {
success: success,
code: success ? ResponseCodeEnum.Success : ResponseCodeEnum.AuthFailure,
message: success ? GenericResponseMessage.Success : GenericResponseMessage.AuthFailure,
};
});
},
connectToSession:
(ctx: NetscriptContext) =>
(_host, _password): DarknetResult => {
const targetHost = helpers.string(ctx, "host", _host);
const token = helpers.string(ctx, "password", _password);
if (token.length > 100) {
throw helpers.errorMessage(
ctx,
`Invalid arguments: "password" is too long. Attempted length: ${
token.length
}. Attempted password starts with ${token.slice(0, 100)} `,
);
}
const onlineConnectionCheck = getFailureResult(ctx, targetHost, {
requireAdminRights: true,
});
if (!onlineConnectionCheck.success) {
return {
success: false,
code: onlineConnectionCheck.code,
message: onlineConnectionCheck.message,
};
}
const server = onlineConnectionCheck.server;
const result = checkPassword(server, token, ctx.workerScript.scriptRef.threads, ctx.workerScript.pid);
if (result.code !== ResponseCodeEnum.Success) {
logger(ctx)(
`${server.hostname} does not recognise that password. Use ns.dnet.authenticate() to create a session.`,
);
return {
success: false,
code: ResponseCodeEnum.AuthFailure,
message: GenericResponseMessage.AuthFailure,
};
}
addSessionToServer(server, ctx.workerScript.pid);
logger(ctx)(`Authentication on ${server.hostname} succeeded.`);
return {
success: true,
code: ResponseCodeEnum.Success,
message: GenericResponseMessage.Success,
};
},
heartbleed:
(ctx: NetscriptContext) =>
(_host, _opts): Promise<DarknetResult & { logs: string[] }> => {
const targetHost = helpers.string(ctx, "host", _host ?? ctx.workerScript.hostname);
const options = heartbleedOptions(ctx, _opts);
const onlineConnectionCheck = getFailureResult(ctx, targetHost, {
requireDirectConnection: true,
});
if (!onlineConnectionCheck.success) {
return helpers.netscriptDelay(ctx, 100).then(() => ({
success: false,
code: onlineConnectionCheck.code,
message: onlineConnectionCheck.message,
logs: [],
}));
}
const server = onlineConnectionCheck.server;
const networkDelay =
calculateAuthenticationTime(server, Player, ctx.workerScript.scriptRef.threads) * 1.5 +
(options.additionalMsec ?? 0);
logger(ctx)(
`Attempting to extract data from ${server.hostname}... (Est: ${formatNumber(networkDelay / 1000, 1)}s)`,
);
if (Player.skills.charisma < server.requiredCharismaSkill) {
logger(ctx)(
`You need a higher charisma level to extract data from ${server.hostname}. (${server.requiredHackingSkill} required)`,
);
return helpers.netscriptDelay(ctx, 100).then(() => ({
success: false,
code: ResponseCodeEnum.NotEnoughCharisma,
message: GenericResponseMessage.NotEnoughCharisma,
logs: [],
}));
}
return helpers.netscriptDelay(ctx, networkDelay).then(() => {
const xpGained = Player.mults.charisma_exp * 50 * ((500 + Player.skills.charisma) / 500);
Player.gainCharismaExp(xpGained);
const onlineConnectionCheck = getFailureResult(ctx, targetHost, { requireDirectConnection: true });
if (!onlineConnectionCheck.success) {
return {
success: false,
code: onlineConnectionCheck.code,
message: onlineConnectionCheck.message,
logs: [],
};
}
const serverState = getServerState(server.hostname);
logger(ctx)(`Extracted log data from ${server.hostname}... (Gained ${formatNumber(xpGained, 1)} cha xp)`);
const capturedLogs = serverState.serverLogs.slice(0, options.logsToCapture);
if (!options.peek) {
serverState.serverLogs = serverState.serverLogs.slice(options.logsToCapture);
}
return {
success: true,
code: ResponseCodeEnum.Success,
message: GenericResponseMessage.Success,
logs: capturedLogs.map((log) =>
typeof log.message === "string" ? log.message : JSON.stringify(log.message),
),
};
});
},
openCache:
(ctx: NetscriptContext) =>
(_fileName, _suppressToast): CacheResult => {
const fileName = helpers.string(ctx, "fileName", _fileName);
const suppressToast = helpers.boolean(ctx, "suppressToast", _suppressToast ?? false);
const server = expectRunningOnDarknetServer(ctx);
expectDarknetAccess(ctx);
const path = resolveCacheFilePath(fileName);
if (!path) {
throw helpers.errorMessage(ctx, `Invalid cache file. (File must end in .cache) : ${fileName}`);
}
const hasCacheFile = server.caches.includes(path);
if (!hasCacheFile) {
throw helpers.errorMessage(ctx, `Cache file not found: ${fileName} on server ${server.hostname}`);
}
server.caches = server.caches.filter((cache) => cache !== fileName);
const result = getRewardFromCache(server, fileName, suppressToast);
logger(ctx)(`Data file ${fileName} opened. ${result.message}.`);
return result;
},
probe:
(ctx: NetscriptContext) =>
(_returnByIp): string[] => {
const returnByIP = helpers.boolean(ctx, "returnByIP", _returnByIp ?? false);
const server = ctx.workerScript.getServer();
const out = [];
for (const neighbor of server.serversOnNetwork) {
const neighborServer = GetServer(neighbor);
if (!(neighborServer instanceof DarknetServer)) {
continue;
}
const entry = helpers.returnServerID(neighborServer, { returnByIP });
if (entry) {
out.push(entry);
}
}
helpers.log(ctx, () => `Returned ${out.length} connections for ${server.hostname}`);
// The order of results is shuffled. This is to avoid clues to the network structure
// like there are in the standard network's scan results order.
return shuffle(out);
},
setStasisLink:
(ctx: NetscriptContext) =>
(_shouldLink): Promise<DarknetResult> => {
const shouldLink = helpers.boolean(ctx, "shouldLink", _shouldLink ?? true);
const targetHost = ctx.workerScript.getServer().hostname;
const onlineConnectionCheck = getFailureResult(ctx, targetHost);
if (!onlineConnectionCheck.success) {
return helpers.netscriptDelay(ctx, 100).then(() => ({
success: false,
code: onlineConnectionCheck.code,
message: onlineConnectionCheck.message,
}));
}
const server = onlineConnectionCheck.server;
const stasisLinkCount = getStasisLinkServers().length;
const stasisLinkLimit = getStasisLinkLimit();
if (shouldLink && stasisLinkCount >= stasisLinkLimit) {
helpers.log(ctx, () => `Stasis link limit reached. (${stasisLinkCount}/${stasisLinkLimit})`);
return helpers.netscriptDelay(ctx, 100).then(() => ({
success: false,
code: ResponseCodeEnum.StasisLinkLimitReached,
message: GenericResponseMessage.StasisLinkLimitReached,
}));
}
helpers.log(
ctx,
() => `Beginning stasis ${shouldLink ? "" : "removal "}procedure on ${server.hostname}... (Est: 30s)`,
);
// setStasisLink's delay is hardcoded at 30s. We should skip this delay in Jest tests.
return helpers
.netscriptDelay(ctx, getSetStasisLinkDuration())
.then(() => setStasisLink(ctx, server, shouldLink));
},
getStasisLinkLimit: (ctx: NetscriptContext) => (): number => {
const limit = getStasisLinkLimit();
logger(ctx)(`Stasis link limit: ${limit}`);
return limit;
},
getStasisLinkedServers:
(ctx: NetscriptContext) =>
(_returnByIP): string[] => {
const returnByIp = helpers.boolean(ctx, "returnByIP", _returnByIP ?? false);
const servers = getStasisLinkServers();
const serverNames = servers.map((s) => (returnByIp ? s.ip : s.hostname));
logger(ctx)(`Stasis linked servers: ${serverNames}`);
return serverNames;
},
getServerAuthDetails: (ctx) => (_host) => {
const targetHost = helpers.string(ctx, "host", _host ?? ctx.workerScript.hostname);
const onlineConnectionCheck = getFailureResult(ctx, targetHost);
if (!onlineConnectionCheck.success) {
logger(ctx)(onlineConnectionCheck.message);
return {
isOnline: false,
isConnectedToCurrentServer: false,
hasSession: false,
modelId: "",
passwordHint: "",
data: "",
logTrafficInterval: -1,
passwordLength: -1,
passwordFormat: "numeric",
} satisfies ReturnType<DarknetAPI["getServerAuthDetails"]>;
}
const targetServer = onlineConnectionCheck.server;
const localServer = ctx.workerScript.getServer();
const isConnected = isDirectConnected(localServer, targetServer);
const hasSession = isAuthenticated(targetServer, ctx.workerScript.pid);
return {
isOnline: true,
isConnectedToCurrentServer: isConnected,
hasSession,
modelId: targetServer.modelId,
passwordHint: targetServer.staticPasswordHint,
data: targetServer.passwordHintData ?? "",
logTrafficInterval: targetServer.logTrafficInterval,
passwordLength: targetServer.password.length,
passwordFormat: getPasswordType(targetServer.password),
} satisfies ReturnType<DarknetAPI["getServerAuthDetails"]>;
},
packetCapture: (ctx) => (_host) => {
const targetHost = helpers.string(ctx, "host", _host ?? ctx.workerScript.hostname);
const onlineConnectionCheck = getFailureResult(ctx, targetHost, {
requireDirectConnection: true,
});
if (!onlineConnectionCheck.success) {
return helpers.netscriptDelay(ctx, 100).then(() => ({
success: false,
code: onlineConnectionCheck.code,
message: onlineConnectionCheck.message,
data: "",
}));
}
const server = onlineConnectionCheck.server;
const networkDelay = calculateAuthenticationTime(server, Player, ctx.workerScript.scriptRef.threads) * 4;
const xp = formatNumber(calculatePasswordAttemptChaGain(server, ctx.workerScript.scriptRef.threads), 1);
logger(ctx)(`Captured some outgoing transmissions from ${server.hostname}. (Gained ${xp} cha xp)`);
return helpers.netscriptDelay(ctx, networkDelay).then(() => {
return {
success: true,
code: ResponseCodeEnum.Success,
message: GenericResponseMessage.Success,
data: capturePackets(server),
};
});
},
induceServerMigration:
(ctx) =>
(_host): Promise<DarknetResult> => {
const targetHost = helpers.string(ctx, "host", _host);
const onlineConnectionCheck = getFailureResult(ctx, targetHost, {
requireDirectConnection: true,
preventUseOnStationaryServers: true,
});
if (!onlineConnectionCheck.success) {
return helpers.netscriptDelay(ctx, 100).then(() => ({
success: false,
code: onlineConnectionCheck.code,
message: onlineConnectionCheck.message,
}));
}
const hostOfCurrentServer = !isIPAddress(targetHost)
? ctx.workerScript.hostname
: getDarknetServerOrThrow(ctx.workerScript.hostname).ip;
if (targetHost === hostOfCurrentServer) {
const message = `Cannot induce migration on a script's own server. induceServerMigration must target a neighboring connected server.`;
logger(ctx)(message);
return helpers.netscriptDelay(ctx, 100).then(() => ({
success: false,
code: ResponseCodeEnum.DirectConnectionRequired,
message: message,
}));
}
const server = onlineConnectionCheck.server;
logger(ctx)(`Inducing server migration of ${server.hostname}... (Est: 6s)`);
// induceServerMigration's delay is hardcoded at 6s. We should skip this delay in Jest tests.
return helpers.netscriptDelay(ctx, !CONSTANTS.isInTestEnvironment ? 6000 : 0).then(() => {
const onlineConnectionCheck = getFailureResult(ctx, targetHost, {
requireDirectConnection: true,
preventUseOnStationaryServers: true,
});
if (!onlineConnectionCheck.success) {
return helpers.netscriptDelay(ctx, 100).then(() => ({
success: false,
code: onlineConnectionCheck.code,
message: onlineConnectionCheck.message,
}));
}
const server = onlineConnectionCheck.server;
const currentDepth = server.depth;
const result = chargeServerMigration(server, ctx.workerScript.scriptRef.threads);
logger(ctx)(
`Induced ${formatNumber(result.chargeIncrease * 100)}%. Migration prep is now at ${formatNumber(
result.newCharge * 100,
)}%. (Gained ${formatNumber(result.xpGained)} cha xp)`,
);
if (result.newCharge >= 1 && currentDepth < server.depth) {
logger(ctx)(`${server.hostname} has been migrated!`);
}
return {
success: true,
code: ResponseCodeEnum.Success,
message: GenericResponseMessage.Success,
};
});
},
unleashStormSeed: (ctx) => (): DarknetResult => {
expectDarknetAccess(ctx);
const server = ctx.workerScript.getServer();
const hasStormSeed = server.programs.includes(CompletedProgramName.stormSeed);
if (!hasStormSeed) {
const result = `${CompletedProgramName.stormSeed} not found on ${server.hostname}`;
logger(ctx)(result);
return {
success: false,
code: ResponseCodeEnum.NotFound,
message: GenericResponseMessage.NotFound,
};
}
const result = `The webstorm has been unleashed...`;
logger(ctx)(result);
handleStormSeed(server);
return {
success: true,
code: ResponseCodeEnum.Success,
message: GenericResponseMessage.Success,
};
},
isDarknetServer: (ctx) => (_host) => {
const targetHost = helpers.string(ctx, "host", _host ?? ctx.workerScript.hostname);
const server = GetServer(targetHost);
if (!server) {
return false;
}
if (!(server instanceof DarknetServer)) {
return false;
}
return true;
},
memoryReallocation:
(ctx) =>
(_host): Promise<DarknetResult> => {
const targetHost = helpers.string(ctx, "host", _host ?? ctx.workerScript.hostname);
const onlineConnectionCheck = getFailureResult(ctx, targetHost, {
requireDirectConnection: true,
requireAdminRights: true,
});
if (!onlineConnectionCheck.success) {
return helpers.netscriptDelay(ctx, 100).then(() => ({
success: false,
code: onlineConnectionCheck.code,
message: onlineConnectionCheck.message,
}));
}
const server = onlineConnectionCheck.server;
if (server.blockedRam <= 0) {
logger(ctx)(`Server ${server.hostname} has no host-owned ram left to reallocate.`);
return helpers.netscriptDelay(ctx, 100).then(() => ({
success: false,
code: ResponseCodeEnum.NoBlockRAM,
message: GenericResponseMessage.NoBlockRAM,
}));
}
logger(ctx)(`Attempting to liberate RAM from '${server.hostname}'s owner ...`);
const delayTime = Math.max(8000 * (500 / (500 + Player.skills.charisma)), 200);
return helpers.netscriptDelay(ctx, delayTime).then(() => {
const onlineConnectionCheck = getFailureResult(ctx, targetHost, {
requireDirectConnection: true,
requireAdminRights: true,
});
if (!onlineConnectionCheck.success) {
return helpers.netscriptDelay(ctx, 100).then(() => ({
success: false,
code: onlineConnectionCheck.code,
message: onlineConnectionCheck.message,
}));
}
const server = onlineConnectionCheck.server;
if (server.blockedRam <= 0) {
logger(ctx)(`Server ${server.hostname} has no host-owned ram left to reallocate.`);
return {
success: false,
code: ResponseCodeEnum.NoBlockRAM,
message: GenericResponseMessage.NoBlockRAM,
};
}
return handleRamBlockRemoved(ctx, server);
});
},
getBlockedRam:
(ctx) =>
(_host): number => {
const targetHost = helpers.string(ctx, "host", _host ?? ctx.workerScript.hostname);
expectRunningOnDarknetServer(ctx);
const onlineConnectionCheck = getFailureResult(ctx, targetHost);
if (!onlineConnectionCheck.success) {
return 0;
}
return onlineConnectionCheck.server.blockedRam;
},
getDepth:
(ctx) =>
(_host): number => {
const targetHost = helpers.string(ctx, "host", _host ?? ctx.workerScript.hostname);
expectRunningOnDarknetServer(ctx);
const onlineConnectionCheck = getFailureResult(ctx, targetHost);
if (!onlineConnectionCheck.success) {
return -1;
}
return onlineConnectionCheck.server.depth;
},
promoteStock:
(ctx: NetscriptContext) =>
(_symbol): Promise<DarknetResult> => {
const symbol = helpers.string(ctx, "symbol", _symbol);
const stock = getStockFromSymbol(ctx, symbol);
expectRunningOnDarknetServer(ctx);
expectDarknetAccess(ctx);
const waitTime = Math.max(8000 * (600 / (600 + Player.skills.charisma)), 200);
logger(ctx)(
`Spreading ${stock.name} stock propaganda to raise volatility... (Est: ${formatNumber(waitTime / 1000, 1)}s)`,
);
return helpers.netscriptDelay(ctx, waitTime).then(() => {
const threads = ctx.workerScript.scriptRef.threads;
const promotionAmount = threads * ((500 + Player.skills.charisma) / 500);
DarknetState.stockPromotions[symbol] = (DarknetState.stockPromotions[symbol] ?? 0) + promotionAmount;
const chaXp = Player.mults.charisma_exp * threads * 10 * ((200 + Player.skills.charisma) / 200);
Player.gainCharismaExp(chaXp);
logger(ctx)(`Spread promotion for ${stock.name}. (Gained ${formatNumber(chaXp, 1)} cha xp)`);
return {
success: true,
code: ResponseCodeEnum.Success,
message: GenericResponseMessage.Success,
};
});
},
phishingAttack: (ctx: NetscriptContext) => (): Promise<DarknetResult> => {
const waitTime = getPhishingAttackSpeed();
const server = expectRunningOnDarknetServer(ctx);
expectDarknetAccess(ctx);
return helpers.netscriptDelay(ctx, waitTime).then(() => {
return handlePhishingAttack(ctx, server);
});
},
getDarknetInstability: (ctx) => () => {
expectDarknetAccess(ctx);
return {
authenticationDurationMultiplier: getBackdoorAuthTimeDebuff(),
authenticationTimeoutChance: getTimeoutChance(),
};
},
nextMutation: (ctx) => () => {
expectDarknetAccess(ctx);
return DarknetState.nextMutation;
},
getServerRequiredCharismaLevel:
(ctx) =>
(_host): number => {
const targetHost = helpers.string(ctx, "host", _host);
const onlineConnectionCheck = getFailureResult(ctx, targetHost);
if (!onlineConnectionCheck.success) {
return -1;
}
return onlineConnectionCheck.server.requiredCharismaSkill;
},
labreport: (ctx) => async () => {
expectDarknetAccess(ctx);
expectRunningOnDarknetServer(ctx);
const lab = getLabyrinthDetails().lab;
if (!lab) {
const status = "You feel lost...";
logger(ctx)(status);
return {
success: false,
message: status,
};
}
const currentServer = getDarknetServerOrThrow(ctx.workerScript.hostname);
if (!isDirectConnected(currentServer, lab)) {
const status = "You feel disconnected...";
logger(ctx)(status);
return {
success: false,
message: status,
};
}
const pid = ctx.workerScript.pid;
const authenticationTime = calculateAuthenticationTime(lab, Player, ctx.workerScript.scriptRef.threads);
await helpers.netscriptDelay(ctx, authenticationTime);
return getLabyrinthLocationReport(pid);
},
labradar: (ctx) => async () => {
expectDarknetAccess(ctx);
expectRunningOnDarknetServer(ctx);
const lab = getLabyrinthDetails().lab;
if (!lab) {
const status = "You feel blind...";
logger(ctx)(status);
return {
success: false,
message: status,
};
}
const currentServer = getDarknetServerOrThrow(ctx.workerScript.hostname);
if (!isDirectConnected(currentServer, lab)) {
const status = "You feel disconnected...";
logger(ctx)(status);
return {
success: false,
message: status,
};
}
const pid = ctx.workerScript.pid;
const authenticationTime = calculateAuthenticationTime(lab, Player, ctx.workerScript.scriptRef.threads);
await helpers.netscriptDelay(ctx, authenticationTime);
const [x, y] = DarknetState.labLocations[pid] ?? [1, 1];
return {
success: true,
message: getSurroundingsVisualized(getLabMaze(), x, y, 3, true, true),
};
},
};
}

View File

@@ -53,6 +53,9 @@ import { Skills } from "../Bladeburner/data/Skills";
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 { getRamBlockRemoved } from "../DarkNet/effects/ramblock";
export function NetscriptFormulas(): InternalAPI<IFormulas> {
const checkFormulasAccess = function (ctx: NetscriptContext): void {
@@ -466,6 +469,32 @@ export function NetscriptFormulas(): InternalAPI<IFormulas> {
return skill.calculateMaxUpgradeCount(level, skillPoints as PositiveNumber);
},
},
dnet: {
getAuthenticateTime:
(ctx) =>
(_darknetServerData, _threads, _player): number => {
assertDarknetServerData(ctx, _darknetServerData);
const threads = helpers.number(ctx, "threads", _threads ?? 1);
const person = helpers.person(ctx, _player ?? Player);
return calculateAuthenticationTime(_darknetServerData, person, threads);
},
getHeartbleedTime:
(ctx) =>
(_darknetServerData, _threads, _player): number => {
assertDarknetServerData(ctx, _darknetServerData);
const threads = helpers.number(ctx, "threads", _threads ?? 1);
const person = helpers.person(ctx, _player ?? Player);
return calculateAuthenticationTime(_darknetServerData, person, threads) * 1.5;
},
getExpectedRamBlockRemoved:
(ctx) =>
(_darknetServerData, _threads, _person): number => {
assertDarknetServerData(ctx, _darknetServerData);
const threads = helpers.number(ctx, "threads", _threads ?? 1);
const person = helpers.person(ctx, _person ?? Player);
return getRamBlockRemoved(_darknetServerData, threads, person);
},
},
};
// Removed functions

View File

@@ -23,7 +23,7 @@ import { Companies } from "../Company/Companies";
import { Factions } from "../Faction/Factions";
import { helpers } from "../Netscript/NetscriptHelpers";
import { convertTimeMsToTimeElapsedString } from "../utils/StringHelperFunctions";
import { getServerOnNetwork } from "../Server/ServerHelpers";
import { getServerOnNetwork, getTorRouter } from "../Server/ServerHelpers";
import { Terminal } from "../Terminal";
import { calculateHackingTime } from "../Hacking";
import { Server } from "../Server/Server";
@@ -52,6 +52,8 @@ import { validBitNodes } from "../BitNode/Constants";
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
import { cat } from "../Terminal/commands/cat";
import { Crimes } from "../Crime/Crimes";
import { DarknetServer } from "../Server/DarknetServer";
import { populateDarknet } from "../DarkNet/controllers/NetworkGenerator";
export function NetscriptSingularity(): InternalAPI<ISingularity> {
const runAfterReset = function (cbScript: ScriptFilePath) {
@@ -407,11 +409,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
}
Player.loseMoney(CONSTANTS.TorRouterCost, "other");
const darkweb = GetServer(SpecialServers.DarkWeb);
if (!darkweb) throw helpers.errorMessage(ctx, "DarkWeb was not a server but should have been");
Player.getHomeComputer().serversOnNetwork.push(darkweb.hostname);
darkweb.serversOnNetwork.push(Player.getHomeComputer().hostname);
getTorRouter();
Player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain / 500);
helpers.log(ctx, () => "You have purchased a Tor router!");
return true;
@@ -453,6 +451,11 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
() => `You have purchased the '${item.program}' program. The new program can be found on your home computer.`,
);
Player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain / 5000);
if (item.program === CompletedProgramName.darkscape) {
populateDarknet();
}
return true;
},
getCurrentServer: (ctx) => (_returnOpts) => {
@@ -517,16 +520,18 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
installBackdoor: (ctx) => async (): Promise<void> => {
helpers.checkSingularityAccess(ctx);
const baseserver = Player.getCurrentServer();
if (!(baseserver instanceof Server)) {
if (!(baseserver instanceof Server || baseserver instanceof DarknetServer)) {
throw helpers.errorMessage(ctx, "Cannot backdoor this kind of server.");
}
const server = baseserver;
const installTime = (calculateHackingTime(server, Player) / 4) * 1000;
// No root access or skill level too low
const canHack = netscriptCanHack(server, "backdoor");
if (!canHack.res) {
throw helpers.errorMessage(ctx, canHack.msg || "");
if (server instanceof Server) {
// No root access or skill level too low
const canHack = netscriptCanHack(server, "backdoor");
if (!canHack.res) {
throw helpers.errorMessage(ctx, canHack.msg || "");
}
}
if (server.backdoorInstalled) {

View File

@@ -24,6 +24,7 @@ import { helpers } from "../Netscript/NetscriptHelpers";
import { StockMarketConstants } from "../StockMarket/data/Constants";
import { getEnumHelper } from "../utils/EnumHelper";
import { CONSTANTS } from "../Constants";
import { getDarknetVolatilityMult } from "../DarkNet/effects/effects";
export function NetscriptStockMarket(): InternalAPI<StockAPI> {
/** Checks if the player has TIX API access. Throws an error if the player does not */
@@ -230,8 +231,9 @@ export function NetscriptStockMarket(): InternalAPI<StockAPI> {
throw helpers.errorMessage(ctx, "You don't have 4S Market Data TIX API Access!");
}
const stock = getStockFromSymbol(ctx, symbol);
const volatility = stock.mv * getDarknetVolatilityMult(symbol);
return stock.mv / 100; // Convert from percentage to decimal
return volatility / 100; // Convert from percentage to decimal
},
getForecast: (ctx) => (_symbol) => {
const symbol = helpers.string(ctx, "symbol", _symbol);
@@ -351,3 +353,12 @@ export function NetscriptStockMarket(): InternalAPI<StockAPI> {
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;
};

View File

@@ -33,6 +33,7 @@ import { Player } from "@player";
import { UIEventEmitter, UIEventType } from "./ui/UIEventEmitter";
import { getErrorMessageWithStackAndCause } from "./utils/ErrorHelper";
import { exceptionAlert } from "./utils/helpers/exceptionAlert";
import { DarknetServer } from "./Server/DarknetServer";
export const NetscriptPorts = new Map<PortNumber, Port>();
@@ -237,9 +238,9 @@ export function loadAllRunningScripts(): void {
Terminal.warn("Skipped loading player scripts during startup");
console.info("Skipping the load of any scripts during startup");
}
for (const server of GetAllServers()) {
// Reset each server's RAM usage to 0
server.ramUsed = 0;
for (const server of GetAllServers(true)) {
// Reset each server's RAM usage
server.updateRamUsed(roundToTwo(server instanceof DarknetServer ? server.blockedRam : 0));
const rsList = server.savedScripts;
server.savedScripts = undefined;

View File

@@ -0,0 +1,17 @@
import { Directory } from "./Directory";
import { FilePath, resolveFilePath } from "./FilePath";
/** Filepath with the additional constraint of having a .cache extension */
type WithCacheExtension = string & { __fileType: "Cache" };
export type CacheFilePath = FilePath & WithCacheExtension;
/** Check extension only */
export function hasCacheExtension(path: string): path is WithCacheExtension {
return path.endsWith(".cache");
}
/** Sanitize a player input, resolve any relative paths, and for imports add the correct extension if missing */
export function resolveCacheFilePath(path: string, base = "" as FilePath | Directory): CacheFilePath | null {
const result = resolveFilePath(path, base);
return result && hasCacheExtension(result) ? result : null;
}

View File

@@ -30,6 +30,8 @@ const invalidCharacters = ["/", "*", "?", "[", "]", "!", "\\", "~", "|", "#", '"
/** A valid character is any character that is not one of the invalid characters */
export const oneValidCharacter = `[^${escapeRegExp(invalidCharacters.join(""))}\\s]`;
export const oneInvalidCharacter = `[${escapeRegExp(invalidCharacters.join(""))}\\s]`;
/** Regex string for matching the directory part of a valid filepath */
export const directoryRegexString = `^(?<directory>(?:${oneValidCharacter}+\\/)*)`;

View File

@@ -3,6 +3,7 @@ import { currentNodeMults } from "../../BitNode/BitNodeMultipliers";
import { calculateCurrentShareBonus } from "../../NetworkShare/Share";
import { Person as IPerson } from "@nsdefs";
import { calculateIntelligenceBonus } from "./intelligence";
import { Player } from "@player";
function mult(favor: number): number {
let favorMult = 1 + favor / 100;
@@ -14,7 +15,7 @@ function mult(favor: number): number {
export function getHackingWorkRepGain(p: IPerson, favor: number): number {
return (
((p.skills.hacking + p.skills.intelligence / 3) / CONSTANTS.MaxSkillLevel) *
((p.skills.hacking + p.skills.intelligence + getDarknetCharismaBonus(p, 0.15) / 3) / CONSTANTS.MaxSkillLevel) *
p.mults.faction_rep *
calculateIntelligenceBonus(p.skills.intelligence, 1) *
mult(favor) *
@@ -29,7 +30,7 @@ export function getFactionSecurityWorkRepGain(p: IPerson, favor: number): number
p.skills.defense +
p.skills.dexterity +
p.skills.agility +
(p.skills.hacking + p.skills.intelligence) * calculateCurrentShareBonus())) /
(p.skills.hacking + p.skills.intelligence + getDarknetCharismaBonus(p, 0.3)) * calculateCurrentShareBonus())) /
CONSTANTS.MaxSkillLevel /
4.5;
return t * p.mults.faction_rep * mult(favor) * calculateIntelligenceBonus(p.skills.intelligence, 1);
@@ -43,8 +44,15 @@ export function getFactionFieldWorkRepGain(p: IPerson, favor: number): number {
p.skills.dexterity +
p.skills.agility +
p.skills.charisma +
(p.skills.hacking + p.skills.intelligence) * calculateCurrentShareBonus())) /
(p.skills.hacking + p.skills.intelligence + getDarknetCharismaBonus(p, 0.3)) * calculateCurrentShareBonus())) /
CONSTANTS.MaxSkillLevel /
5.5;
return t * p.mults.faction_rep * mult(favor) * calculateIntelligenceBonus(p.skills.intelligence, 1);
}
function getDarknetCharismaBonus(p: IPerson, scalar: number = 1): number {
if (Player.sourceFileLvl(15) >= 3) {
return p.skills.charisma * scalar;
}
return 0;
}

View File

@@ -11,8 +11,8 @@ import { Player } from "@player";
import { recentScripts } from "./Netscript/RecentScripts";
import { resetPidCounter } from "./Netscript/Pid";
import { GetServer, AddToAllServers, initForeignServers, prestigeAllServers } from "./Server/AllServers";
import { prestigeHomeComputer } from "./Server/ServerHelpers";
import { GetServer, AddToAllServers, prestigeAllServers } from "./Server/AllServers";
import { initForeignServers, prestigeHomeComputer } from "./Server/ServerHelpers";
import { SpecialServers } from "./Server/data/SpecialServers";
import { canAccessStockMarket, deleteStockMarket, initStockMarket } from "./StockMarket/StockMarket";
import { Terminal } from "./Terminal";
@@ -30,7 +30,9 @@ import { calculateExp } from "./PersonObjects/formulas/skill";
import { currentNodeMults } from "./BitNode/BitNodeMultipliers";
import { canAccessBitNodeFeature } from "./BitNode/BitNodeUtils";
import { pendingUIShareJobIds } from "./NetworkShare/Share";
import { getDarkscapeNavigator } from "./DarkNet/effects/effects";
import { CodingContractEventEmitter } from "./CodingContract/CodingContractEventEmitter";
import { showLiterature } from "./Literature/LiteratureHelpers";
const BitNode8StartingMoney = 250e6;
function delayedDialog(message: string, canBeDismissedEasily = true) {
@@ -95,6 +97,10 @@ export function prestigeAugmentation(): void {
// Re-create foreign servers
initForeignServers(Player.getHomeComputer());
if (canAccessBitNodeFeature(15)) {
getDarkscapeNavigator();
}
// Gain favor for Companies and Factions
for (const company of Object.values(Companies)) company.prestigeAugmentation();
for (const faction of Object.values(Factions)) faction.prestigeAugmentation();
@@ -341,6 +347,13 @@ export function prestigeSourceFile(isFlume: boolean): void {
}
staneksGift.prestigeSourceFile();
if (Player.bitNodeN === 15 && !homeComp.messages.includes(LiteratureName.DarknetHandbook)) {
homeComp.messages.push(LiteratureName.DarknetHandbook);
}
if (Player.bitNodeN === 15 && Player.sourceFileLvl(15) === 0) {
showLiterature(LiteratureName.DarknetHandbook);
}
// Gain int exp
if (Player.activeSourceFileLvl(5) !== 0 && !isFlume) {
Player.gainIntelligenceExp(300);

View File

@@ -12,4 +12,6 @@ export enum CompletedProgramName {
formulas = "Formulas.exe",
bitFlume = "b1t_flum3.exe",
flight = "fl1ght.exe",
darkscape = "DarkscapeNavigator.exe",
stormSeed = "STORM_SEED.exe",
}

View File

@@ -12,16 +12,19 @@ interface ProgramConstructorParams {
name: CompletedProgramName;
create: IProgramCreate | null;
run: (args: string[], server: BaseServer) => void;
nsMethod?: string;
}
export class Program {
name: ProgramFilePath & CompletedProgramName;
create: IProgramCreate | null;
run: (args: string[], server: BaseServer) => void;
nsMethod?: string;
constructor({ name, create, run }: ProgramConstructorParams) {
constructor({ name, create, run, nsMethod }: ProgramConstructorParams) {
this.name = asProgramFilePath(name);
this.create = create;
this.run = run;
this.nsMethod = nsMethod ?? "";
}
}

View File

@@ -14,6 +14,7 @@ import { CompletedProgramName, FactionName } from "@enums";
import { Router } from "../ui/GameRoot";
import { Page } from "../ui/Router";
import { knowAboutBitverse } from "../BitNode/BitNodeUtils";
import { handleStormSeed } from "../DarkNet/effects/webstorm";
import { clampNumber } from "../utils/helpers/clampNumber";
function requireHackingLevel(lvl: number) {
@@ -46,6 +47,7 @@ function warnIfNonArgProgramIsRunWithArgs(name: CompletedProgramName, args: stri
export const Programs: Record<CompletedProgramName, Program> = {
[CompletedProgramName.nuke]: new Program({
name: CompletedProgramName.nuke,
nsMethod: "nuke",
create: {
level: 1,
tooltip: "This virus is used to gain root access to a machine if enough ports are opened.",
@@ -75,6 +77,7 @@ export const Programs: Record<CompletedProgramName, Program> = {
}),
[CompletedProgramName.bruteSsh]: new Program({
name: CompletedProgramName.bruteSsh,
nsMethod: "brutessh",
create: {
level: 50,
tooltip: "This program executes a brute force attack that opens SSH ports",
@@ -99,6 +102,7 @@ export const Programs: Record<CompletedProgramName, Program> = {
}),
[CompletedProgramName.ftpCrack]: new Program({
name: CompletedProgramName.ftpCrack,
nsMethod: "ftpcrack",
create: {
level: 100,
tooltip: "This program cracks open FTP ports",
@@ -123,6 +127,7 @@ export const Programs: Record<CompletedProgramName, Program> = {
}),
[CompletedProgramName.relaySmtp]: new Program({
name: CompletedProgramName.relaySmtp,
nsMethod: "relaysmtp",
create: {
level: 250,
tooltip: "This program opens SMTP ports by redirecting data",
@@ -147,6 +152,7 @@ export const Programs: Record<CompletedProgramName, Program> = {
}),
[CompletedProgramName.httpWorm]: new Program({
name: CompletedProgramName.httpWorm,
nsMethod: "httpworm",
create: {
level: 500,
tooltip: "This virus opens up HTTP ports",
@@ -171,6 +177,7 @@ export const Programs: Record<CompletedProgramName, Program> = {
}),
[CompletedProgramName.sqlInject]: new Program({
name: CompletedProgramName.sqlInject,
nsMethod: "sqlinject",
create: {
level: 750,
tooltip: "This virus opens SQL ports",
@@ -221,6 +228,7 @@ export const Programs: Record<CompletedProgramName, Program> = {
}),
[CompletedProgramName.serverProfiler]: new Program({
name: CompletedProgramName.serverProfiler,
nsMethod: "getServer",
create: {
level: 75,
tooltip: "This program is used to display hacking and Netscript-related information about servers",
@@ -343,4 +351,27 @@ export const Programs: Record<CompletedProgramName, Program> = {
Terminal.print(`-- ${FactionName.Daedalus} --`);
},
}),
[CompletedProgramName.darkscape]: new Program({
name: CompletedProgramName.darkscape,
create: null,
run: (): void => {
Terminal.print("This program gives access to the dark net.");
Terminal.print(
"The dark net is an unstable, constantly shifting network of servers that are only connected to the normal network through the darkweb server.",
);
Terminal.print(
"This network can be accessed using the `ns.dnet` api functions, or the DarkNet UI on the left-hand panel.",
);
},
}),
[CompletedProgramName.stormSeed]: new Program({
name: CompletedProgramName.stormSeed,
nsMethod: "dnet.unleashStormSeed",
create: null,
run: (): void => {
Terminal.print("You can feel a storm approaching...");
const connectedServer = Player.getCurrentServer();
handleStormSeed(connectedServer);
},
}),
};

View File

@@ -29,9 +29,12 @@ import { handleGetSaveDataInfoError } from "./utils/ErrorHandler";
import { isObject, assertObject } from "./utils/TypeAssertion";
import { evaluateVersionCompatibility } from "./utils/SaveDataMigrationUtils";
import { Reviver } from "./utils/GenericReviver";
import { populateDarknet } from "./DarkNet/controllers/NetworkGenerator";
import { getDarkNetSave, loadDarkNet } from "./DarkNet/effects/SaveLoad";
import { giveExportBonus } from "./ExportBonus";
import { loadInfiltrations } from "./Infiltration/SaveLoadInfiltration";
import { InfiltrationState } from "./Infiltration/formulas/game";
import { hasDarknetAccess } from "./DarkNet/utils/darknetAuthUtils";
/* SaveObject.js
* Defines the object used to save/load games
@@ -85,6 +88,7 @@ export type BitburnerSaveObjectType = {
LastExportBonus?: string;
StaneksGiftSave: string;
GoSave: unknown; // "loadGo" function can process unknown data
DarknetSave: unknown;
InfiltrationsSave: unknown;
};
@@ -196,6 +200,7 @@ class BitburnerSaveObject implements BitburnerSaveObjectType {
LastExportBonus = "0";
StaneksGiftSave = "";
GoSave = "";
DarknetSave = "";
InfiltrationsSave = "";
async getSaveData(forceExcludeRunningScripts = false): Promise<SaveData> {
@@ -217,6 +222,7 @@ class BitburnerSaveObject implements BitburnerSaveObjectType {
this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus);
this.StaneksGiftSave = JSON.stringify(staneksGift);
this.GoSave = JSON.stringify(getGoSave());
this.DarknetSave = JSON.stringify(getDarkNetSave());
this.InfiltrationsSave = JSON.stringify(InfiltrationState);
if (Player.gang) this.AllGangsSave = JSON.stringify(AllGangs);
@@ -468,7 +474,9 @@ async function loadGame(saveData: SaveData): Promise<boolean> {
loadCompanies(saveObj.CompaniesSave);
loadFactions(saveObj.FactionsSave, Player);
loadGo(saveObj.GoSave);
loadDarkNet(saveObj.DarknetSave);
loadInfiltrations(saveObj.InfiltrationsSave);
try {
loadAliases(saveObj.AliasesSave);
} catch (e) {
@@ -538,6 +546,11 @@ async function loadGame(saveData: SaveData): Promise<boolean> {
} else {
createNewUpdateText();
}
if (hasDarknetAccess()) {
populateDarknet();
}
return true;
}

View File

@@ -1,7 +1,7 @@
/**
* @public
*/
type _ValueOf<T> = T[keyof T];
export type _ValueOf<T> = T[keyof T];
/** @public */
type SuccessResult<T extends object> = { success: true; message?: string } & T;
@@ -3975,7 +3975,8 @@ export interface CodingContract {
*
* @param answer - Attempted solution for the contract. This can be a string formatted like submitting manually, or the answer in the format of the specific contract type.
* @param filename - Filename of the contract.
* @param host - Hostname/IP of the server containing the contract. Optional. Defaults to the server the calling script is running on.
* @param host - Hostname/IP of the server containing the contract. Optional. Defaults to current server if not
* provided.
* @returns A reward description string on success, or an empty string on failure.
*/
attempt(answer: any, filename: string, host?: string): string;
@@ -3989,7 +3990,7 @@ export interface CodingContract {
* (e.g. Find Largest Prime Factor, Total Ways to Sum, etc.)
*
* @param filename - Filename of the contract.
* @param host - Hostname/IP of the server containing the contract. Optional. Defaults to the server the calling script is running on.
* @param host - Hostname/IP of the server containing the contract. Optional. Defaults to current server if not provided.
* @returns Name describing the type of problem posed by the Coding Contract.
*/
getContractType(filename: string, host?: string): CodingContractName;
@@ -4002,7 +4003,7 @@ export interface CodingContract {
* Get the full text description for the problem posed by the Coding Contract.
*
* @param filename - Filename of the contract.
* @param host - Hostname/IP of the server containing the contract. Optional. Defaults to the server the calling script is running on.
* @param host - Hostname/IP of the server containing the contract. Optional. Defaults to current server if not provided.
* @returns Contracts text description.
*/
getDescription(filename: string, host?: string): string;
@@ -4017,7 +4018,7 @@ export interface CodingContract {
* This is just the data that the contract wants you to act on in order to solve the contract.
*
* @param filename - Filename of the contract.
* @param host - Hostname/IP of the server containing the contract. Optional. Defaults to the server the calling script is running on.
* @param host - Hostname/IP of the server containing the contract. Optional. Defaults to current server if not provided.
* @returns The specified contracts data, data type depends on contract type.
*/
getData(filename: string, host?: string): any;
@@ -4041,7 +4042,7 @@ export interface CodingContract {
* ```
*
* @param filename - Filename of the contract.
* @param host - Hostname/IP of the server containing the contract. Optional. Defaults to the server the calling script is running on.
* @param host - Hostname/IP of the server containing the contract. Optional. Default to the current server if not provided.
* @returns An object containing various data about the contract specified.
*/
getContract(filename: string, host?: string): CodingContractObject;
@@ -4054,7 +4055,7 @@ export interface CodingContract {
* Get the number of tries remaining on the contract before it self-destructs.
*
* @param filename - Filename of the contract.
* @param host - Hostname/IP of the server containing the contract. Optional. Defaults to the server the calling script is running on.
* @param host - Hostname/IP of the server containing the contract. Optional. Defaults to current server if not provided.
* @returns How many attempts are remaining for the contract.
*/
getNumTriesRemaining(filename: string, host?: string): number;
@@ -4300,6 +4301,464 @@ export interface Format {
time(milliseconds: number, milliPrecision?: boolean): string;
}
/**
* Errors:
*
* - DirectConnectionRequired: The target server is not directly connected to the current server. This may be caused
* by a user error (specifying the wrong neighbor host's hostname) or a network change (the target server was moved).
*
* - AuthFailure: Authentication failed. The password is incorrect.
*
* - NotFound: The API requires a specific resource (e.g., an exe file), but it does not exist on the server.
*
* - RequestTimeOut: The request failed (though the password may or may not have been correct). Caused by network instability.
*
* - ServiceUnavailable: The server is offline.
*
* @public
*/
type DarknetResponseCodeType = {
Success: 200;
DirectConnectionRequired: 351;
AuthFailure: 401;
Forbidden: 403;
NotFound: 404;
RequestTimeOut: 408;
NotEnoughCharisma: 451;
StasisLinkLimitReached: 453;
NoBlockRAM: 454;
PhishingFailed: 455;
ServiceUnavailable: 503;
};
/** @public */
type DarknetResponseCode = _ValueOf<DarknetResponseCodeType>;
/** @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;
message: string;
karmaLoss: number;
};
/**
* Details about a server's authentication schema
* @public
*/
type ServerAuthDetails = {
/** 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. */
modelId: string;
/** Static password reminder text set for this server. */
passwordHint: string;
/** Data from the passwordHint, if any. */
data: string;
/** The frequency (in seconds) of the server adding its own messages to its logs, visible with heartBleed(). */
logTrafficInterval: number;
/** The number of characters in the password */
passwordLength: number;
/** The character set used in the password */
passwordFormat: "numeric" | "alphabetic" | "alphanumeric" | "ASCII" | "unicode";
};
/**
* Options to change the behavior of {@link Darknet.heartbleed | heartbleed} API.
* @public
*/
type HeartbleedOptions = {
/** If true, looks at the most recent log lines but does not remove them. Default is false. */
peek?: boolean;
/** The number of log lines to remove from the server, up to a max of 8. Default is 1. Must be a positive integer. */
logsToCapture?: number;
/** The number of additional milliseconds to add to the run time of the heartbleed request. Default is 0. Must be a non-negative integer. */
additionalMsec?: number;
};
/**
* Instability of the darknet caused by excessive backdoor-ing of servers.
* @public
*/
type DarknetInstability = {
/** The increase in time that authentication takes, as a decimal */
authenticationDurationMultiplier: number;
/** The chance that authentication will time out instead of resolving, as a decimal */
authenticationTimeoutChance: number;
};
/**
* Darknet API
* @public
*/
export interface Darknet {
/**
* Sends a network request to try to authenticate on a darkweb server. The target server must be directly connected
* to the server that the script is running on. The speed of authentication scales with the number of threads used.
*
* If successful, grants the script a session, allowing it to exec() scripts on that server, or scp() files to it. (scp() *from* the server is always allowed.)
*
* @remarks
* RAM cost: 0.6 GB
*
* @param host - Hostname/IP of the target server (connected to the current server) to try a password.
* @param password - Password to attempt to authenticate with.
* @param additionalMsec - Optional. The number of additional milliseconds to add to the run time of the authentication request. Default is 0.
* @returns A promise that resolves to a {@link DarknetResult} object. The resolved object may contain an optional
* property. The type of this property is intentionaly undocumented. You are supposed to experiment and discover the
* content of this property.
*/
authenticate(host: string, password: string, additionalMsec?: number): Promise<DarknetResult & { data?: any }>;
/**
* Attempts to connect to a target darkweb server that you have previously authenticated on. Unlike `authenticate`,
* connectToSession can be used to get a session on servers at any distance.
*
* If successful, grants the script a session, allowing it to scp() files from that target. It also allows starting scripts
* with exec() on that target, if the target is directly connected to the server that the script is running on,
* or has a backdoor or stasis link.
*
* If unsuccessful, more detail may be able to be gathered by using heartbleed() to look at the resulting logs on the server.
*
* @remarks
* RAM cost: 0.05 GB
*
* @param host - Hostname/IP of the target server to connect to existing session
* @param password - The server's password, to verify the session
* @returns A {@link DarknetResult} object
*/
connectToSession(host: string, password: string): DarknetResult;
/**
* Uses an exploit to extract log data from a server by sending a malformed heartbeat request.
* Retrieves the most recent logs on the server. This can be used to get more feedback on authentication attempts.
* The retrieved logs are removed from the server, unless the "peek" flag is set to true in the provided HeartbleedOptions.
*
* Servers will periodically produce logs themselves, as well, which sometimes are useful, but most times are not.
*
* The speed of capture scales with the number of threads used. See formulas.dnet.getHeartbleedTime for more information.
*
* @remarks
* RAM cost: 0.6 GB
*
* @param host - Hostname/IP of the target server. Must be directly connected to the current server.
* @param options - Optional {@link HeartbleedOptions} to modify how the exploit works.
* @returns A promise that resolves to a {@link DarknetResult} object, plus the scraped logs.
*
*/
heartbleed(host: string, options?: HeartbleedOptions): Promise<DarknetResult & { logs: string[] }>;
/**
* Opens a .cache file on the current server to acquire its valuable contents.
*
* @remarks
* RAM cost: 2 GB
*
* @param filename - Name of the cache file to open.
* @param suppressToast - Optional. If true, suppresses the toast notification that appears when opening a cache file. Defaults to false.
* @returns An object containing the contents of the cache, and the karma lost.
*/
openCache(filename: string, suppressToast?: boolean): CacheResult;
/**
* Returns a list of all darknet servers connected to the script's current server.
* For example, if called from a script running on `home`, it will return `["darkweb"]`.
* It will return an empty list if there are no darknet servers connected to the current server.
*
* Note that there is no guarantee about the order of servers in the returned list.
*
* @remarks
* RAM cost: 0.2 GB
*
* @param returnByIP - Optional. Controls whether the function returns IPs instead of hostnames. Defaults to false.
* @returns An array of strings containing the hostnames/IPs of all servers connected to the current server.
*/
probe(returnByIP?: boolean): string[];
/**
* Applies or removes a stasis link to the script's current server. This will allow you to connectToSession() or exec() to the server remotely, even if it is
* not directly connected to the server a script is running on. It also allows direct connection to the server via the terminal.
*
* Stasis links also prevent the server from going offline or moving. It does not prevent other servers from moving or
* going offline, though, so it does not guarantee that the stasis link server will never lose connections to other servers.
*
* There is a maximum of stasis links that can be applied globally, which can be seen using getStasisLinkLimit().
* This limit can be increased by finding special augmentations in the deep darknet.
*
* @remarks
* RAM cost: 12 GB
*
* @param shouldLink - true to apply a stasis link, false to remove it. Optional. Defaults to true.
* @returns A promise that resolves to a {@link DarknetResult} object.
*/
setStasisLink(shouldLink?: boolean): Promise<DarknetResult>;
/**
* Returns the maximum number of stasis links that can be applied globally, based on the player's current status.
* Stasis link limit can be increased by finding special augmentations in the deep darknet.
*
* @remarks
* RAM cost: 0 GB
*
* @returns Maximum number of stasis links.
*/
getStasisLinkLimit(): number;
/**
* Returns the hostnames/IPs of servers that have a stasis link applied.
*
* @remarks
* RAM cost: 0 GB
*
* @param returnByIP - Optional. If true, returns IPs instead of hostnames. Defaults to false.
* @returns Hostnames/IPs
*/
getStasisLinkedServers(returnByIP?: boolean): string[];
/**
* Returns the server's authentication protocol details.
*
* @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.
*/
getServerAuthDetails(host?: string): ServerAuthDetails & { isOnline: boolean };
/**
* Spends some time listening for unsecured network traffic on an adjacent server. If you are lucky, the server password may be somewhere in all the noise.
* The target server must be directly connected to the server that the script is running on.
*
* Using multiple threads speeds up the capture process.
*
* @remarks
* RAM cost: 6 GB
*
* @param host - Hostname/IP of the server to listen to.
* @returns A promise that resolves to a {@link DarknetResult} object, plus the captured data.
*/
packetCapture(host: string): Promise<DarknetResult & { data: string }>;
/**
* Increases the chance that target connected server will move to other parts of the darknet, by overloading the connections between it and the current server.
* Cannot target the current server. Must be run from a darknet server.
*
* Effect scales with threads and charisma level.
*
* @remarks
* RAM cost: 4 GB
*
* @param host - Hostname/IP of the connected server to migrate.
* @returns A promise that resolves to a {@link DarknetResult} object.
*/
induceServerMigration(host: string): Promise<DarknetResult>;
/**
* Executes STORM_SEED.exe, if it is present on the server the script is running on.
*
* Warning: That exe file creates a webstorm that can cause catastrophic damage to the darknet. Run at your own risk.
*
* @remarks
* RAM cost: 0.1 GB
*
* @returns A promise that resolves to a {@link DarknetResult} object.
*/
unleashStormSeed(): DarknetResult;
/**
* Returns whether the server is a darknet server.
*
* Returns false if the server does not exist or has gone offline recently. This function does not DarkscapeNavigator.exe.
*
* @remarks
* RAM cost: 0.1 GB
*
* @param host - Optional. Hostname/IP for the requested server object. Defaults to the running script's server.
* @returns true if the server is a darknet server, false otherwise.
*/
isDarknetServer(host?: string): boolean;
/**
* Spends some time freeing some of the RAM currently blocked by the server owner. Must target an authenticated and
* directly connected server.
*
* The amount of ram recovered scales with charisma and the number of threads used.
*
* @remarks
* RAM cost: 1 GB
*
* @param host - Optional. Hostname/IP of the authenticated and directly connected server to free ram from. Defaults to the running script's server.
* @returns A promise that resolves to a {@link DarknetResult} object.
*/
memoryReallocation(host?: string): Promise<DarknetResult>;
/**
* Gets the amount of RAM blocked by the server owner's processes. This ram can be freed for use using memoryReallocation().
*
* @remarks
* RAM cost: 0 GB
*
* @param host - Optional. Hostname/IP of the server to check. Defaults to the running script's server.
* @returns The amount of RAM blocked by the server owner's processes.
*/
getBlockedRam(host?: string): number;
/**
* Gets the current depth of the specified server into the darknet. Servers immediately below Darkweb are depth 0, and
* each visual row in the UI below that increases the depth of the server.
*
* Returns -1 if the server is offline, not found, or not a darkweb server.
*
* @remarks
* RAM cost: 0.1 GB
*
* @param host - Optional. Hostname/IP of the server to check. Defaults to the running script's server.
* @returns The current depth of the server into the darknet.
*/
getDepth(host?: string): number;
/**
* Spends some time spreading propaganda about a stock to increase its volatility. This does not actually change the stock's forecasts, but
* a savvy investor can take advantage of the chaos. The effect scales with charisma and the number of threads used, but degrades over time if left alone.
*
* @remarks
* RAM cost: 2 GB
*
* @param sym - Stock symbol.
* @returns A promise that resolves to a {@link DarknetResult} object.
*/
promoteStock(sym: string): Promise<DarknetResult>;
/**
* Spends time sending out phishing emails, attempting to find some non-technical middle manager to fall for the scam. Builds charisma.
* Often the attempt will fail, but success can be increased with crime success rate and charisma stats.
*
* The amount of money lifted scales with the number of threads used, if successful. Very occasionally you can retrieve a cache file from the attempt.
*
* Phishing attacks can only be run from scripts on darknet servers.
*
* @remarks
* RAM cost: 2 GB
*
* @returns A promise that resolves to a {@link DarknetResult} object.
*/
phishingAttack(): Promise<DarknetResult>;
/**
* Gets the current instability of the darknet caused by excessive backdoor-ing of servers.
* @remarks
* Ram cost: 0 GB
*
* @returns An object containing the current instability values.
*/
getDarknetInstability(): DarknetInstability;
/**
* Sleep until the next mutation of the network of darknet servers (which occur frequently).
* Note that in the majority of cases, whatever changed out on the net (if anything) will not be nearby to,
* or visible from, the current server.
*
* Some possible mutations that can occur somewhere on the darknet each cycle:
*
* - Nothing changes.
*
* - Some servers move to other locations on the net, breaking existing connections and forming new ones.
*
* - Some servers go offline, which in many cases is permanent - they are effectively deleted.
*
* - Some servers restart, which kills all running scripts on the server.
*
* - New servers appear on the net (which may be previously offline servers, but cleaned and with a new password).
*
* @remarks
* RAM cost: 1 GB
*/
nextMutation(): Promise<void>;
/**
* Gets the required charisma level to target the server with dnet.heartbleed().
*
* Insufficient charisma will also cause authentication to take much longer - or, in certain servers deep
* in the darknet, be impossible.
*
* @remarks
* RAM cost: 0.1 GB
*
* @param host - Hostname/IP of the server to check.
* @returns Required charisma level of the host.
*/
getServerRequiredCharismaLevel(host: string): number;
/**
* Not all who wander are lost.
*
* @remarks
* RAM cost: 0 GB
*/
labreport(): Promise<Result<any>>;
/**
* There is more than meets the eye.
*
* @remarks
* RAM cost: 0 GB
*/
labradar(): Promise<Result<any>>;
}
/**
* Gang API
* @remarks
@@ -5949,6 +6408,35 @@ interface BladeburnerFormulas {
skillMaxUpgradeCount(name: BladeburnerSkillName, level: number, skillPoints: number): number;
}
/**
* Darknet formulas
* @public
*/
interface DarknetFormulas {
/**
* Gets the time it will take to authenticate a server.
* @param darknetServerData - 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
*/
getAuthenticateTime(darknetServerData: DarknetServerData, threads?: number, player?: Person): 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 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;
/**
* 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 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;
}
/**
* Formulas API
* @remarks
@@ -5975,6 +6463,8 @@ export interface Formulas {
work: WorkFormulas;
/** Bladeburner formulas */
bladeburner: BladeburnerFormulas;
/** Darknet formulas */
dnet: DarknetFormulas;
}
/** @public */
@@ -6209,7 +6699,7 @@ interface UserInterface {
* ns.ui.openTail("foo.js", "foodnstuff", 1, "test");
* ```
* @param fn - Optional. Filename or PID of the script being tailed. If omitted, the current script is tailed.
* @param host - Optional. Hostname/IP of the script being tailed. Defaults to the server the calling script is running on.
* @param host - Optional. Hostname/IP of the script being tailed. Defaults to the server this script is running on. If args are specified, this is not optional.
* @param args - Arguments for the script being tailed.
*/
openTail(fn?: FilenameOrPID, host?: string, ...args: ScriptArg[]): void;
@@ -6309,7 +6799,7 @@ interface UserInterface {
*
* @param pixel - Optional. The new font size in pixels. If omitted, the default tail font size is used.
* @param fn - Optional. Filename or PID of the target script. If omitted, the current script is used.
* @param host - Optional. Hostname/IP of the target script. Defaults to the server the calling script is running on.
* @param host - Optional. Hostname/IP of the target script. Defaults to the server this script is running on. If args are specified, this is not optional.
* @param args - Arguments for the target script.
*/
setTailFontSize(pixel?: number, fn?: FilenameOrPID, host?: string, ...args: ScriptArg[]): void;
@@ -6437,6 +6927,12 @@ export interface NS {
*/
readonly cloud: Cloud;
/**
* Namespace for darknet functions. Contains spoilers.
* @remarks RAM cost: 0 GB
*/
readonly dnet: Darknet;
/**
* Namespace for {@link Format | formatting} functions.
* @remarks RAM cost: 0 GB
@@ -6701,14 +7197,14 @@ export interface NS {
hackAnalyze(host: string): number;
/**
* Get the security increase for a number of hack threads.
* Get the security increase for a number of threads.
* @remarks
* RAM cost: 1 GB
*
* Returns the security increase that would occur if a hack with this many threads happened.
*
* @param threads - Amount of threads that will be used.
* @param host - Optional. Hostname/IP of the target server. If specified, the value of the "threads" parameter is limited to the number of threads needed to hack the specified server's maximum amount of money.
* @param host - Hostname/IP of the target server. The number of threads is limited to the number needed to hack the server's maximum amount of money.
* @returns The security increase.
*/
hackAnalyzeSecurity(threads: number, host?: string): number;
@@ -6770,7 +7266,7 @@ export interface NS {
* Returns the security increase that would occur if a grow with this many threads happened.
*
* @param threads - Amount of threads that will be used.
* @param host - Optional. Hostname/IP of the target server. If specified, the value of the "threads" parameter is limited to the number of threads needed to reach the specified server's maximum money.
* @param host - Optional. Hostname/IP of the target server. If provided, security increase is limited by the number of threads needed to reach maximum money.
* @param cores - Optional. The number of cores of the server that would run grow.
* @returns The security increase.
*/
@@ -7034,7 +7530,7 @@ export interface NS {
* ns.getScriptLogs("foo.js", "foodnstuff", 1, "test");
* ```
* @param fn - Optional. Filename or PID of script to get logs from.
* @param host - Optional. Hostname/IP of the server that the script is on. Defaults to the server the calling script is running on.
* @param host - Optional. Hostname/IP of the server that the script is on.
* @param args - Arguments to identify which scripts to get logs for.
* @returns Returns a string array, where each line is an element in the array. The most recently logged line is at the end of the array.
*/
@@ -7062,7 +7558,8 @@ export interface NS {
getRecentScripts(): RecentScript[];
/**
* Get the list of hostnames or IP addresses connected to a server.
* Get the list of hostnames or IP addresses connected to a server. This function does not return darknet servers
* (e.g., darkweb). Use {@link Darknet.probe | probe} if you want to list darknet servers.
* @remarks
* RAM cost: 0.2 GB
*
@@ -7120,7 +7617,7 @@ export interface NS {
* }
* ```
*
* @param host - Optional. Hostname/IP of the server to scan. Defaults to the server the calling script is running on.
* @param host - Optional. Hostname/IP of the server to scan, default to current server.
* @param returnOpts - Optional. Controls whether the function returns IPs.
* @returns Returns an array of hostnames.
*/
@@ -7286,7 +7783,7 @@ export interface NS {
* @remarks
* RAM cost: 1.3 GB
*
* Run a script as a separate process on a specified server. This is similar to the function {@link NS.run | run}
* Run a script as a separate process on a specified server. This is similar to the function {@link NS.run | run},
* except that it can be used to run a script that already exists on any server, instead of just the current server.
*
* If the script was successfully started, then this function returns the PID of that script.
@@ -7338,6 +7835,8 @@ export interface NS {
*
* Running this function with 0 or fewer threads will cause a runtime error.
*
* For password-protected servers (such as darknet servers), a session must be established with the destination server before using this function.
*
* @example
* ```js
* //The following example will execute the script foo.js with 10 threads, in 500 milliseconds and the arguments foodnstuff and 90:
@@ -7395,7 +7894,7 @@ export interface NS {
* ns.kill("foo.js", ns.getHostname(), 1, "foodnstuff", false);
* ```
* @param filename - Filename of the script to kill.
* @param host - Hostname/IP where the script to kill is running. Optional. Defaults to the server the calling script is running on.
* @param host - Hostname/IP where the script to kill is running. Defaults to the current server.
* @param args - Arguments of the script to kill.
* @returns True if the scripts were successfully killed, and false otherwise.
*/
@@ -7411,8 +7910,8 @@ export interface NS {
* true if there are any scripts running on the target server.
* If no host is defined, it will kill all scripts, where the script is running.
*
* @param host - Hostname/IP of the server on which to kill all scripts. Optional. Defaults to the server the calling script is running on.
* @param safetyGuard - Skips the script that calls this function. Optional. Defaults to false.
* @param host - Hostname/IP of the server on which to kill all scripts.
* @param safetyGuard - Skips the script that calls this function
* @returns True if any scripts were killed, and false otherwise.
*/
killall(host?: string, safetyGuard?: boolean): boolean;
@@ -7447,6 +7946,9 @@ export interface NS {
* const files = ["hack.js", "weaken.js", "grow.js"];
* ns.scp(files, server, "home");
* ```
*
* For password-protected servers (such as darknet servers), a session must be established with the destination server before using this function.
*
* @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 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 its omitted the source will be the current server.
@@ -7483,7 +7985,7 @@ export interface NS {
* ns.tprint(script.args);
* }
* ```
* @param host - Hostname/IP of the target server. Optional. Defaults to the server the calling script is running on.
* @param host - Hostname/IP of the target server. If not specified, it will be the current servers IP by default.
* @returns Array with general information about all scripts running on the specified target server.
*/
ps(host?: string): ProcessInfo[];
@@ -7572,14 +8074,18 @@ export interface NS {
getHacknetMultipliers(): HacknetMultipliers;
/**
* Returns a server object for the given server.
* 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`.
*
* @remarks
* RAM cost: 2 GB
* @param host - Optional. Hostname/IP for the requested server object. Defaults to the server the calling script is running on.
* @returns The requested server object.
*
* @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;
getServer(host?: string): Server | (DarknetServerData & { isOnline: boolean });
/**
* Get money available on a server.
@@ -7736,7 +8242,7 @@ export interface NS {
* ns.fileExists("ftpcrack.exe");
* ```
* @param filename - Filename of file to check.
* @param host - Hostname/IP of target server. Optional. Defaults to the server the calling script is running on.
* @param host - Hostname/IP of target server. Optional, defaults to the server the script is running on.
* @returns True if specified file exists, and false otherwise.
*/
fileExists(filename: string, host?: string): boolean;
@@ -7765,7 +8271,7 @@ export interface NS {
* ns.isRunning("foo.js", "joesguns", 1, 5, "test");
* ```
* @param script - Filename or PID of script to check. This is case-sensitive.
* @param host - Hostname/IP of target server. Optional. Defaults to the server the calling script is running on.
* @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.
*/
@@ -7784,7 +8290,7 @@ export interface NS {
* functions like this that check based on filename, the filename plus arguments forms the key.)
*
* @param filename - Optional. Filename or PID of the script.
* @param host - Hostname/IP of target server. Optional. Defaults to the server the calling script is running on.
* @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 The info about the running script if found, and null otherwise.
*/
@@ -7980,7 +8486,7 @@ export interface NS {
* type except message (.msg) files.
*
* @param name - Filename of file to remove. Must include the extension.
* @param host - Hostname/IP of the server on which to delete the file. Optional. Defaults to the server the calling script is running on.
* @param host - Hostname/IP of the server on which to delete the file. Optional. Defaults to current server.
* @returns True if it successfully deletes the file, and false otherwise.
*/
rm(name: string, host?: string): boolean;
@@ -8055,10 +8561,10 @@ export interface NS {
* Returns the amount of time in milliseconds it takes to execute the {@link NS.hack | hack} Netscript function on the target server.
* The required time is increased by the security level of the target server and decreased by the player's hacking level.
*
* @param host - Hostname/IP of target server. Optional. Defaults to the server the calling script is running on.
* @param host - Hostname/IP of target server.
* @returns Returns the amount of time in milliseconds it takes to execute the {@link NS.hack | hack} Netscript function.
*/
getHackTime(host?: string): number;
getHackTime(host: string): number;
/**
* Get the execution time of a grow() call.
@@ -8068,10 +8574,10 @@ export interface NS {
* Returns the amount of time in milliseconds it takes to execute the grow Netscript function on the target server.
* The required time is increased by the security level of the target server and decreased by the player's hacking level.
*
* @param host - Hostname/IP of target server. Optional. Defaults to the server the calling script is running on.
* @param host - Hostname/IP of target server.
* @returns Returns the amount of time in milliseconds it takes to execute the grow Netscript function.
*/
getGrowTime(host?: string): number;
getGrowTime(host: string): number;
/**
* Get the execution time of a weaken() call.
@@ -8081,10 +8587,10 @@ export interface NS {
* Returns the amount of time in milliseconds it takes to execute the {@link NS.weaken | weaken} Netscript function on the target server.
* The required time is increased by the security level of the target server and decreased by the player's hacking level.
*
* @param host - Hostname/IP of target server. Optional. Defaults to the server the calling script is running on.
* @param host - Hostname/IP of target server.
* @returns Returns the amount of time in milliseconds it takes to execute the {@link NS.weaken | weaken} Netscript function.
*/
getWeakenTime(host?: string): number;
getWeakenTime(host: string): number;
/**
* Get the income of all scripts.
@@ -8270,7 +8776,7 @@ export interface NS {
* ```
* @param url - URL to pull data from.
* @param target - Filename to write data to. Must be script or text file.
* @param host - Hostname/IP of server for target file. Optional. Defaults to the server the calling script is running on.
* @param host - Optional hostname/ip of server for target file.
* @returns True if the data was successfully retrieved from the URL, false otherwise.
*/
wget(url: string, target: string, host?: string): Promise<boolean>;
@@ -8679,6 +9185,7 @@ type LocationNameEnumType = {
ChongqingKuaiGongInternational: "KuaiGong International";
ChongqingSolarisSpaceSystems: "Solaris Space Systems";
ChongqingChurchOfTheMachineGod: "Church of the Machine God";
ChongqingShadowedWalkway: "Shadowed Walkway";
Sector12AlphaEnterprises: "Alpha Enterprises";
Sector12BladeIndustries: "Blade Industries";
@@ -8952,6 +9459,7 @@ type NSEnums = {
BladeburnerActionType: BladeburnerActionEnumType;
SpecialBladeburnerActionTypeForSleeve: SpecialBladeburnerActionEnumTypeForSleeve;
FragmentType: FragmentEnumType;
DarknetResponseCode: DarknetResponseCodeType;
};
/**

View File

@@ -1,26 +1,25 @@
import { Server } from "./Server";
import { BaseServer } from "./BaseServer";
import { serverMetadata } from "./data/servers";
import { HacknetServer } from "../Hacknet/HacknetServer";
import { IMinMaxRange } from "../types";
import { createRandomIp } from "../utils/IPAddress";
import { getRandomIntInclusive } from "../utils/helpers/getRandomIntInclusive";
import { Reviver } from "../utils/GenericReviver";
import { SpecialServers } from "./data/SpecialServers";
import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
import { IPAddress, isIPAddress } from "../Types/strings";
import "../Script/RunningScript"; // For reviver side-effect
import { assertObject } from "../utils/TypeAssertion";
import { DarknetServer } from "./DarknetServer";
import { applyRamBlocks } from "../DarkNet/effects/ramblock";
import { DarknetState } from "../DarkNet/models/DarknetState";
import { MAX_NET_DEPTH, NET_WIDTH } from "../DarkNet/Enums";
/**
* Map of all Servers that exist in the game
* Key (string) = IP
* Value = Server object
*/
let AllServers: Record<string, Server | HacknetServer> = {};
let AllServers: Record<string, Server | HacknetServer | DarknetServer> = {};
function GetServerByIP(ip: string): BaseServer | undefined {
for (const server of Object.values(AllServers)) {
@@ -71,9 +70,12 @@ export function GetReachableServer(s: string): BaseServer | null {
return server;
}
export function GetAllServers(): BaseServer[] {
export function GetAllServers(showDarkweb = false): BaseServer[] {
const servers: BaseServer[] = [];
for (const key of Object.keys(AllServers)) {
if (!showDarkweb && AllServers[key] instanceof DarknetServer) {
continue;
}
servers.push(AllServers[key]);
}
return servers;
@@ -89,6 +91,20 @@ export function DeleteServer(serverkey: string): void {
}
}
export const connectServers = (server1: BaseServer, server2: BaseServer) => {
if (!server1.serversOnNetwork.includes(server2.hostname)) {
server1.serversOnNetwork.push(server2.hostname);
}
if (!server2.serversOnNetwork.includes(server1.hostname)) {
server2.serversOnNetwork.push(server1.hostname);
}
};
export const disconnectServers = (server1: BaseServer, server2: BaseServer) => {
server1.serversOnNetwork = server1.serversOnNetwork.filter((conn) => conn !== server2.hostname);
server2.serversOnNetwork = server2.serversOnNetwork.filter((conn) => conn !== server1.hostname);
};
export function ipExists(ip: string): boolean {
for (const hostName in AllServers) {
if (AllServers[hostName].ip === ip) {
@@ -109,11 +125,20 @@ export function createUniqueRandomIp(): IPAddress {
}
// Safely add a Server to the AllServers map
export function AddToAllServers(server: Server | HacknetServer): void {
if (GetServer(server.hostname)) {
console.warn(`The hostname of the server that's being added is: ${server.hostname}`);
console.warn(`The server that already has this hostname is: ${AllServers[server.hostname].hostname}`);
throw new Error(`Error: Trying to add a server with an existing hostname. Hostname: ${server.hostname}.`);
export function AddToAllServers(server: Server | HacknetServer | DarknetServer): void {
let existingServer = GetServer(server.hostname);
if (existingServer) {
throw new Error(
`Trying to add a server with an existing hostname. New server: ${server.hostname} (${server.ip}). ` +
`Existing server: ${existingServer.hostname} (IP: ${existingServer.ip}).`,
);
}
existingServer = GetServer(server.ip);
if (existingServer) {
throw new Error(
`Trying to add a server with an existing IP. New server: ${server.hostname} (${server.ip}). ` +
`Existing server: ${existingServer.hostname} (IP: ${existingServer.ip}).`,
);
}
AllServers[server.hostname] = server;
@@ -125,95 +150,16 @@ export const renameServer = (hostname: string, newName: string): void => {
delete AllServers[hostname];
};
interface IServerParams {
hackDifficulty?: number;
hostname: string;
ip: IPAddress;
maxRam?: number;
moneyAvailable?: number;
numOpenPortsRequired: number;
organizationName: string;
requiredHackingSkill?: number;
serverGrowth?: number;
}
export function initForeignServers(homeComputer: Server): void {
/* Create a randomized network for all the foreign servers */
//Groupings for creating a randomized network
const networkLayers: Server[][] = [];
for (let i = 0; i < 15; i++) {
networkLayers.push([]);
}
const toNumber = (value: number | IMinMaxRange): number => {
if (typeof value === "number") return value;
else return getRandomIntInclusive(value.min, value.max);
};
for (const metadata of serverMetadata) {
const serverParams: IServerParams = {
hostname: metadata.hostname,
ip: createUniqueRandomIp(),
numOpenPortsRequired: metadata.numOpenPortsRequired,
organizationName: metadata.organizationName,
};
if (metadata.maxRamExponent !== undefined) {
serverParams.maxRam = Math.pow(2, toNumber(metadata.maxRamExponent));
}
if (metadata.hackDifficulty) serverParams.hackDifficulty = toNumber(metadata.hackDifficulty);
if (metadata.moneyAvailable) serverParams.moneyAvailable = toNumber(metadata.moneyAvailable);
if (metadata.requiredHackingSkill) serverParams.requiredHackingSkill = toNumber(metadata.requiredHackingSkill);
if (metadata.serverGrowth) serverParams.serverGrowth = toNumber(metadata.serverGrowth);
const server = new Server(serverParams);
if (metadata.networkLayer) {
const layer = toNumber(metadata.networkLayer);
server.cpuCores = getRandomIntInclusive(Math.ceil(layer / 2), layer);
}
for (const filename of metadata.literature || []) {
server.messages.push(filename);
}
if (server.hostname === SpecialServers.WorldDaemon) {
server.requiredHackingSkill *= currentNodeMults.WorldDaemonDifficulty;
}
AddToAllServers(server);
if (metadata.networkLayer !== undefined) {
networkLayers[toNumber(metadata.networkLayer) - 1].push(server);
}
}
/* Create a randomized network for all the foreign servers */
const linkComputers = (server1: Server, server2: Server): void => {
server1.serversOnNetwork.push(server2.hostname);
server2.serversOnNetwork.push(server1.hostname);
};
const getRandomArrayItem = <T>(arr: T[]): T => arr[Math.floor(Math.random() * arr.length)];
const linkNetworkLayers = (network1: Server[], selectServer: () => Server): void => {
for (const server of network1) {
linkComputers(server, selectServer());
}
};
// Connect the first tier of servers to the player's home computer
linkNetworkLayers(networkLayers[0], () => homeComputer);
for (let i = 1; i < networkLayers.length; i++) {
linkNetworkLayers(networkLayers[i], () => getRandomArrayItem(networkLayers[i - 1]));
}
}
export function prestigeAllServers(): void {
for (const member of Object.keys(AllServers)) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete AllServers[member];
}
AllServers = {};
// WIP: Check other properties in DarknetState as well, then improve validateDarknetNetwork.
DarknetState.Network = new Array(MAX_NET_DEPTH)
.fill(null)
.map(() => new Array<DarknetServer | null>(NET_WIDTH).fill(null));
}
export function loadAllServers(saveString: string): void {
@@ -223,12 +169,15 @@ export function loadAllServers(saveString: string): void {
throw new Error("Server list is empty.");
}
for (const [serverName, server] of Object.entries(allServersData)) {
if (!(server instanceof Server) && !(server instanceof HacknetServer)) {
if (!(server instanceof Server) && !(server instanceof HacknetServer) && !(server instanceof DarknetServer)) {
throw new Error(`Server ${serverName} is not an instance of Server or HacknetServer.`);
}
}
// We validated the data above, so it's safe to typecast here.
AllServers = allServersData as typeof AllServers;
// Apply blocked ram for darknet servers
applyRamBlocks();
}
export function saveAllServers(): string {

View File

@@ -27,7 +27,7 @@ import type { ScriptKey } from "../utils/helpers/scriptKey";
import { assertObject } from "../utils/TypeAssertion";
import { clampNumber } from "../utils/helpers/clampNumber";
interface IConstructorParams {
export interface BaseServerConstructorParams {
adminRights?: boolean;
hostname: string;
ip?: IPAddress;
@@ -73,7 +73,7 @@ export abstract class BaseServer implements IServer {
messages: (MessageFilename | LiteratureName)[] = [];
// Name of company/faction/etc. that this server belongs to.
// Optional, not applicable to all Servers
// Optional, not applicable to all Servers (e.g., pserver, hacknet server, and dnet server)
organizationName = "";
// Programs on this servers. Contains only the names of the programs
@@ -125,7 +125,7 @@ export abstract class BaseServer implements IServer {
serverGrowth?: number;
isHacknetServer?: boolean;
constructor(params: IConstructorParams = { hostname: "", ip: createRandomIp() }) {
constructor(params: BaseServerConstructorParams = { hostname: "", ip: createRandomIp() }) {
this.ip = params.ip ? params.ip : createRandomIp();
this.hostname = params.hostname;

View File

@@ -0,0 +1,97 @@
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";
export interface DarknetServerConstructorParams {
// Properties of BaseServer
hostname: string;
ip: IPAddress;
maxRam: number;
// Properties of DarknetServer
password: string;
modelId: string;
staticPasswordHint: string;
passwordHintData: string;
difficulty: number;
depth: number;
leftOffset: number;
hasStasisLink: boolean;
blockedRam: number;
logTrafficInterval: number;
requiredCharismaSkill: number;
isStationary: boolean;
}
export class DarknetServer extends BaseServer implements DarknetServerData {
// Override the optional "backdoorInstalled" property in BaseServer
backdoorInstalled: boolean = false;
/** Random reward caches on this server */
caches: CacheFilePath[] = [];
/** The password for the server, used for authentication */
password: string;
/** The model of the server. Similar models have similar vulnerabilities. */
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 left offset of the server in the net */
leftOffset: number;
/** If the server has a stasis link applied */
hasStasisLink: boolean;
/** The amount of ram blocked by the server owner */
blockedRam: number;
/** The interval at which the server logs traffic */
logTrafficInterval: number;
/** The charisma skill required to heartbleed the server */
requiredCharismaSkill: number;
/** If this darknet server cannot be moved. True for fixed/story servers. */
isStationary: boolean;
/** The number of CPU cores on the server. Included to make sure that grow etc on the server work as expected */
cpuCores: number = 1;
constructor(
params: DarknetServerConstructorParams = {
...exampleDarknetServerData,
ip: createRandomIp(),
password: "",
leftOffset: 0,
isStationary: false,
},
) {
super(params);
this.password = params.password;
this.modelId = params.modelId;
this.maxRam = params.maxRam;
this.staticPasswordHint = params.staticPasswordHint;
this.passwordHintData = params.passwordHintData;
this.difficulty = params.difficulty;
this.depth = params.depth;
this.leftOffset = params.leftOffset;
this.hasStasisLink = params.hasStasisLink;
this.blockedRam = params.blockedRam;
this.logTrafficInterval = params.logTrafficInterval;
this.requiredCharismaSkill = params.requiredCharismaSkill;
this.isStationary = params.isStationary;
}
toJSON(): IReviverValue {
return this.toJSONBase("DarknetServer", includedKeys);
}
static fromJSON(value: IReviverValue): DarknetServer {
return BaseServer.fromJSONBase(value, DarknetServer, includedKeys);
}
}
const includedKeys = BaseServer.getIncludedKeys(DarknetServer);
constructorsForReviver.DarknetServer = DarknetServer;

View File

@@ -8,7 +8,7 @@ import { createRandomIp } from "../utils/IPAddress";
import { IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { IPAddress } from "../Types/strings";
export interface IConstructorParams {
export interface StandardServerConstructorParams {
adminRights?: boolean;
hackDifficulty?: number;
hostname: string;
@@ -56,7 +56,7 @@ export class Server extends BaseServer {
// be increased using the grow() Netscript function
serverGrowth = 1;
constructor(params: IConstructorParams = { hostname: "", ip: createRandomIp() }) {
constructor(params: StandardServerConstructorParams = { hostname: "", ip: createRandomIp() }) {
super(params);
// "hacknet-node-X" hostnames are reserved for Hacknet Servers

View File

@@ -1,5 +1,12 @@
import { GetServer, createUniqueRandomIp, ipExists } from "./AllServers";
import { Server, IConstructorParams } from "./Server";
import {
AddToAllServers,
GetServer,
GetServerOrThrow,
connectServers,
createUniqueRandomIp,
ipExists,
} from "./AllServers";
import { Server, StandardServerConstructorParams } from "./Server";
import { BaseServer } from "./BaseServer";
import { calculateGrowMoney, calculateServerGrowthLog } from "./formulas/grow";
import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
@@ -15,6 +22,11 @@ import { exceptionAlert } from "../utils/helpers/exceptionAlert";
import { HacknetServer } from "../Hacknet/HacknetServer";
import { SpecialServers } from "./data/SpecialServers";
import { throwIfReachable } from "../utils/helpers/throwIfReachable";
import { initDarkwebServer, populateDarknet } from "../DarkNet/controllers/NetworkGenerator";
import { hasDarknetAccess } from "../DarkNet/utils/darknetAuthUtils";
import type { IMinMaxRange } from "../types";
import { getRandomIntInclusive } from "../utils/helpers/getRandomIntInclusive";
import type { IPAddress } from "../Types/strings";
export enum ServerOwnershipType {
All = 0,
@@ -27,7 +39,7 @@ export enum ServerOwnershipType {
* Constructs a new server, while also ensuring that the new server
* does not have a duplicate hostname/ip.
*/
export function safelyCreateUniqueServer(params: IConstructorParams): Server {
export function safelyCreateUniqueServer(params: StandardServerConstructorParams): Server {
let hostname: string = params.hostname.replace(/ /g, `-`);
if (params.ip != null && ipExists(params.ip)) {
@@ -310,3 +322,92 @@ export function checkServerOwnership(baseServer: BaseServer, serverType: ServerO
}
return false;
}
export function getTorRouter() {
connectServers(Player.getHomeComputer(), GetServerOrThrow(SpecialServers.DarkWeb));
}
interface IServerParams {
hackDifficulty?: number;
hostname: string;
ip: IPAddress;
maxRam?: number;
moneyAvailable?: number;
numOpenPortsRequired: number;
organizationName: string;
requiredHackingSkill?: number;
serverGrowth?: number;
}
export function initForeignServers(homeComputer: Server): void {
/* Create a randomized network for all the foreign servers */
//Groupings for creating a randomized network
const networkLayers: Server[][] = [];
for (let i = 0; i < 15; i++) {
networkLayers.push([]);
}
const toNumber = (value: number | IMinMaxRange): number => {
if (typeof value === "number") return value;
else return getRandomIntInclusive(value.min, value.max);
};
for (const metadata of serverMetadata) {
const serverParams: IServerParams = {
hostname: metadata.hostname,
ip: createUniqueRandomIp(),
numOpenPortsRequired: metadata.numOpenPortsRequired,
organizationName: metadata.organizationName,
};
if (metadata.maxRamExponent !== undefined) {
serverParams.maxRam = Math.pow(2, toNumber(metadata.maxRamExponent));
}
if (metadata.hackDifficulty) serverParams.hackDifficulty = toNumber(metadata.hackDifficulty);
if (metadata.moneyAvailable) serverParams.moneyAvailable = toNumber(metadata.moneyAvailable);
if (metadata.requiredHackingSkill) serverParams.requiredHackingSkill = toNumber(metadata.requiredHackingSkill);
if (metadata.serverGrowth) serverParams.serverGrowth = toNumber(metadata.serverGrowth);
const server = new Server(serverParams);
if (metadata.networkLayer) {
const layer = toNumber(metadata.networkLayer);
server.cpuCores = getRandomIntInclusive(Math.ceil(layer / 2), layer);
}
for (const filename of metadata.literature || []) {
server.messages.push(filename);
}
if (server.hostname === SpecialServers.WorldDaemon) {
server.requiredHackingSkill *= currentNodeMults.WorldDaemonDifficulty;
}
AddToAllServers(server);
if (metadata.networkLayer !== undefined) {
networkLayers[toNumber(metadata.networkLayer) - 1].push(server);
}
}
/* Create a randomized network for all the foreign servers */
const getRandomArrayItem = <T>(arr: T[]): T => arr[Math.floor(Math.random() * arr.length)];
const linkNetworkLayers = (network1: Server[], selectServer: () => Server): void => {
for (const server of network1) {
connectServers(server, selectServer());
}
};
// Connect the first tier of servers to the player's home computer
linkNetworkLayers(networkLayers[0], () => homeComputer);
for (let i = 1; i < networkLayers.length; i++) {
linkNetworkLayers(networkLayers[i], () => getRandomArrayItem(networkLayers[i - 1]));
}
initDarkwebServer();
if (hasDarknetAccess()) {
getTorRouter();
populateDarknet();
}
}

View File

@@ -10,4 +10,11 @@ export const SpecialServers = {
DaedalusServer: "The-Cave",
WorldDaemon: "w0r1d_d43m0n",
DarkWeb: "darkweb",
NormalLab: "th3_l4byr1nth",
CruelLab: "cru3l_l4byr1nth",
MercilessLab: "m3rc1l3ss_l4byr1nth",
UberLab: "ub3r_l4byr1nth",
EternalLab: "et3rn4l_l4byr1nth",
FinalLab: "f1n4l_l4byr1nth",
BonusLab: "b0nus_l4byr1nth",
} as const;

View File

@@ -1537,12 +1537,4 @@ export const serverMetadata: IServerMetadata[] = [
serverGrowth: 0,
specialName: SpecialServers.WorldDaemon,
},
{
hostname: SpecialServers.DarkWeb,
moneyAvailable: 0,
numOpenPortsRequired: 5,
organizationName: SpecialServers.DarkWeb,
requiredHackingSkill: 1,
specialName: SpecialServers.DarkWeb,
},
];

View File

@@ -38,6 +38,7 @@ import AccountBoxIcon from "@mui/icons-material/AccountBox"; // Character
import PublicIcon from "@mui/icons-material/Public"; // World
import LiveHelpIcon from "@mui/icons-material/LiveHelp"; // Help
import BorderInnerSharpIcon from "@mui/icons-material/BorderInnerSharp"; // IPvGO
import ShareIcon from "@mui/icons-material/Share"; // DarkWeb
import BiotechIcon from "@mui/icons-material/Biotech"; // Grafting
import { Router } from "../../ui/GameRoot";
@@ -70,6 +71,8 @@ import {
import { throwIfReachable } from "../../utils/helpers/throwIfReachable";
import { ErrorState } from "../../ErrorHandling/ErrorState";
import { hasDarknetAccess } from "../../DarkNet/utils/darknetAuthUtils";
const RotatedDoubleArrowIcon = React.forwardRef(function RotatedDoubleArrowIcon(
props: { color: "primary" | "secondary" | "error" },
__ref: React.ForwardedRef<SVGSVGElement>,
@@ -174,6 +177,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
const canBladeburner = !!Player.bladeburner;
const canStaneksGift = Player.augmentations.some((aug) => aug.name === AugmentationName.StaneksGift1);
const canIPvGO = playerHasDiscoveredGo();
const canDarkNet = hasDarknetAccess();
const clickPage = useCallback(
(page: Page) => {
@@ -232,6 +236,8 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
return canGang;
case SimplePage.Go:
return canIPvGO;
case SimplePage.DarkNet:
return canDarkNet;
case ScriptEditorAction.Save:
case ScriptEditorAction.GoToTerminal:
case ScriptEditorAction.Run:
@@ -253,6 +259,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
canCorporation,
canGang,
canIPvGO,
canDarkNet,
],
);
@@ -400,6 +407,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
canCorporation && { key_: Page.Corporation, icon: BusinessIcon },
canGang && { key_: Page.Gang, icon: SportsMmaIcon },
canIPvGO && { key_: Page.Go, icon: BorderInnerSharpIcon },
canDarkNet && { key_: Page.DarkNet, icon: ShareIcon },
]}
/>
<Divider />

View File

@@ -19,4 +19,5 @@ export function initSourceFiles() {
SourceFiles.SourceFile12 = new SourceFile(12, BitNodes.BitNode12.sfDescription);
SourceFiles.SourceFile13 = new SourceFile(13, BitNodes.BitNode13.sfDescription);
SourceFiles.SourceFile14 = new SourceFile(14, BitNodes.BitNode14.sfDescription);
SourceFiles.SourceFile15 = new SourceFile(15, BitNodes.BitNode15.sfDescription);
}

View File

@@ -168,7 +168,12 @@ export function applySourceFile(bn: number, lvl: number): void {
// Grants more space on Stanek's Gift.
break;
case 14: // IPvGO
// Grands increased buffs and favor limit from IPvGO
// Grants increased buffs and favor limit from IPvGO
break;
case 15: // The Dark Net
// Level 1: grants permanent access to the Tor router and DarkscapeNavigator.exe
// Level 2: grants a slight buff to company work rep gain based on charisma level
// Level 3: grants a slight buff to faction work rep gain based on charisma level
break;
default:
console.error(`Invalid source file number: ${bn}`);

View File

@@ -18,6 +18,7 @@ import { helpers } from "../Netscript/NetscriptHelpers";
import { getRandomIntInclusive } from "../utils/helpers/getRandomIntInclusive";
import { JsonSchemaValidator } from "../JsonSchema/JsonSchemaValidator";
import { Player } from "../Player";
import { scaleDarknetVolatilityIncreases, getDarknetVolatilityMult } from "../DarkNet/effects/effects";
export function getDefaultEmptyStockMarket(): IStockMarket {
return {
@@ -225,9 +226,9 @@ function stockMarketCycle(): void {
stock.b = !stock.b;
stock.flipForecastForecast();
}
StockMarket.ticksUntilCycle = StockMarketConstants.TicksPerCycle;
}
scaleDarknetVolatilityIncreases(0.4);
}
const cyclesPerStockUpdate = StockMarketConstants.msPerStockUpdate / CONSTANTS.MilliPerCycle;
@@ -260,7 +261,8 @@ export function processStockPrices(numCycles = 1): void {
for (const name of Object.keys(StockMarket)) {
const stock = StockMarket[name];
if (!(stock instanceof Stock)) continue;
let av = (v * stock.mv) / 100;
const volatility = stock.mv * getDarknetVolatilityMult(stock.symbol);
let av = (v * volatility) / 100;
if (isNaN(av)) {
av = 0.02;
}

View File

@@ -12,6 +12,7 @@ import { Player } from "@player";
import { Settings } from "../../Settings/Settings";
import { formatMoney, formatPercent } from "../../ui/formatNumber";
import Typography from "@mui/material/Typography";
import { getDarknetVolatilityMult } from "../../DarkNet/effects/effects";
interface IProps {
stock: Stock;
@@ -34,7 +35,8 @@ export function StockTickerHeaderText(props: IProps): React.ReactElement {
let hdrText = `${stock.name}${spacesAfterStockName}${stock.symbol} -${spacesBeforePrice}${stockPriceFormat}`;
if (Player.has4SData) {
hdrText += ` - Volatility: ${formatPercent(stock.mv / 100)} - Price Forecast: `;
const volatility = stock.mv * getDarknetVolatilityMult(stock.symbol);
hdrText += ` - Volatility: ${formatPercent(volatility / 100)} - Price Forecast: `;
let plusOrMinus = stock.b; // True for "+", false for "-"
if (stock.otlkMag < 0) {
plusOrMinus = !plusOrMinus;

View File

@@ -39,10 +39,10 @@ export class Link {
export class TTimer {
time: number;
timeLeft: number;
action: "h" | "b" | "a" | "g" | "w";
action: "h" | "b" | "a" | "g" | "w" | "c";
server?: BaseServer;
constructor(time: number, action: "h" | "b" | "a" | "g" | "w", server?: BaseServer) {
constructor(time: number, action: "h" | "b" | "a" | "g" | "w" | "c", server?: BaseServer) {
this.time = time;
this.timeLeft = time;
this.action = action;

View File

@@ -86,6 +86,8 @@ import { hasTextExtension } from "../Paths/TextFilePath";
import { ContractFilePath } from "../Paths/ContractFilePath";
import { ServerConstants } from "../Server/data/Constants";
import { isIPAddress } from "../Types/strings";
import { getRewardFromCache } from "../DarkNet/effects/cacheFiles";
import { DarknetServer } from "../Server/DarknetServer";
export const TerminalCommands: Record<string, (args: (string | number | boolean)[], server: BaseServer) => void> = {
"scan-analyze": scananalyze,
@@ -226,7 +228,8 @@ export class Terminal {
this.error("Cannot backdoor this kind of server");
return;
}
if (!(server instanceof Server)) throw new Error("server should be normal server");
if (!(server instanceof Server || server instanceof DarknetServer))
throw new Error("server should be normal server");
this.startAction(calculateHackingTime(server, Player) / 4, "b", server);
}
@@ -236,7 +239,7 @@ export class Terminal {
this.startAction(1, "a", server);
}
startAction(n: number, action: "h" | "b" | "a" | "g" | "w", server?: BaseServer): void {
startAction(n: number, action: "h" | "b" | "a" | "g" | "w" | "c", server?: BaseServer): void {
this.action = new TTimer(n, action, server);
}
@@ -364,7 +367,8 @@ export class Terminal {
this.error("Cannot hack this kind of server");
return;
}
if (!(server instanceof Server)) throw new Error("server should be normal server");
if (!(server instanceof Server || server instanceof DarknetServer))
throw new Error("server should be normal server");
server.backdoorInstalled = true;
if (SpecialServers.WorldDaemon === server.hostname) {
if (Player.bitNodeN == null) {
@@ -392,6 +396,11 @@ export class Terminal {
const canRunScripts = hasAdminRights && currServ.maxRam > 0;
this.print("Can run scripts on this host: " + (canRunScripts ? "YES" : "NO"));
this.print("RAM: " + formatRam(currServ.maxRam));
if (currServ instanceof DarknetServer && currServ.blockedRam) {
this.print("RAM blocked by owner: " + formatRam(currServ.blockedRam));
this.print("Stasis link: " + (currServ.hasStasisLink ? "YES" : "NO"));
this.print("Backdoor: " + (currServ.backdoorInstalled ? "YES" : "NO"));
}
if (currServ instanceof Server) {
this.print("Backdoor: " + (currServ.backdoorInstalled ? "YES" : "NO"));
const hackingSkill = currServ.requiredHackingSkill;
@@ -439,6 +448,14 @@ export class Terminal {
this.finishBackdoor(this.action.server, cancelled);
} else if (this.action.action === "a") {
this.finishAnalyze(this.action.server, cancelled);
} else if (this.action.action === "c" && this.action.server instanceof DarknetServer) {
const cache = this.action.server.caches.pop();
if (!cache) {
this.print("No cache files found.");
} else {
const result = getRewardFromCache(this.action.server, cache, true);
this.print(result.message);
}
}
if (cancelled) {
@@ -576,19 +593,30 @@ export class Terminal {
}
const ignoreServer = (s: BaseServer, d: number): boolean =>
(!all && s.purchasedByPlayer && s.hostname != "home") || d > depth || (!all && s instanceof HacknetServer);
(!all && s.purchasedByPlayer && s.hostname != "home") ||
d > depth ||
(!all && s instanceof HacknetServer) ||
(!all && s instanceof DarknetServer && s.hostname !== SpecialServers.DarkWeb);
const makeNode = (parent: string, s: BaseServer, d = 1): Node => ({
hostname: s.hostname,
children: s.serversOnNetwork
.filter((h) => h != parent)
.map((s) => GetServer(s))
.filter((v): v is BaseServer => !!v)
.filter((v) => !ignoreServer(v, d))
.map((h) => makeNode(s.hostname, h, d + 1)),
});
const makeNode = (root: BaseServer = Player.getCurrentServer()) => {
// Keep track of previously seen servers to prevent backtracking (since darknet can be cyclical)
const seenServers = [root.hostname];
const populateNode = (s: BaseServer, d = 1): Node => {
seenServers.push(s.hostname);
return {
hostname: s.hostname,
children: s.serversOnNetwork
.filter((h) => !seenServers.includes(h))
.map((s) => GetServer(s))
.filter((v): v is BaseServer => !!v)
.filter((v) => !ignoreServer(v, d))
.map((h) => populateNode(h, d + 1)),
};
};
return populateNode(root);
};
const root = makeNode(Player.getCurrentServer().hostname, Player.getCurrentServer());
const root = makeNode();
const printOutput = (node: Node, prefix = [" "], last = true) => {
const titlePrefix = prefix.slice(0, prefix.length - 1).join("") + (last ? "┗ " : "┣ ");
@@ -601,12 +629,14 @@ export class Terminal {
const server = GetServer(node.hostname);
if (!server) return;
const hasRoot = server.hasAdminRights ? "YES" : "NO";
if (server instanceof Server) {
const hasRoot = server.hasAdminRights ? "YES" : "NO";
this.print(
`${infoPrefix}Root Access: ${hasRoot}, Required hacking skill: ${server.requiredHackingSkill}` + "\n",
);
this.print(`${infoPrefix}Number of open ports required to NUKE: ${server.numOpenPortsRequired}` + "\n");
} else {
this.print(`${infoPrefix}Root Access: ${hasRoot}` + "\n");
}
this.print(`${infoPrefix}RAM: ${formatRam(server.maxRam)}` + "\n");
node.children.forEach((n, i) =>

Some files were not shown because too many files have changed in this diff Show More