Merge branch 'dev' into feat/open-multiple-files-from-cli

This commit is contained in:
hydroflame 2021-12-21 11:02:06 -05:00 committed by GitHub
commit c1c7131545
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 317 additions and 108 deletions

@ -66,6 +66,7 @@
| [StockOrder](./bitburner.stockorder.md) | Return value of [getOrders](./bitburner.tix.getorders.md) |
| [StockOrderObject](./bitburner.stockorderobject.md) | Value in map of [StockOrder](./bitburner.stockorder.md) |
| [TIX](./bitburner.tix.md) | Stock market API |
| [UserInterface](./bitburner.userinterface.md) | User Interface API. |
| [Warehouse](./bitburner.warehouse.md) | Warehouse for a division in a city |
| [WarehouseAPI](./bitburner.warehouseapi.md) | Corporation Warehouse API |

@ -51,6 +51,7 @@ export async function main(ns) {
| [sleeve](./bitburner.ns.sleeve.md) | [Sleeve](./bitburner.sleeve.md) | Namespace for sleeve functions. |
| [stanek](./bitburner.ns.stanek.md) | [Stanek](./bitburner.stanek.md) | Namespace for stanek functions. RAM cost: 0 GB |
| [stock](./bitburner.ns.stock.md) | [TIX](./bitburner.tix.md) | Namespace for stock functions. |
| [ui](./bitburner.ns.ui.md) | [UserInterface](./bitburner.userinterface.md) | Namespace for user interface functions. RAM cost: 0 GB |
## Methods

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [NS](./bitburner.ns.md) &gt; [ui](./bitburner.ns.ui.md)
## NS.ui property
Namespace for user interface functions. RAM cost: 0 GB
<b>Signature:</b>
```typescript
readonly ui: UserInterface;
```

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [UserInterface](./bitburner.userinterface.md) &gt; [getTheme](./bitburner.userinterface.gettheme.md)
## UserInterface.getTheme() method
Get the current theme
<b>Signature:</b>
```typescript
getTheme(): UserInterfaceTheme;
```
<b>Returns:</b>
UserInterfaceTheme
An object containing the theme's colors
## Remarks
RAM cost: cost: 0 GB

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [UserInterface](./bitburner.userinterface.md)
## UserInterface interface
User Interface API.
<b>Signature:</b>
```typescript
interface UserInterface
```
## Methods
| Method | Description |
| --- | --- |
| [getTheme()](./bitburner.userinterface.gettheme.md) | Get the current theme |

@ -34,6 +34,12 @@ import { getTimestamp } from "../utils/helpers/getTimestamp";
import { joinFaction } from "../Faction/FactionHelpers";
import { WorkerScript } from "../Netscript/WorkerScript";
interface BlackOpsAttempt {
error?: string;
isAvailable?: boolean;
action?: BlackOperation;
}
export class Bladeburner implements IBladeburner {
numHosp = 0;
moneyLost = 0;
@ -113,6 +119,44 @@ export class Bladeburner implements IBladeburner {
return Math.min(1, this.stamina / (0.5 * this.maxStamina));
}
canAttemptBlackOp(actionId: IActionIdentifier): BlackOpsAttempt {
// Safety measure - don't repeat BlackOps that are already done
if (this.blackops[actionId.name] != null) {
return { error: "Tried to start a Black Operation that had already been completed" }
}
const action = this.getActionObject(actionId);
if (!(action instanceof BlackOperation)) throw new Error(`Action should be BlackOperation but isn't`);
if (action == null) throw new Error("Failed to get BlackOperation object for: " + actionId.name);
if (action.reqdRank > this.rank) {
return { error: "Tried to start a Black Operation without the rank requirement" };
}
// Can't start a BlackOp if you haven't done the one before it
const blackops = [];
for (const nm in BlackOperations) {
if (BlackOperations.hasOwnProperty(nm)) {
blackops.push(nm);
}
}
blackops.sort(function (a, b) {
return BlackOperations[a].reqdRank - BlackOperations[b].reqdRank; // Sort black ops in intended order
});
const i = blackops.indexOf(actionId.name);
if (i === -1) {
return { error: `Invalid Black Op: '${name}'` };
}
if (i > 0 && this.blackops[blackops[i - 1]] == null) {
return { error: `Preceding Black Op must be completed before starting '${actionId.name}'.` }
}
return { isAvailable: true, action }
}
startAction(player: IPlayer, actionId: IActionIdentifier): void {
if (actionId == null) return;
this.action = actionId;
@ -156,18 +200,13 @@ export class Bladeburner implements IBladeburner {
case ActionTypes["BlackOp"]:
case ActionTypes["BlackOperation"]: {
try {
// Safety measure - don't repeat BlackOps that are already done
if (this.blackops[actionId.name] != null) {
const testBlackOp = this.canAttemptBlackOp(actionId);
if (!testBlackOp.isAvailable) {
this.resetAction();
this.log("Error: Tried to start a Black Operation that had already been completed");
this.log(`Error: ${testBlackOp.error}`);
break;
}
const action = this.getActionObject(actionId);
if (action == null) {
throw new Error("Failed to get BlackOperation object for: " + actionId.name);
}
this.actionTimeToComplete = action.getActionTime(this);
this.actionTimeToComplete = testBlackOp.action.getActionTime(this);
} catch (e: any) {
exceptionAlert(e);
}
@ -502,6 +541,7 @@ export class Bladeburner implements IBladeburner {
const skill = Skills[skillName];
if (skill == null || !(skill instanceof Skill)) {
this.postToConsole("Invalid skill name (Note that it is case-sensitive): " + skillName);
break;
}
if (args[1].toLowerCase() === "list") {
let level = 0;
@ -515,7 +555,11 @@ export class Bladeburner implements IBladeburner {
currentLevel = this.skills[skillName];
}
const pointCost = skill.calculateCost(currentLevel);
if (this.skillPoints >= pointCost) {
if (skill.maxLvl !== 0 && currentLevel >= skill.maxLvl) {
this.postToConsole(
`This skill ${skill.name} is already at max level (${currentLevel}/${skill.maxLvl}).`,
);
} else if (this.skillPoints >= pointCost) {
this.skillPoints -= pointCost;
this.upgradeSkill(skill);
this.log(skill.name + " upgraded to Level " + this.skills[skillName]);
@ -2032,44 +2076,9 @@ export class Bladeburner implements IBladeburner {
// Special logic for Black Ops
if (actionId.type === ActionTypes["BlackOp"]) {
// Can't start a BlackOp if you don't have the required rank
const action = this.getActionObject(actionId);
if (action == null) throw new Error(`Action not found ${actionId.type}, ${actionId.name}`);
if (!(action instanceof BlackOperation)) throw new Error(`Action should be BlackOperation but isn't`);
//const blackOp = (action as BlackOperation);
if (action.reqdRank > this.rank) {
workerScript.log("bladeburner.startAction", () => `Insufficient rank to start Black Op '${actionId.name}'.`);
return false;
}
// Can't start a BlackOp if its already been done
if (this.blackops[actionId.name] != null) {
workerScript.log("bladeburner.startAction", () => `Black Op ${actionId.name} has already been completed.`);
return false;
}
// Can't start a BlackOp if you haven't done the one before it
const blackops = [];
for (const nm in BlackOperations) {
if (BlackOperations.hasOwnProperty(nm)) {
blackops.push(nm);
}
}
blackops.sort(function (a, b) {
return BlackOperations[a].reqdRank - BlackOperations[b].reqdRank; // Sort black ops in intended order
});
const i = blackops.indexOf(actionId.name);
if (i === -1) {
workerScript.log("bladeburner.startAction", () => `Invalid Black Op: '${name}'`);
return false;
}
if (i > 0 && this.blackops[blackops[i - 1]] == null) {
workerScript.log(
"bladeburner.startAction",
() => `Preceding Black Op must be completed before starting '${actionId.name}'.`,
);
const canRunOp = this.canAttemptBlackOp(actionId);
if (!canRunOp.isAvailable) {
workerScript.log("bladeburner.startAction", () => canRunOp.error);
return false;
}
}

@ -54,7 +54,6 @@ interface IProps {
export function Console(props: IProps): React.ReactElement {
const classes = useStyles();
const scrollHook = useRef<HTMLDivElement>(null);
const [command, setCommand] = useState("");
const setRerender = useState(false)[1];
@ -64,22 +63,14 @@ export function Console(props: IProps): React.ReactElement {
const [consoleHistoryIndex, setConsoleHistoryIndex] = useState(props.bladeburner.consoleHistory.length);
// TODO: Figure out how to actually make the scrolling work correctly.
function scrollToBottom(): void {
if (!scrollHook.current) return;
scrollHook.current.scrollTop = scrollHook.current.scrollHeight;
}
function rerender(): void {
setRerender((old) => !old);
}
useEffect(() => {
const id = setInterval(rerender, 1000);
const id2 = setInterval(scrollToBottom, 100);
return () => {
clearInterval(id);
clearInterval(id2);
};
}, []);
@ -113,6 +104,7 @@ export function Console(props: IProps): React.ReactElement {
setConsoleHistoryIndex(i);
const prevCommand = consoleHistory[i];
event.currentTarget.value = prevCommand;
setCommand(prevCommand);
}
if (event.keyCode === 40) {
@ -134,17 +126,23 @@ export function Console(props: IProps): React.ReactElement {
setConsoleHistoryIndex(consoleHistoryIndex + 1);
const prevCommand = consoleHistory[consoleHistoryIndex + 1];
event.currentTarget.value = prevCommand;
setCommand(prevCommand);
}
}
}
return (
<Box height={"60vh"} display={"flex"} alignItems={"stretch"} component={Paper}>
<Paper>
<Box sx={{
height: '60vh',
paddingBottom: '8px',
display: 'flex',
alignItems: 'stretch',
}}>
<Box>
<List sx={{ height: "100%", overflow: "auto" }}>
{props.bladeburner.consoleLogs.map((log: any, i: number) => (
<Line key={i} content={log} />
))}
<Logs entries={[...props.bladeburner.consoleLogs]} />
</Box>
</Box>
<TextField
classes={{ root: classes.textfield }}
autoFocus
@ -164,9 +162,32 @@ export function Console(props: IProps): React.ReactElement {
spellCheck: false,
}}
/>
</List>
<div ref={scrollHook}></div>
</Box>
</Box>
</Paper>
);
}
interface ILogProps {
entries: string[];
}
function Logs({entries}: ILogProps): React.ReactElement {
const scrollHook = useRef<HTMLUListElement>(null);
// TODO: Text gets shifted up as new entries appear, if the user scrolled up it should attempt to keep the text focused
function scrollToBottom(): void {
if (!scrollHook.current) return;
scrollHook.current.scrollTop = scrollHook.current.scrollHeight;
}
useEffect(() => {
scrollToBottom();
}, [entries]);
return (
<List sx={{ height: "100%", overflow: "auto", p: 1 }} ref={scrollHook}>
{entries && entries.map((log: any, i: number) => (
<Line key={i} content={log} />
))}
</List>
);
}

@ -174,7 +174,7 @@ export function AugmentationsPage(props: IProps): React.ReactElement {
<>
<br />
<Typography variant="h4">Purchased Augmentations</Typography>
<Typography>This factions also offers these augmentations but you already own them.</Typography>
<Typography>This faction also offers these augmentations but you already own them.</Typography>
{owned.map((aug) => purchaseableAugmentation(aug, true))}
</>
);

@ -73,7 +73,7 @@ export function TechVendorLocation(props: IProps): React.ReactElement {
{purchaseServerButtons}
<br />
<Typography>
<i>"You can order bigger servers via scripts. We don't take custom order in person."</i>
<i>"You can order bigger servers via scripts. We don't take custom orders in person."</i>
</Typography>
<br />
<TorButton p={player} rerender={rerender} />

@ -348,6 +348,10 @@ export const RamCosts: IMap<any> = {
remove: RamCostConstants.ScriptStanekDeleteAt,
},
ui: {
getTheme: 0,
},
heart: {
// Easter egg function
break: 0,

@ -64,7 +64,7 @@ import { NetscriptSleeve } from "./NetscriptFunctions/Sleeve";
import { NetscriptExtra } from "./NetscriptFunctions/Extra";
import { NetscriptHacknet } from "./NetscriptFunctions/Hacknet";
import { NetscriptStanek } from "./NetscriptFunctions/Stanek";
import { NetscriptUserInterface } from './NetscriptFunctions/UserInterface';
import { NetscriptBladeburner } from "./NetscriptFunctions/Bladeburner";
import { NetscriptCodingContract } from "./NetscriptFunctions/CodingContract";
import { NetscriptCorporation } from "./NetscriptFunctions/Corporation";
@ -455,6 +455,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
const formulas = NetscriptFormulas(Player, workerScript, helper);
const singularity = NetscriptSingularity(Player, workerScript, helper);
const stockmarket = NetscriptStockMarket(Player, workerScript, helper);
const ui = NetscriptUserInterface(Player, workerScript, helper);
const base: INS = {
...singularity,
@ -465,7 +466,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
sleeve: sleeve,
corporation: corporation,
stanek: stanek,
ui: ui,
formulas: formulas,
stock: stockmarket,
args: workerScript.args,

@ -0,0 +1,20 @@
import { INetscriptHelper } from "./INetscriptHelper";
import { WorkerScript } from "../Netscript/WorkerScript";
import { IPlayer } from "../PersonObjects/IPlayer";
import { getRamCost } from "../Netscript/RamCostGenerator";
import { UserInterface as IUserInterface, UserInterfaceTheme } from "../ScriptEditor/NetscriptDefinitions";
import { Settings } from "../Settings/Settings";
export function NetscriptUserInterface(
player: IPlayer,
workerScript: WorkerScript,
helper: INetscriptHelper,
): IUserInterface {
return {
getTheme: function (): UserInterfaceTheme {
helper.updateDynamicRam("getTheme", getRamCost("ui", "getTheme"));
return {...Settings.theme};
},
}
}

@ -194,6 +194,8 @@ async function parseOnlyRamCalculate(
func = workerScript.env.vars.sleeve[ref];
} else if (ref in workerScript.env.vars.stock) {
func = workerScript.env.vars.stock[ref];
} else if (ref in workerScript.env.vars.ui) {
func = workerScript.env.vars.ui[ref];
} else {
func = workerScript.env.vars[ref];
}

@ -3741,6 +3741,21 @@ interface Stanek {
remove(rootX: number, rootY: number): boolean;
}
/**
* User Interface API.
* @public
*/
interface UserInterface {
/**
* Get the current theme
* @remarks
* RAM cost: cost: 0 GB
*
* @returns An object containing the theme's colors
*/
getTheme(): UserInterfaceTheme;
}
/**
* Collection of all functions passed to scripts
* @public
@ -3823,6 +3838,12 @@ export interface NS extends Singularity {
*/
readonly corporation: Corporation;
/**
* Namespace for user interface functions.
* RAM cost: 0 GB
*/
readonly ui: UserInterface;
/**
* Arguments passed into the script.
*
@ -5381,6 +5402,11 @@ export interface NS extends Singularity {
/**
* Get information about the player.
* @remarks
* RAM cost: 0.5 GB
*
* Returns an object with information on the current player.
*
* @returns Player info
*/
getPlayer(): Player;
@ -5885,3 +5911,45 @@ interface Division {
/** Cities in which this division has expanded */
cities: string[];
}
/**
* Interface Theme
* @internal
*/
interface UserInterfaceTheme {
[key: string]: string | undefined;
primarylight: string;
primary: string;
primarydark: string;
successlight: string;
success: string;
successdark: string;
errorlight: string;
error: string;
errordark: string;
secondarylight: string;
secondary: string;
secondarydark: string;
warninglight: string;
warning: string;
warningdark: string;
infolight: string;
info: string;
infodark: string;
welllight: string;
well: string;
white: string;
black: string;
hp: string;
money: string;
hack: string;
combat: string;
cha: string;
int: string;
rep: string;
disabled: string;
backgroundprimary: string;
backgroundsecondary: string;
button: string;
}

@ -125,6 +125,25 @@ export function Root(props: IProps): React.ReactElement {
vim: props.vim || Settings.MonacoVim,
});
const [dimensions, setDimensions] = useState({
height: window.innerHeight,
width: window.innerWidth,
});
useEffect(() => {
const debouncedHandleResize = debounce(function handleResize() {
setDimensions({
height: window.innerHeight,
width: window.innerWidth,
});
}, 250);
window.addEventListener("resize", debouncedHandleResize);
return () => {
window.removeEventListener("resize", debouncedHandleResize);
};
}, []);
useEffect(() => {
// Save currentScript
window.localStorage.setItem(
@ -203,7 +222,7 @@ export function Root(props: IProps): React.ReactElement {
// Generates a new model for the script
function regenerateModel(script: OpenScript): void {
if (monacoRef.current !== null) {
script.model = monacoRef.current.editor.createModel(script.code, "javascript");
script.model = monacoRef.current.editor.createModel(script.code, script.fileName.endsWith(".txt") ? "plaintext" : "javascript");
}
}
@ -217,6 +236,10 @@ export function Root(props: IProps): React.ReactElement {
);
async function updateRAM(newCode: string): Promise<void> {
if (currentScript != null && currentScript.fileName.endsWith(".txt")) {
debouncedSetRAM("");
return;
}
setUpdatingRam(true);
const codeCopy = newCode + "";
const ramUsage = await calculateRamUsage(codeCopy, props.player.getCurrentServer().scripts);
@ -348,7 +371,7 @@ export function Root(props: IProps): React.ReactElement {
code,
props.hostname,
new monacoRef.current.Position(0, 0),
monacoRef.current.editor.createModel(code, "javascript"),
monacoRef.current.editor.createModel(code, filename.endsWith(".txt") ? "plaintext" : "javascript"),
);
setOpenScripts((oldArray) => [...oldArray, newScript]);
setCurrentScript({ ...newScript });
@ -642,11 +665,13 @@ export function Root(props: IProps): React.ReactElement {
}
}
// TODO: Make this responsive to window resizes
// Toolbars are roughly 108px + vim bar 34px
// Get percentage of space that toolbars represent and the rest should be the
// editor
const editorHeight = 100 - ((108 + (options.vim ? 34 : 0)) / window.innerHeight) * 100;
// Toolbars are roughly 112px:
// 8px body margin top
// 38.5px filename tabs
// 5px padding for top of editor
// 44px bottom tool bar + 16px margin
// + vim bar 34px
const editorHeight = dimensions.height - (112 + (options.vim ? 34 : 0));
return (
<>
@ -725,7 +750,7 @@ export function Root(props: IProps): React.ReactElement {
beforeMount={beforeMount}
onMount={onMount}
loading={<Typography>Loading script editor!</Typography>}
height={`${editorHeight}%`}
height={`${editorHeight}px`}
defaultLanguage="javascript"
defaultValue={""}
onChange={updateCode}

@ -11,7 +11,8 @@ import PaletteSharpIcon from "@mui/icons-material/PaletteSharp";
import { Color, ColorPicker } from "material-ui-color";
import { ThemeEvents } from "./Theme";
import { Settings, defaultSettings } from "../../Settings/Settings";
import { ITheme, getPredefinedThemes } from "../../Settings/Themes";
import { getPredefinedThemes } from "../../Settings/Themes";
import { UserInterfaceTheme } from "../../ScriptEditor/NetscriptDefinitions";
interface IProps {
open: boolean;
@ -75,7 +76,7 @@ export function ThemeEditorModal(props: IProps): React.ReactElement {
</Button>
)) || <></>;
function setTheme(theme: ITheme): void {
function setTheme(theme: UserInterfaceTheme): void {
setCustomTheme(theme);
Object.assign(Settings.theme, theme);
ThemeEvents.emit();
@ -105,7 +106,7 @@ export function ThemeEditorModal(props: IProps): React.ReactElement {
ThemeEvents.emit();
}
function setTemplateTheme(theme: ITheme): void {
function setTemplateTheme(theme: UserInterfaceTheme): void {
setTheme(theme);
}

@ -33,8 +33,8 @@ describe("Netscript Dynamic RAM Calculation/Generation Tests", function () {
}
// Runs a Netscript function and properly catches it if it returns promise
function runPotentiallyAsyncFunction(fn) {
const res = fn();
function runPotentiallyAsyncFunction(fn, ...args) {
const res = fn(...args);
if (res instanceof Promise) {
res.catch(() => undefined);
}
@ -48,7 +48,7 @@ describe("Netscript Dynamic RAM Calculation/Generation Tests", function () {
* @param {string[]} fnDesc - describes the name of the function being tested,
* including the namespace(s). e.g. ["gang", "getMemberNames"]
*/
async function testNonzeroDynamicRamCost(fnDesc) {
async function testNonzeroDynamicRamCost(fnDesc, ...args) {
if (!Array.isArray(fnDesc)) {
throw new Error("Non-array passed to testNonzeroDynamicRamCost()");
}
@ -61,7 +61,7 @@ describe("Netscript Dynamic RAM Calculation/Generation Tests", function () {
// We don't need a real WorkerScript
const workerScript = {
args: [],
args: args,
code: code,
dynamicLoadedFns: {},
dynamicRamUsage: RamCostConstants.ScriptBaseRamCost,
@ -91,9 +91,9 @@ describe("Netscript Dynamic RAM Calculation/Generation Tests", function () {
// actually initialized. Call the fn multiple times to test that repeated calls
// do not incur extra RAM costs.
try {
runPotentiallyAsyncFunction(curr);
runPotentiallyAsyncFunction(curr);
runPotentiallyAsyncFunction(curr);
runPotentiallyAsyncFunction(curr, ...args);
runPotentiallyAsyncFunction(curr, ...args);
runPotentiallyAsyncFunction(curr, ...args);
} catch (e) {}
} else {
throw new Error(`Invalid function specified: [${fnDesc}]`);
@ -434,7 +434,7 @@ describe("Netscript Dynamic RAM Calculation/Generation Tests", function () {
it("purchaseServer()", async function () {
const f = ["purchaseServer"];
await testNonzeroDynamicRamCost(f);
await testNonzeroDynamicRamCost(f, "abc", '64');
});
it("deleteServer()", async function () {