From 1cb2a83b4fab11c3644e49467ca1798f38da2582 Mon Sep 17 00:00:00 2001
From: catloversg <152669316+catloversg@users.noreply.github.com>
Date: Wed, 8 Jan 2025 13:35:29 +0700
Subject: [PATCH] UI: Share RAM to boost reputation gain (#1862)
---
src/Faction/ui/FactionRoot.tsx | 4 +-
src/Faction/ui/Info.tsx | 1 -
src/Faction/ui/ShareOption.tsx | 87 ++++++++++++++++++++++++
src/NetscriptFunctions.ts | 20 +++---
src/NetworkShare/Share.ts | 57 ++++++++++++++++
src/NetworkShare/Share.tsx | 12 ----
src/NetworkShare/formulas/share.tsx | 5 --
src/PersonObjects/formulas/reputation.ts | 8 +--
src/Prestige.ts | 8 +++
src/Server/BaseServer.ts | 3 +-
10 files changed, 169 insertions(+), 36 deletions(-)
create mode 100644 src/Faction/ui/ShareOption.tsx
create mode 100644 src/NetworkShare/Share.ts
delete mode 100644 src/NetworkShare/Share.tsx
delete mode 100644 src/NetworkShare/formulas/share.tsx
diff --git a/src/Faction/ui/FactionRoot.tsx b/src/Faction/ui/FactionRoot.tsx
index be324619e..643224cb1 100644
--- a/src/Faction/ui/FactionRoot.tsx
+++ b/src/Faction/ui/FactionRoot.tsx
@@ -22,6 +22,7 @@ import { GangButton } from "./GangButton";
import { FactionWork } from "../../Work/FactionWork";
import { useCycleRerender } from "../../ui/React/hooks";
import { repNeededToDonate } from "../formulas/donation";
+import { ShareOption } from "./ShareOption";
type FactionRootProps = {
faction: Faction;
@@ -45,7 +46,7 @@ const securityWorkInfo =
"You will gain exp for all combat stats and hacking.";
const augmentationsInfo =
"As your reputation with this faction rises, you will " +
- "unlock Augmentations, which you can purchase to enhance " +
+ "unlock augmentations, which you can purchase to enhance " +
"your abilities.";
const sleevePurchasesInfo = "Purchase Duplicate Sleeves and upgrades. These are permanent!";
@@ -131,6 +132,7 @@ function MainPage({ faction, rerender, onAugmentations }: IMainProps): React.Rea
{!isPlayersGang && factionInfo.offersWork() && (
)}
+ {!isPlayersGang && factionInfo.offersWork() && }
{canPurchaseSleeves && (
<>
diff --git a/src/Faction/ui/Info.tsx b/src/Faction/ui/Info.tsx
index de9dad0f5..5d4636de4 100644
--- a/src/Faction/ui/Info.tsx
+++ b/src/Faction/ui/Info.tsx
@@ -27,7 +27,6 @@ interface IProps {
const useStyles = makeStyles()({
noformat: {
whiteSpace: "pre-wrap",
- lineHeight: "1em",
},
});
diff --git a/src/Faction/ui/ShareOption.tsx b/src/Faction/ui/ShareOption.tsx
new file mode 100644
index 000000000..934d44f95
--- /dev/null
+++ b/src/Faction/ui/ShareOption.tsx
@@ -0,0 +1,87 @@
+import React, { useState } from "react";
+
+import { TextField } from "@mui/material";
+import Button from "@mui/material/Button";
+import Paper from "@mui/material/Paper";
+import Typography from "@mui/material/Typography";
+
+import { Player } from "@player";
+import {
+ calculateCurrentShareBonus,
+ calculateShareBonusWithAdditionalThreads,
+ pendingUIShareJobIds,
+ ShareBonusTime,
+ startSharing,
+} from "../../NetworkShare/Share";
+import { formatRam } from "../../ui/formatNumber";
+import { dialogBoxCreate } from "../../ui/React/DialogBox";
+import { useCycleRerender } from "../../ui/React/hooks";
+import { roundToTwo } from "../../utils/helpers/roundToTwo";
+
+export function ShareOption({ rerender }: { rerender: () => void }): React.ReactElement {
+ const [ram, setRam] = useState(0);
+ useCycleRerender();
+
+ const home = Player.getHomeComputer();
+ const threads = Math.floor(ram / 4);
+
+ function onShare(): void {
+ if (threads === 0) {
+ return;
+ }
+ if (!Number.isFinite(threads) || threads < 0) {
+ dialogBoxCreate("Invalid RAM amount.");
+ return;
+ }
+ const freeRAM = home.maxRam - home.ramUsed;
+ const ramUsage = roundToTwo(4 * threads);
+ if (ramUsage > freeRAM + 0.001) {
+ dialogBoxCreate("Not enough RAM.");
+ return;
+ }
+
+ home.updateRamUsed(roundToTwo(home.ramUsed + ramUsage));
+ const end = startSharing(threads, home.cpuCores);
+ const jobId = window.setTimeout(() => {
+ end();
+ if (pendingUIShareJobIds.includes(jobId)) {
+ home.updateRamUsed(roundToTwo(home.ramUsed - ramUsage));
+ }
+ rerender();
+ }, ShareBonusTime);
+ pendingUIShareJobIds.push(jobId);
+ }
+
+ return (
+
+
+ You can share free RAM of your home computer with your faction to get a bonus multiplier for reputation gain.
+ Each time you share your free RAM, you get a boost for {ShareBonusTime / 1000} seconds. You can share free RAM
+ of other servers that you have admin rights by using ns.share() API.
+
+ Free RAM on home computer: {formatRam(home.maxRam - home.ramUsed)}.
+
+ Current bonus: {calculateCurrentShareBonus()}. Bonus with {formatRam(ram)}:{" "}
+ {calculateShareBonusWithAdditionalThreads(threads, home.cpuCores)}
+
+
+ {
+ if (event.target.value === "") {
+ setRam(0);
+ return;
+ }
+ const value = Number.parseFloat(event.target.value);
+ if (!Number.isFinite(value) || value < 0) {
+ return;
+ }
+ setRam(value);
+ }}
+ InputProps={{
+ endAdornment: ,
+ }}
+ />
+
+ );
+}
diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts
index 1bab92632..2a65975e1 100644
--- a/src/NetscriptFunctions.ts
+++ b/src/NetscriptFunctions.ts
@@ -35,7 +35,6 @@ import {
numCycleForGrowthCorrected,
processSingleServerGrowth,
safelyCreateUniqueServer,
- getCoreBonus,
getWeakenEffect,
} from "./Server/ServerHelpers";
import {
@@ -89,8 +88,7 @@ import { SnackbarEvents } from "./ui/React/Snackbar";
import { matchScriptPathExact } from "./utils/helpers/scriptKey";
import { Flags } from "./NetscriptFunctions/Flags";
-import { calculateIntelligenceBonus } from "./PersonObjects/formulas/intelligence";
-import { CalculateShareMult, StartSharing } from "./NetworkShare/Share";
+import { calculateCurrentShareBonus, ShareBonusTime, startSharing } from "./NetworkShare/Share";
import { recentScripts } from "./Netscript/RecentScripts";
import { InternalAPI, setRemovedFunctions, NSProxy } from "./Netscript/APIWrapper";
import { INetscriptExtra } from "./NetscriptFunctions/Extra";
@@ -421,19 +419,17 @@ export const ns: InternalAPI = {
return getWeakenEffect(threads, cores);
},
share: (ctx) => () => {
- const cores = helpers.getServer(ctx, ctx.workerScript.hostname).cpuCores;
- const coreBonus = getCoreBonus(cores);
- helpers.log(ctx, () => "Sharing this computer.");
- const end = StartSharing(
- ctx.workerScript.scriptRef.threads * calculateIntelligenceBonus(Player.skills.intelligence, 2) * coreBonus,
- );
- return helpers.netscriptDelay(ctx, 10000).finally(function () {
- helpers.log(ctx, () => "Finished sharing this computer.");
+ const threads = ctx.workerScript.scriptRef.threads;
+ const hostname = ctx.workerScript.hostname;
+ helpers.log(ctx, () => `Sharing ${threads} threads on ${hostname}.`);
+ const end = startSharing(threads, helpers.getServer(ctx, hostname).cpuCores);
+ return helpers.netscriptDelay(ctx, ShareBonusTime).finally(function () {
+ helpers.log(ctx, () => `Finished sharing ${threads} threads on ${hostname}.`);
end();
});
},
getSharePower: () => () => {
- return CalculateShareMult();
+ return calculateCurrentShareBonus();
},
print:
(ctx) =>
diff --git a/src/NetworkShare/Share.ts b/src/NetworkShare/Share.ts
new file mode 100644
index 000000000..01d206076
--- /dev/null
+++ b/src/NetworkShare/Share.ts
@@ -0,0 +1,57 @@
+import { Player } from "@player";
+import { calculateIntelligenceBonus } from "../PersonObjects/formulas/intelligence";
+import { getCoreBonus } from "../Server/ServerHelpers";
+import { clampNumber } from "../utils/helpers/clampNumber";
+
+let sharePower = 1;
+
+export const ShareBonusTime = 10000;
+
+/**
+ * When the player shares free RAM via UI, it's a "pending job". After that job finishes, we restore the free RAM by
+ * decreasing server.ramUsed by calling server.updateRamUsed(). However, if the player prestiges before that, all
+ * servers are reset and ramUsed is reset to 0. This means that when a job finishes, we may modify ramUsed of a new
+ * server.
+ *
+ * To solve this problem, we use an array to save job IDs. When the player prestiges, we clear this array. When a job
+ * finishes, we check if that job ID is still in this array. If it is not, it means that the player performed a
+ * prestige, and we do not need to decrease ramUsed.
+ */
+export const pendingUIShareJobIds: number[] = [];
+
+export function calculateEffectiveSharedThreads(threads: number, cpuCores: number): number {
+ const coreBonus = getCoreBonus(cpuCores);
+ return threads * calculateIntelligenceBonus(Player.skills.intelligence, 2) * coreBonus;
+}
+
+export function startSharing(threads: number, cpuCores: number): () => void {
+ const effectiveThreads = calculateEffectiveSharedThreads(threads, cpuCores);
+ sharePower += effectiveThreads;
+ return () => {
+ /**
+ * Due to floating point inaccuracy, sharePower may be slightly higher or lower than 1 after many times the player
+ * shares their RAM. We need to make sure that it's not smaller than 1.
+ */
+ sharePower = clampNumber(sharePower - effectiveThreads, 1);
+ // sharePower may be slightly higher than 1. Reset sharePower if it's smaller than a threshold.
+ if (sharePower < 1.00001) {
+ sharePower = 1;
+ }
+ };
+}
+
+function calculateShareBonus(sharePower: number): number {
+ const bonus = 1 + Math.log(sharePower) / 25;
+ if (!Number.isFinite(bonus)) {
+ return 1;
+ }
+ return bonus;
+}
+
+export function calculateShareBonusWithAdditionalThreads(threads: number, cpuCores: number): number {
+ return calculateShareBonus(sharePower + calculateEffectiveSharedThreads(threads, cpuCores));
+}
+
+export function calculateCurrentShareBonus(): number {
+ return calculateShareBonus(sharePower);
+}
diff --git a/src/NetworkShare/Share.tsx b/src/NetworkShare/Share.tsx
deleted file mode 100644
index d0fe2ba84..000000000
--- a/src/NetworkShare/Share.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { CalculateShareMult as CSM } from "./formulas/share";
-
-export let sharePower = 1;
-
-export function StartSharing(threads: number): () => void {
- sharePower += threads;
- return () => (sharePower -= threads);
-}
-
-export function CalculateShareMult(): number {
- return CSM(sharePower);
-}
diff --git a/src/NetworkShare/formulas/share.tsx b/src/NetworkShare/formulas/share.tsx
deleted file mode 100644
index 6c5fbd572..000000000
--- a/src/NetworkShare/formulas/share.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-export function CalculateShareMult(power: number): number {
- const x = 1 + Math.log(power) / 25;
- if (isNaN(x) || !isFinite(x)) return 1;
- return x;
-}
diff --git a/src/PersonObjects/formulas/reputation.ts b/src/PersonObjects/formulas/reputation.ts
index ccfd5cc36..746f7a003 100644
--- a/src/PersonObjects/formulas/reputation.ts
+++ b/src/PersonObjects/formulas/reputation.ts
@@ -1,6 +1,6 @@
import { CONSTANTS } from "../../Constants";
import { currentNodeMults } from "../../BitNode/BitNodeMultipliers";
-import { CalculateShareMult } from "../../NetworkShare/Share";
+import { calculateCurrentShareBonus } from "../../NetworkShare/Share";
import { Person as IPerson } from "@nsdefs";
import { calculateIntelligenceBonus } from "./intelligence";
@@ -18,7 +18,7 @@ export function getHackingWorkRepGain(p: IPerson, favor: number): number {
p.mults.faction_rep *
calculateIntelligenceBonus(p.skills.intelligence, 1) *
mult(favor) *
- CalculateShareMult()
+ calculateCurrentShareBonus()
);
}
@@ -29,7 +29,7 @@ export function getFactionSecurityWorkRepGain(p: IPerson, favor: number): number
p.skills.defense +
p.skills.dexterity +
p.skills.agility +
- (p.skills.hacking + p.skills.intelligence) * CalculateShareMult())) /
+ (p.skills.hacking + p.skills.intelligence) * calculateCurrentShareBonus())) /
CONSTANTS.MaxSkillLevel /
4.5;
return t * p.mults.faction_rep * mult(favor) * calculateIntelligenceBonus(p.skills.intelligence, 1);
@@ -43,7 +43,7 @@ export function getFactionFieldWorkRepGain(p: IPerson, favor: number): number {
p.skills.dexterity +
p.skills.agility +
p.skills.charisma +
- (p.skills.hacking + p.skills.intelligence) * CalculateShareMult())) /
+ (p.skills.hacking + p.skills.intelligence) * calculateCurrentShareBonus())) /
CONSTANTS.MaxSkillLevel /
5.5;
return t * p.mults.faction_rep * mult(favor) * calculateIntelligenceBonus(p.skills.intelligence, 1);
diff --git a/src/Prestige.ts b/src/Prestige.ts
index acd1994ec..3cde0a85d 100644
--- a/src/Prestige.ts
+++ b/src/Prestige.ts
@@ -29,6 +29,7 @@ import { Go } from "./Go/Go";
import { calculateExp } from "./PersonObjects/formulas/skill";
import { currentNodeMults } from "./BitNode/BitNodeMultipliers";
import { canAccessBitNodeFeature } from "./BitNode/BitNodeUtils";
+import { pendingUIShareJobIds } from "./NetworkShare/Share";
const BitNode8StartingMoney = 250e6;
function delayedDialog(message: string) {
@@ -75,6 +76,9 @@ export function prestigeAugmentation(): void {
AddToAllServers(homeComp);
prestigeHomeComputer(homeComp);
+ // Clear all pending share jobs created via UI
+ pendingUIShareJobIds.length = 0;
+
// Receive starting money and programs from installed augmentations
for (const ownedAug of Player.augmentations) {
const aug = Augmentations[ownedAug.name];
@@ -211,6 +215,10 @@ export function prestigeSourceFile(isFlume: boolean): void {
// Reset home computer (only the programs) and add to AllServers
AddToAllServers(homeComp);
prestigeHomeComputer(homeComp);
+
+ // Clear all pending share jobs created via UI
+ pendingUIShareJobIds.length = 0;
+
// Ram usage needs to be cleared for bitnode-level resets, due to possible change in singularity cost.
for (const script of homeComp.scripts.values()) script.ramUsage = null;
diff --git a/src/Server/BaseServer.ts b/src/Server/BaseServer.ts
index 1edb7b857..de79d70f0 100644
--- a/src/Server/BaseServer.ts
+++ b/src/Server/BaseServer.ts
@@ -25,6 +25,7 @@ import { Settings } from "../Settings/Settings";
import type { ScriptKey } from "../utils/helpers/scriptKey";
import { objectAssert } from "../utils/helpers/typeAssertion";
+import { clampNumber } from "../utils/helpers/clampNumber";
interface IConstructorParams {
adminRights?: boolean;
@@ -232,7 +233,7 @@ export abstract class BaseServer implements IServer {
}
updateRamUsed(ram: number): void {
- this.ramUsed = ram;
+ this.ramUsed = clampNumber(ram, 0, this.maxRam);
}
pushProgram(program: ProgramFilePath | CompletedProgramName): void {