mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-23 01:32:55 +02:00
195 lines
6.7 KiB
TypeScript
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;
|
|
};
|