Files
bitburner-src/src/DarkNet/models/DarknetServerOptions.ts
T
2026-03-06 11:11:06 -08:00

195 lines
6.7 KiB
TypeScript

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";
import { isIPAddress } from "../../Types/strings";
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;
name?: string;
preventBlockedRam?: boolean;
};
export const DnetServerBuilder = (options: DarknetServerOptions): DarknetServer => {
const maxRam = 16 * 2 ** Math.floor(options.difficulty / 4);
const ramBlock = options.preventBlockedRam ? 0 : getRamBlock(maxRam);
const name = options.name ?? generateDarknetServerName();
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);
DarknetState.offlineServers.delete(name);
DarknetState.offlineServers.delete(server.ip);
AddToAllServers(server);
return server;
};
export const generateDarknetServerName = (): string => {
if (Math.random() < 0.03 && DarknetState.offlineServers.size > 0 && hasFullDarknetAccess()) {
// Reuse a hostname that went offline. Note that we're only reusing hostnames,
// not IPs. This asymmetry is intentional - people find hostnames easier to
// work with, and this adds a bit of friction to compensate.
// Use an iterator to directly skip to the appropriate position. Sets don't
// have a way to index by offset, and converting to an array would be wasteful.
const offset = Math.floor(Math.random() * DarknetState.offlineServers.size);
const it = DarknetState.offlineServers.values();
for (let i = 0; i < offset; ++i) {
it.next();
}
// The set contains both IPs and hostnames. If we hit an IP, keep going
// forward until we find a hostname. *If* the Set implements traversal in
// the same order as insertion, then the fact that we insert IPs first
// means that we will always find a hostname and the sampling will be
// unbiased. Otherwise, it will be Mostly Unbiased(TM), and in rare cases
// we will fall off the end without reusing a name.
let serverName = it.next().value;
while (serverName != null && isIPAddress(serverName)) {
serverName = it.next().value;
}
if (serverName != null) {
return serverName;
}
}
return decorateName(getBaseName());
};
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()}`;
continue;
}
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;
};