import { Player } from "@player"; import type { DarknetServerData, Person as IPerson } from "@nsdefs"; import { AugmentationName, CompletedProgramName, LiteratureName } from "@enums"; 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, getBackdooredDarknetServers, 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); const chance = 0.1 * 1.05 ** server?.difficulty; if (Math.random() < chance && !isLabyrinthServer(server.hostname)) { addCacheToServer(server, false); } }; 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) * @param linear - if true, the time scaling is linear with the number of threads instead of having diminishing returns */ export const calculateAuthenticationTime = ( darknetServerData: DarknetServerData, person: IPerson = Player, threads = 1, attemptedPassword = "", linear = false, ) => { const chaRequired = darknetServerData.requiredCharismaSkill; const difficulty = darknetServerData.difficulty; const baseDiff = (difficulty + 1) * 100; const diffFactor = 5; const baseTime = 850; const threadsFactor = 1 / (linear ? threads : 1 + 0.2 * (threads - 1)); const skillFactor = (diffFactor * chaRequired + baseDiff) / (person.skills.charisma + 150); const backdoorFactor = getBackdoorAuthTimeDebuff(); const applyUnderleveledFactor = person.skills.charisma <= chaRequired && darknetServerData.depth > 1; const underleveledFactor = applyUnderleveledFactor ? 1.5 + (chaRequired + 50) / (person.skills.charisma + 50) : 1; const hasBootsFactor = Player.hasAugmentation(AugmentationName.TheBoots) ? 0.8 : 1; const hasSf15_2Factor = Player.activeSourceFileLvl(15) > 2 ? 0.8 : 1; const time = baseTime * skillFactor * backdoorFactor * underleveledFactor * hasBootsFactor * hasSf15_2Factor * threadsFactor; // We need to call GetServer and check if it's a dnet server later because this function can be called by formulas // APIs (darknetServerData.hostname may be an invalid hostname). const server = GetServer(darknetServerData.hostname); const password = server instanceof DarknetServer ? server.password : ""; // Add extra time for timing attack server, per correct character const sharedChars = darknetServerData.modelId === ModelIds.TimingAttack ? getSharedChars(password, attemptedPassword) : 0; const sharedCharsExtraTime = sharedChars * 50 * threadsFactor; return time * calculateIntelligenceBonus(person.skills.intelligence, 0.25) + sharedCharsExtraTime; }; export const getBackdoorAuthTimeDebuff = () => { const backdooredServerCount = getBackdooredDarknetServers().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) ); }; export const calculatePasswordAttemptChaGain = (server: DarknetServerData, threads: number = 1, success = false) => { const baseXpGain = 3; const difficultyBase = 1.1; 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; const staffLimitIncrease = Player.hasAugmentation(AugmentationName.TheStaff) ? 1 : 0; return 1 + brokenWingLimitIncrease + hammerLimitIncrease + staffLimitIncrease; }; 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 * 5 * threads * server.difficulty; Player.gainCharismaExp(xpGained); const currentCharge = DarknetState.migrationInductionServers.get(server.hostname) ?? 0; const newCharge = Math.min(currentCharge + chargeIncrease, 1); DarknetState.migrationInductionServers.set(server.hostname, newCharge); const result = { chargeIncrease, newCharge: newCharge, xpGained: xpGained, }; if (newCharge >= 1) { moveDarknetServer(server, -2, 4); DarknetState.migrationInductionServers.set(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 cctCooldownReached = () => { const timeSinceLastCCT = new Date().getTime() - DarknetState.lastCctRewardTime.getTime(); return timeSinceLastCCT > 10 * 60 * 1000; }; export const hasFullDarknetAccess = (): boolean => Player.bitNodeN === 15 || Player.activeSourceFileLvl(15) > 0;