mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-05-07 16:17:49 +02:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb431145ee | |||
| 2ab144cff2 | |||
| 7c6d147ff7 | |||
| 4ee26edb85 | |||
| a99197a9ea | |||
| c616ba3b46 | |||
| a7b34b110c | |||
| 15a67d0156 | |||
| 530392eeee | |||
| 2ef68d31ae | |||
| ec307241e0 | |||
| d324a93bac | |||
| d3bdfc44a4 | |||
| 0fb913d158 | |||
| 63c3993995 |
@@ -22,11 +22,11 @@ jobs:
|
|||||||
name: Build Windows
|
name: Build Windows
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.inputs.git-sha || inputs.git-sha || github.sha }}
|
ref: ${{ github.event.inputs.git-sha || inputs.git-sha || github.sha }}
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
@@ -39,14 +39,14 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: npm run electron-win
|
run: npm run electron-win
|
||||||
- name: Upload x64 artifact
|
- name: Upload x64 artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: bitburner-win32-x64
|
name: bitburner-win32-x64
|
||||||
include-hidden-files: true
|
include-hidden-files: true
|
||||||
path: .build/bitburner-win32-x64/*
|
path: .build/bitburner-win32-x64/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
- name: Upload arm64 artifact
|
- name: Upload arm64 artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: bitburner-win32-arm64
|
name: bitburner-win32-arm64
|
||||||
include-hidden-files: true
|
include-hidden-files: true
|
||||||
@@ -57,11 +57,11 @@ jobs:
|
|||||||
name: Build Linux
|
name: Build Linux
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.inputs.git-sha || inputs.git-sha || github.sha }}
|
ref: ${{ github.event.inputs.git-sha || inputs.git-sha || github.sha }}
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
@@ -72,14 +72,14 @@ jobs:
|
|||||||
- name: Build the Electron app
|
- name: Build the Electron app
|
||||||
run: npm run electron-linux
|
run: npm run electron-linux
|
||||||
- name: Upload x64 artifact
|
- name: Upload x64 artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: bitburner-linux-x64
|
name: bitburner-linux-x64
|
||||||
include-hidden-files: true
|
include-hidden-files: true
|
||||||
path: .build/bitburner-linux-x64/*
|
path: .build/bitburner-linux-x64/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
- name: Upload arm64 artifact
|
- name: Upload arm64 artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: bitburner-linux-arm64
|
name: bitburner-linux-arm64
|
||||||
include-hidden-files: true
|
include-hidden-files: true
|
||||||
@@ -90,11 +90,11 @@ jobs:
|
|||||||
name: Build macOS
|
name: Build macOS
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.inputs.git-sha || inputs.git-sha || github.sha }}
|
ref: ${{ github.event.inputs.git-sha || inputs.git-sha || github.sha }}
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
@@ -105,7 +105,7 @@ jobs:
|
|||||||
- name: Build the Electron app
|
- name: Build the Electron app
|
||||||
run: npm run electron-mac
|
run: npm run electron-mac
|
||||||
- name: Upload darwin-universal artifact
|
- name: Upload darwin-universal artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: bitburner-darwin-universal
|
name: bitburner-darwin-universal
|
||||||
include-hidden-files: true
|
include-hidden-files: true
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
needs: [build]
|
needs: [build]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
path: .build
|
path: .build
|
||||||
pattern: bitburner-*
|
pattern: bitburner-*
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ jobs:
|
|||||||
url: ${{ steps.deployment.outputs.page_url }}
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
@@ -38,9 +38,9 @@ jobs:
|
|||||||
else
|
else
|
||||||
npm run build
|
npm run build
|
||||||
fi
|
fi
|
||||||
- uses: actions/upload-pages-artifact@v3
|
- uses: actions/upload-pages-artifact@v5
|
||||||
with:
|
with:
|
||||||
path: ".app"
|
path: ".app"
|
||||||
- name: Deploy to gh pages
|
- name: Deploy to gh pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@v4
|
uses: actions/deploy-pages@v5
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "bitburner",
|
"name": "bitburner",
|
||||||
"version": "3.0.0",
|
"version": "3.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bitburner",
|
"name": "bitburner",
|
||||||
"version": "3.0.0",
|
"version": "3.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@catloversg/steamworks.js": "0.0.3",
|
"@catloversg/steamworks.js": "0.0.3",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bitburner",
|
"name": "bitburner",
|
||||||
"version": "3.0.0",
|
"version": "3.0.1",
|
||||||
"description": "A cyberpunk-themed programming incremental game",
|
"description": "A cyberpunk-themed programming incremental game",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"author": "Daniel Xie, hydroflame, et al.",
|
"author": "Daniel Xie, hydroflame, et al.",
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "bitburner",
|
"name": "bitburner",
|
||||||
"version": "3.0.0",
|
"version": "3.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bitburner",
|
"name": "bitburner",
|
||||||
"version": "3.0.0",
|
"version": "3.0.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "SEE LICENSE IN license.txt",
|
"license": "SEE LICENSE IN license.txt",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bitburner",
|
"name": "bitburner",
|
||||||
"license": "SEE LICENSE IN license.txt",
|
"license": "SEE LICENSE IN license.txt",
|
||||||
"version": "3.0.0",
|
"version": "3.0.1",
|
||||||
"main": "electron-main.js",
|
"main": "electron-main.js",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Daniel Xie, hydroflame, et al."
|
"name": "Daniel Xie, hydroflame, et al."
|
||||||
|
|||||||
+4
-460
@@ -4,8 +4,8 @@
|
|||||||
* Constants for specific mechanics or features will NOT be here.
|
* Constants for specific mechanics or features will NOT be here.
|
||||||
*/
|
*/
|
||||||
export const CONSTANTS = {
|
export const CONSTANTS = {
|
||||||
VersionString: "3.0.0",
|
VersionString: "3.0.1Dev",
|
||||||
isDevBranch: false,
|
isDevBranch: true,
|
||||||
isInTestEnvironment: globalThis.process?.env?.JEST_WORKER_ID !== undefined,
|
isInTestEnvironment: globalThis.process?.env?.JEST_WORKER_ID !== undefined,
|
||||||
VersionNumber: 50,
|
VersionNumber: 50,
|
||||||
|
|
||||||
@@ -111,467 +111,11 @@ export const CONSTANTS = {
|
|||||||
|
|
||||||
// Also update Documentation/doc/en/changelog.md when appropriate (when doing a release)
|
// Also update Documentation/doc/en/changelog.md when appropriate (when doing a release)
|
||||||
LatestUpdate: `
|
LatestUpdate: `
|
||||||
## v3.0.0 Release: 1 May 2026
|
## v3.0.1 Dev: 1 May 2026
|
||||||
|
|
||||||
### BREAKING CHANGES
|
|
||||||
|
|
||||||
- Remove API server (#2084) (@catloversg)
|
|
||||||
- Remove support for running NS1 scripts (#2083) (@catloversg)
|
|
||||||
- Enforce stricter param check on ns.getBitNodeMultipliers and ns.hacknet.spendHashes (#2085) (@catloversg)
|
|
||||||
- Remove APIs that were deprecated a long time ago (#2088) (@catloversg)
|
|
||||||
- Moved formatting functions to their own interface (#1635) (@G4mingJon4s)
|
|
||||||
- Remove fuzzy matching when checking params (#2091) (@catloversg)
|
|
||||||
- Remove deprecated tail-related APIs (#2143) (@catloversg)
|
|
||||||
- Rename setAutoJobAssignment API to setJobAssignment (#2146) (@catloversg)
|
|
||||||
- Make nuke and port cracking APIs return false instead of throwing error (#1023, #2153) (@Hydrogeniouss, @catloversg)
|
|
||||||
- Standardize names of Stock APIs (#2173) (@catloversg)
|
|
||||||
- Rename BN multiplier RepToDonateToFaction to FavorToDonateToFaction (#2178) (@catloversg)
|
|
||||||
- Make ns.bladeburner.getActionRepGain return the expected reputation gain (#2186) (@catloversg)
|
|
||||||
- Rename FactionName.BachmanAssociates to FactionName.BachmanAndAssociates (#2048, #2183) (@masarakki, @catloversg)
|
|
||||||
- Remove DreamSense upgrade (#2232) (@catloversg)
|
|
||||||
- Use different term for dividend modifier instead of tax (#2237) (@catloversg)
|
|
||||||
- Remove Spring Water industry (#2240) (@catloversg)
|
|
||||||
- Remove VeChain (#2245) (@catloversg)
|
|
||||||
- Make some APIs throw error when server is invalid (#2261) (@catloversg)
|
|
||||||
- Rename equipment that uses real brand names (#2293) (@catloversg)
|
|
||||||
- Make TIX access independent from WSE account (#2342) (@catloversg)
|
|
||||||
- Move and rename purchased server functions to cloud API (#2367) (@gmcew)
|
|
||||||
- Fix: Coding contracts may have duplicate names (#2399) (@aaaa-imcute)
|
|
||||||
- Generate test contracts on executing host by default. Add support for optional parameter to specify the server (#2417) (@1337JiveTurkey)
|
|
||||||
- The "darkweb" server becomes a darknet server (Check new Dark Net feature in #2139) (@ficocelliguy)
|
|
||||||
- Remove RAM cost of hacknet namespace and set RAM cost of each hacknet API (#2502) (@catloversg)
|
|
||||||
- Cancel sleeve's current task when calling ns.sleeve.travel() (#2559) (@catloversg)
|
|
||||||
- Make ns.cloud.purchaseServer() and ns.cloud.deleteServer() use hostname as provided (#2560) (@catloversg)
|
|
||||||
- Make implicit string conversion consistent across all coding contracts (#2608) (@catloversg)
|
|
||||||
- Rename ns.gang.getOtherGangInformation to getAllGangInformation (#2635) (@lstutzman)
|
|
||||||
- Standardize "nextCompletion" promise in tasks (#2687) (@catloversg)
|
|
||||||
|
|
||||||
### MAJOR CHANGES
|
### MAJOR CHANGES
|
||||||
|
|
||||||
- Added Darknet, a new mechanic based on spreading through an unstable network and cracking passwords. Purchase DarkscapeNavigator.exe in the terminal to unlock access. (#2139) (@ficocelliguy)
|
- No changes yet, initial dev branch.
|
||||||
- Balance change: IPvGO: Improve favor gain from wins to balance around the rep value of favor (#2131) (@ficocelliguy)
|
|
||||||
- Search and read NS API docs in editor tab and documentation tab (#2163) (@catloversg)
|
|
||||||
- Balance change: Infiltration: Rebalance rewards, add min stat requirement, add market demand (#2210) (@ficocelliguy, @d0sboots, @catloversg)
|
|
||||||
|
|
||||||
### UI
|
|
||||||
|
|
||||||
- Fix: Hacknet's RAM upgrade button is off-by-one (#2093) (@catloversg)
|
|
||||||
- Add visual indicators for tooltips of reputation/favor (#2092) (@catloversg)
|
|
||||||
- Mitigate crash in Terminal page in edge cases (#2099) (@catloversg)
|
|
||||||
- Update favicon files (#2122) (@catloversg)
|
|
||||||
- Add option to enable/disable syncing Steam achievements (#2117) (@catloversg)
|
|
||||||
- Change min and step value of "Tail render interval" setting (#2129) (@catloversg)
|
|
||||||
- Realign and update text in achievement icons (#2127) (@catloversg)
|
|
||||||
- Show achievement lists in grids (#2109) (@catloversg)
|
|
||||||
- Fix styling of IPvGO score modal (#2166) (@ficocelliguy)
|
|
||||||
- Add RAM usage and file size to "ls -l" output (#2135) (@HansLuft778)
|
|
||||||
- Fix: Coding contract UI does not handle error properly when answer format is invalid (#2171) (@catloversg)
|
|
||||||
- Improve Recovery Mode screen (#2206) (@catloversg)
|
|
||||||
- Add "Recent Errors" tab and improved error modal (#2169) (@ficocelliguy)
|
|
||||||
- Validate bet input of casino mini games (#1694) (@catloversg)
|
|
||||||
- Scroll to top when opening new NS API doc page in popup mode (#2181) (@catloversg)
|
|
||||||
- Fix nbsp rendering as text rather than a space (#2225) (@gmcew)
|
|
||||||
- Show terminal warning instead of popup for breaking changes (#2244) (@catloversg)
|
|
||||||
- Show money in exponential form instead of "0.000" for dividends when it's > 0 but still too small (#2243) (@catloversg)
|
|
||||||
- Place tooltips and popups in front of log windows (#2253) (@catloversg)
|
|
||||||
- Fix: Go history page shows favor bonus instead of reputation bonus (#2251) (@catloversg)
|
|
||||||
- Fix: Error message shows blob URL instead of script name (#2265) (@catloversg)
|
|
||||||
- Add tooltip for reputation/favor in page of faction's augmentation list (#2268) (@catloversg)
|
|
||||||
- Fix: Dropdown list appears behind modal when it's used in modal (#2282) (@catloversg)
|
|
||||||
- Close coding contract popup on prestige (#2285) (@catloversg)
|
|
||||||
- Add "Run Beautify on Save" option for built-in editor (#2287) (@TheCleric)
|
|
||||||
- Add configurable option for auto-reconnecting to RFA client (#2297) (@catloversg)
|
|
||||||
- Fix: Active Scripts page may mix up UI after prestige (#2303) (@catloversg)
|
|
||||||
- Better status bar animations (#2317) (@d0sboots)
|
|
||||||
- Prevent text from overflowing when script's args are too long in Active Scripts page (#2324) (@catloversg)
|
|
||||||
- Print error detail to terminal if script cannot be compiled or there is runtime error while autocompleting (#2328) (@catloversg)
|
|
||||||
- Fix: Color picker appears behind theme editor (#2321) (@catloversg)
|
|
||||||
- Allow automatically hiding the top menu in Electron (#2325) (@femboyfireball)
|
|
||||||
- Clarify why you can't buy 4S when disabled (#2311) (@femboyfireball)
|
|
||||||
- Warn player if they run no-arg programs with arguments (#2338) (@catloversg)
|
|
||||||
- Show server's money in exponential form if it's too small (#2343) (@catloversg)
|
|
||||||
- Format the shared RAM multipler on the factions (#2355) (@mctylr-gh)
|
|
||||||
- Fix: Electron app does not show achievement icons (#2362) (@catloversg)
|
|
||||||
- Reword tutorial steps to distinguish between instruction and example (#2370) (@catloversg)
|
|
||||||
- Prevent showing redundant error toasts on failed RFA auto connect attempts (#2381) (@shyguy1412)
|
|
||||||
- Fix: Company's job list shows wrong starting jobs (#2391) (@catloversg)
|
|
||||||
- Improve instructions in recovery mode (#2394) (@catloversg)
|
|
||||||
- Add option to set fractional digits (#2419) (@CTNOriginals)
|
|
||||||
- Add links to documentation pages in tutorial's last step (#2424) (@catloversg)
|
|
||||||
- Show useful error messages when loading unsupported save data from newer versions (#2425) (@catloversg)
|
|
||||||
- Change notice for breaking changes in v2 (#2426) (@catloversg)
|
|
||||||
- Do not show error popup related to Stanek's Gift data when loading save files from pre-v1.1.0 (#2429) (@catloversg)
|
|
||||||
- Add Script Editor toggle for Sticky Scroll (#2431) (@gmcew)
|
|
||||||
- Display contract answers on completely failed contracts (#2440) (@TheCleric)
|
|
||||||
- Remove BitNode's difficulty in BitNode selection popup (#2454) (@catloversg)
|
|
||||||
- Add settings to change currency symbol and whether to show it after money value (#2453) (@kaoticengineering)
|
|
||||||
- Fix: Monaco shows wrong TSDoc of APIs that have "export" word (#2473) (@catloversg)
|
|
||||||
- Show karma in Stats page and explain what it is in documentation (#2475) (@catloversg)
|
|
||||||
- Update icon of DARKNET_BACKDOOR and DARKNET_DEPTHS achievements (#2480) (@catloversg)
|
|
||||||
- Update incorrect ps command shown in help (#2484) (@Groot-0909)
|
|
||||||
- Improve help text of scp and run (#2493) (@catloversg)
|
|
||||||
- Add option to disable minimap in script editor (#2504) (@catloversg)
|
|
||||||
- Improve navigation system of in-game documentation viewer (#2499) (@catloversg)
|
|
||||||
- Use font family setting when rendering MUI Link component (#2511) (@catloversg)
|
|
||||||
- Show hints of BitNode documentation and allow opening it in BitVerse (#2513) (@catloversg)
|
|
||||||
- Show errors if using nano/vim with patterns that do not match any files (#2515) (@catloversg)
|
|
||||||
- Tweak CSS/Position of Darknet Docs link (#2517) (@d0sboots)
|
|
||||||
- Add indicator of RFA connection status to overview panel (#2497) (@catloversg)
|
|
||||||
- Fix issues with RFA auto-reconnecting feature (#2535) (@catloversg)
|
|
||||||
- Add inline script RAM usage text to each active script (#2546) (@vadien)
|
|
||||||
- Update toolbar of in-game editor (#2551) (@catloversg)
|
|
||||||
- Show "undefined" instead of -1 as pid in error popup when catching promise errors (#2555) (@catloversg)
|
|
||||||
- Add option to autosave scripts on focus change (#2565) (@catloversg)
|
|
||||||
- Prevent joining banned factions via UI (#2573) (@catloversg)
|
|
||||||
- Fix: Import save comparison popup shows wrong BN level (#2595) (@catloversg)
|
|
||||||
- Fix: Cannot type in text boxes rendered by players' scripts when terminal tab is shown (#2615, #2622) (@lstutzman, @catloversg)
|
|
||||||
- Navigate to gym/university instead of city when stopping focusing on gym/class work (#2613) (@lstutzman)
|
|
||||||
- Ensure prompts shown by ns.prompt do not lose focus in the terminal tab (#2631) (@catloversg)
|
|
||||||
- Remove unnecessary max-width of tab list in in-game editor (#2643) (@catloversg)
|
|
||||||
- Add hooks to sidebar for players to attach custom content (#2651) (@catloversg)
|
|
||||||
- Use exponential notation when formatting very small HP or thread values (#2656) (@catloversg)
|
|
||||||
- Show effective amount of shared RAM when using UI (#2691) (@catloversg)
|
|
||||||
- Use "success" theme color for low infiltration difficulty instead of "primary" (#2693) (@catloversg)
|
|
||||||
- Reload immediately after importing, deleting save data or killing all scripts (#2697) (@catloversg)
|
|
||||||
- Activate recovery mode on critical BN prestige bugs (#2699) (@catloversg)
|
|
||||||
|
|
||||||
### MISC
|
|
||||||
|
|
||||||
- Ensure IPvGO board has at least 1 offline node (#2072) (@ficocelliguy)
|
|
||||||
- Fix: Game crashes when generating CCT in weird case (#2077) (@catloversg)
|
|
||||||
- Add ns.dynamicImport() to dynamically import a script (#2036) (@shyguy1412)
|
|
||||||
- Add functionality and support to fully allow Players to use IP addresses in place of hostnames (#1990) (@NagaOuroboros)
|
|
||||||
- Fix: IPvGO tutorial was getting stuck if you left the tab and returned (#2071) (@ficocelliguy)
|
|
||||||
- Fix: Passive faction reputation gain applies Player.mults.faction_rep twice (#2125) (@catloversg)
|
|
||||||
- Fix: Electron app does not run on Linux due to incompatible glibc version and wrong usage of net.fetch (#2114) (@catloversg)
|
|
||||||
- Change how enums are exposed in NetscriptDefinitions.d.ts (#1998) (@catloversg)
|
|
||||||
- Add formulas API for calculating share power and move UI of sharing RAM (#2126) (@catloversg)
|
|
||||||
- Use FactionName enum in relevant APIs (#2101) (@catloversg)
|
|
||||||
- Prevent running multiple instances of Electron app (#2095) (@catloversg)
|
|
||||||
- Tweak "The Covenant" faction's rumor condition (#2110) (@catloversg)
|
|
||||||
- Export crash report when a fatal error occurs (#2106) (@catloversg)
|
|
||||||
- Correctly end game & winstreak if a cheat attempt critically fails (#2130) (@ficocelliguy)
|
|
||||||
- Show custom error message when player imports decompressed save file (#2108) (@catloversg)
|
|
||||||
- Add "Total Number of Primes" contract (#2116) (@gmcew)
|
|
||||||
- Make IP addresses use the full 32 bit space (#2113) (@whiskeyfur)
|
|
||||||
- Add "--tail" to default autocomplete options (#2103) (@catloversg)
|
|
||||||
- Update blood donation (#2151, #2216, #2508) (@catloversg, @hydroflame, @d0sboots)
|
|
||||||
- Fix typo in sector-12-crime.lit (#2159) (@Boingostarr)
|
|
||||||
- Add versionNumber to ns.ui.getGameInfo() (#2155) (@catloversg)
|
|
||||||
- Fix typos in literature files (#2164) (@catloversg)
|
|
||||||
- Add more enums to ns.enums (#2165) (@catloversg)
|
|
||||||
- Add removal of fuzzy matching to list of breaking changes (#2149) (@catloversg)
|
|
||||||
- Fix: API break detector does not detect affected code in some cases (#2172) (@catloversg)
|
|
||||||
- Mention bug-report channel on Discord for reporting bugs (#2201) (@catloversg)
|
|
||||||
- Add file metadata (timestamps) (#1271, #2199) (@Hoekstraa, @catloversg)
|
|
||||||
- Allow using E notation in expr CLI (#2209) (@catloversg)
|
|
||||||
- Fix: Current work is not shown in edge cases (#2208) (@catloversg)
|
|
||||||
- Make ns.codingcontract.createDummyContract throw error if type is invalid (#2188) (@catloversg)
|
|
||||||
- Fix: Documentation navigator does not handle external URL properly (#2202) (@catloversg)
|
|
||||||
- Detect circular dependencies when generating modules (#2194) (@catloversg)
|
|
||||||
- Fix: Running scripts may be loaded before main UI (#1726) (@catloversg)
|
|
||||||
- Fix: Autocomplete of "connect" command does not list purchased servers (#2229) (@gmcew)
|
|
||||||
- Expose difficulty value of coding contract in NS API (#2230) (@gmcew)
|
|
||||||
- Fix: Exporting game via menu of Steam app does not give export bonus (#2241) (@catloversg)
|
|
||||||
- Update migration instruction for breaking change of ns.nFormat (#2247) (@catloversg)
|
|
||||||
- Fix: Loading code discards entire Go data due to missing migration code for favor/rep (#2252) (@catloversg)
|
|
||||||
- Add removal of API server to list of breaking changes (#2205) (@catloversg)
|
|
||||||
- Fix typo in KARMA_1000000 achievement (#2264) (@UncleCeiling)
|
|
||||||
- Clarify the reason of failure when trying to move a running script (#2160) (@UncleCeiling)
|
|
||||||
- Fix: ns.mv writes to destination file even if it cannot delete source file (#2267) (@catloversg)
|
|
||||||
- Update messages related to text files (#2266) (@catloversg)
|
|
||||||
- Fix calculateExp so that it won't return a too small result (#2274) (@d0sboots)
|
|
||||||
- Change ns.alert to accept multiple args as other print functions (#2278) (@vamo89)
|
|
||||||
- Fix: Coding contract can be solved manually via UI after it is removed on prestige (#2281) (@TheCleric)
|
|
||||||
- Ensure ns.go.analysis.getValidMoves correctly handles playing as white (#2292) (@ficocelliguy)
|
|
||||||
- Ensure that player's promises are changed to "gameOver" once the game is over (#2198) (@ficocelliguy)
|
|
||||||
- Export save data before migrating to v3 (#2304) (@catloversg)
|
|
||||||
- Prevent tiny islands surrounded by offline nodes during initial board generation (#2310) (@ficocelliguy)
|
|
||||||
- Add FragmentType to NS Enums (#2341) (@TheCleric)
|
|
||||||
- Do not round down amount of hacked money in "hack" CLI (#2344, #2345) (@catloversg)
|
|
||||||
- Rename "TIX" interface to "Stock" (#2351) (@catloversg)
|
|
||||||
- Add --temporary flag to run command (#2354) (@catloversg)
|
|
||||||
- Let ServerProfiler.exe autocomplete servers (#2356) (@rladenson)
|
|
||||||
- Correct phrasing in SmartSonar description (#2368) (@The-Chaddeus)
|
|
||||||
- Make ActiveFragment extend Fragment (#2373) (@catloversg)
|
|
||||||
- Expose theme as css custom props (#2380) (@shyguy1412)
|
|
||||||
- Support css file type (#2378) (@shyguy1412)
|
|
||||||
- Do not update captures on passed analysis boards (#2415) (@ficocelliguy)
|
|
||||||
- Update error message when backdooring without admin rights (#2435) (@CicaProductions)
|
|
||||||
- Add literature expanding the augment prestige lore (#2433) (@Nick-Colclasure)
|
|
||||||
- Improve script args validation message (#2451) (@ficocelliguy)
|
|
||||||
- Add sleeve commands to purchase sleeves and memory via the API (#2443) (@TheAimMan)
|
|
||||||
- Allow ns.read to read .msg and .lit files (#2455) (@catloversg)
|
|
||||||
- Fix scoring of very large open areas in IPvGO (#2464) (@ficocelliguy)
|
|
||||||
- Make threads' bonus apply to timing attack puzzle's extra time per char (#2466) (@ficocelliguy)
|
|
||||||
- Improve documentation; remove requirement that getBlockedRam and getDepth be called on a darknet server (#2472) (@ficocelliguy)
|
|
||||||
- Prevent blocked ram on the "darkweb" server (#2468) (@ficocelliguy)
|
|
||||||
- Add extra hint to sorted echo puzzle at high levels (#2465) (@ficocelliguy)
|
|
||||||
- Change coding contract's fallback reward priorities (#2481) (@gmcew)
|
|
||||||
- Add JS object properties as server names; refactor save/load/server storage to support this (#2482) (@ficocelliguy)
|
|
||||||
- Buff packet sniffing slightly (#2485) (@d0sboots)
|
|
||||||
- Fix: Darknet state is not reset properly on prestige (#2486) (@catloversg)
|
|
||||||
- Change how coding contract rewards are randomized (#2490) (@catloversg)
|
|
||||||
- Expose ProgramName enum (#2492) (@catloversg)
|
|
||||||
- More fixes and feedback in darknet (#2489) (@ficocelliguy)
|
|
||||||
- Fix missed cases in offline server handling (#2495) (@d0sboots)
|
|
||||||
- Adjust darknet balance from player feedback (#2512) (@ficocelliguy)
|
|
||||||
- Add "Find Largest Rectangle in a Matrix" coding contract (#2519) (@Misha279-UA)
|
|
||||||
- Tweak Dnet based on player feedback (#2533, #2545, #2593) (@ficocelliguy)
|
|
||||||
- Allow parsing unknown options with data.flags in autocomplete (#2539) (@catloversg)
|
|
||||||
- Fix webstorm by using a mutationLock (#2542) (@d0sboots)
|
|
||||||
- Print error message when calling ns.ui.closeTail with nonexistent pid or pid of stopped scripts (#2557) (@catloversg)
|
|
||||||
- Add minimum width/height constraints to ns.ui.resizeTail (#2558) (@catloversg)
|
|
||||||
- Add API to minimize and expand tail windows (#2556) (@catloversg)
|
|
||||||
- Improve error messages for invalid sleeve numbers (#2567) (@catloversg)
|
|
||||||
- Improve help text of expr command (#2561) (@catloversg)
|
|
||||||
- Rework faction rumor (#2569) (@catloversg)
|
|
||||||
- Fix: hacknetNodeCost formula API throws when using documented optional parameter (#2577) (@catloversg)
|
|
||||||
- Rework intelligence override (#2575) (@catloversg)
|
|
||||||
- Electron: Allow opening dev tools via CLI arguments (#2589) (@catloversg)
|
|
||||||
- Support importing Steam Cloud save file manually (#2583) (@catloversg)
|
|
||||||
- Electron: Add UI menus and CLI flags to change log levels (#2596) (@catloversg)
|
|
||||||
- Import correct cloud file when multiple exist (#2599) (@catloversg)
|
|
||||||
- Dnet: Remove packet capture (#2594) (@ficocelliguy)
|
|
||||||
- Generate more frequent and lower-reward coding contracts (#2603) (@ficocelliguy)
|
|
||||||
- Electron: Fix issues in edge cases of using --export-save (#2590) (@catloversg)
|
|
||||||
- Fix recursive alias detection causing infinite recursion (#2610) (@lstutzman)
|
|
||||||
- Add "hidden" mkdir command (#2646) (@catloversg)
|
|
||||||
- Dnet: Remove bonus time effect on authentication and heartbleed speed; fix ram rounding (#2627) (@ficocelliguy)
|
|
||||||
- Fix tab completion for multi-word quoted autocomplete options (#2612) (@lstutzman)
|
|
||||||
- Add weakenEffect to formulas.hacking namespace (#2626) (@lstutzman)
|
|
||||||
- Update description of "cat" in "help" command (#2654) (@catloversg)
|
|
||||||
- Reduce achievements check interval (#2650) (@catloversg)
|
|
||||||
- Fix: calculateExp throws errors in edge cases (#2667) (@catloversg)
|
|
||||||
- Clear recent scripts when installing augmentations (#2670) (@Mathekatze)
|
|
||||||
- Better error message for port serialization failure (#2688) (@d0sboots)
|
|
||||||
- Adjust charisma augmentation power (#2698) (@ficocelliguy)
|
|
||||||
- Clarify what "backdoor" does in "help" command (#2694) (@abbyintheattic)
|
|
||||||
- Fix: Hacknet upgrade cost formulas ignore upgrade level when enforcing max level (#2696) (@lucebac)
|
|
||||||
- Improve error messages for invalid module parsing (#2707) (@catloversg)
|
|
||||||
|
|
||||||
### DOCUMENTATION
|
|
||||||
|
|
||||||
- Fix wrong links in some docs pages (#2082) (@mizmantle)
|
|
||||||
- Add getContractTypes to Coding Contracts documentation page (#2089) (@gmcew)
|
|
||||||
- Clarify applicability of wiki entry in Hamming contracts (#2087) (@gmcew)
|
|
||||||
- Make minor improvements in "Getting Started" page (#2112) (@catloversg)
|
|
||||||
- Remove mention of Netscript 1.0/2.0/JS in NetscriptDefinitions.d.ts (#2150) (@catloversg)
|
|
||||||
- Clarify ns.go.getGameState (#2158) (@Arjan-akkermans)
|
|
||||||
- Remove unnecessary br tag in scripts.md (#2204) (@catloversg)
|
|
||||||
- Fix minor problem in old changelog (#2203) (@catloversg)
|
|
||||||
- Remove mention of NS2 in NetscriptDefinitions.d.ts (#2200) (@catloversg)
|
|
||||||
- Move CONTRIBUTING.md (#2191) (@catloversg)
|
|
||||||
- Specify parameter types of of ns.go.analysis.highlightPoint and ns.go.analysis.clearPointHighlight (#2175) (@kevinsandow)
|
|
||||||
- Fix warnings when generating NS API docs (#2189) (@kevinsandow)
|
|
||||||
- Support showing images in markdown docs (#2207) (@catloversg)
|
|
||||||
- Update links to CONTRIBUTING.md (#2213) (@catloversg)
|
|
||||||
- Update contribution guide (#2214) (@emmanuel-ferdman)
|
|
||||||
- Clarify condition of joining Daedalus faction (#2234) (@catloversg)
|
|
||||||
- Update infiltration docs (#2259) (@catloversg)
|
|
||||||
- Mention TypeScript support and update example in React docs (#2263) (@catloversg)
|
|
||||||
- Update Remote API docs (#2258) (@catloversg)
|
|
||||||
- Change ns.tail to ns.ui.openTail in example code (#2270) (@gmcew)
|
|
||||||
- Clarify description of ArrayJumpingGame contract (#2277) (@acidduk)
|
|
||||||
- Update 'Hacking Algorithms' page (#2288) (@gmcew)
|
|
||||||
- Reword HammingCode contracts and mention dummy contract API (#2296) (@gmcew)
|
|
||||||
- Clarify cross-host characteristic of PID and port (#2336) (@Thaccus)
|
|
||||||
- Fix wrong commands in getting_started.md (#2349) (@catloversg)
|
|
||||||
- Use alternative fix for newline issue in IPvGO docs (#2350) (@catloversg)
|
|
||||||
- Fix missing/wrong TSDoc of APIs having optional host parameter (#2371) (@catloversg)
|
|
||||||
- Fix typo in README.md (#2375) (@HolyNaet)
|
|
||||||
- Improve wording around host argument of getScriptRam (#2374) (@nickmshelley)
|
|
||||||
- Remove references to defunct wiki from Hack/Weaken/Grow (#2404) (@maglinvinn)
|
|
||||||
- Clarify behavior of NS.getScriptLogs() for running scripts (#2408) (@sg673)
|
|
||||||
- Add instructions to troubleshoot common issues for contributors to CONTRIBUTING.md (#2420) (@catloversg)
|
|
||||||
- Add FAQ and another JIT batcher illustration to documentation (#2400) (@HolyNaet)
|
|
||||||
- Change wording in Singularity.installAugmentation (#2439) (@HolyNaet)
|
|
||||||
- Update README.md to correct Frequently Asked Questions link. (#2444) (@jonathonchase)
|
|
||||||
- Add a question and a grammatical fix in faq.md (#2446) (@HolyNaet)
|
|
||||||
- Move current changelog to changelog-v2.md (#2461) (@catloversg)
|
|
||||||
- Fix some IPvGO docs that do not reflect winstreak rep converted to favor (#2463) (@ficocelliguy)
|
|
||||||
- Add glossary of darknet terms to the documentation page (#2469) (@ficocelliguy)
|
|
||||||
- Add a note to CONTRIBUTING.md about the npm peer dep issue (#2474) (@d0sboots)
|
|
||||||
- Update darknet documentation (#2483) (@ficocelliguy)
|
|
||||||
- Fix invalid links in ns.sleep and ns.asleep (#2496) (@catloversg)
|
|
||||||
- Use relative links instead of absolute links (#2521) (@catloversg)
|
|
||||||
- Document quirky behavior of ns.flags when default value is nullish (#2528) (@catloversg)
|
|
||||||
- Clarify how share power affects reputation gain rate of non-hacking work (#2544) (@catloversg)
|
|
||||||
- Update guides (#2550) (@catloversg)
|
|
||||||
- Add missing newline after RAM cost (#2570) (@catloversg)
|
|
||||||
- Update mention of outdated getStockForecast API (#2578) (@catloversg)
|
|
||||||
- Fix newline issues in IPvGO docs and add missing RAM cost (#2602) (@catloversg)
|
|
||||||
- Document coding contract's generation and rewards (#2624) (@catloversg)
|
|
||||||
- Clarify scp and exec darknet permissions in API docs (#2634) (@lstutzman)
|
|
||||||
- Update RAM cost of hacknet APIs and remove unnecessary RAM cost docs (#2639) (@catloversg)
|
|
||||||
- Update tutorial script for buying cloud servers (#2653) (@catloversg)
|
|
||||||
- Improve coding contract documentation (#2689) (@catloversg)
|
|
||||||
|
|
||||||
### SPOILER CHANGES - UI
|
|
||||||
|
|
||||||
- Fix: BitVerse does not show all BN multipliers in some cases (#2045) (@catloversg)
|
|
||||||
- Add UI hint about scripting tea/party and using Intern (#2179) (@catloversg)
|
|
||||||
- Fix: Bladeburner console prints main body's HP instead of sleeve's HP (#2390) (@catloversg)
|
|
||||||
- Reduce threshold of showing warning of low population (#2450) (@catloversg)
|
|
||||||
- Fix: Hacknet server UI shows NaN hash rate when 100% RAM is being used (#2500) (@catloversg)
|
|
||||||
- Prevent ending BNs through reuse of Bladeburner UI event handler (#2574) (@catloversg)
|
|
||||||
- Always show Black Operations list (#2592) (@catloversg)
|
|
||||||
- Show hints of Sleeves mechanic in pre-endgame (#2605) (@catloversg)
|
|
||||||
- Consistently calculate BitNode "level" (#2645) (@catloversg)
|
|
||||||
- Add tooltips explaining why Bladeburner skill upgrades are disabled (#2648) (@catloversg)
|
|
||||||
- Add button to open Faction page from Gang UI (#2655) (@catloversg)
|
|
||||||
- Ensure intelligence override is a positive integer (#2673) (@catloversg)
|
|
||||||
- Remove max width of corporation division list (#2686) (@catloversg)
|
|
||||||
|
|
||||||
### SPOILER CHANGES - MISC
|
|
||||||
|
|
||||||
- Add achievement for acquiring SF13.1 (#2107) (@catloversg)
|
|
||||||
- Add achievement for completing all BNs (#2128) (@catloversg)
|
|
||||||
- Renamed Division.type to Division.industry (#2079, #2152) (@whiskeyfur, @catloversg)
|
|
||||||
- Make ns.hacknet.spendHashes handle invalid targets in same way as UI (#2102) (@catloversg)
|
|
||||||
- Add achievements for BN14 (#2140) (@catloversg)
|
|
||||||
- Print logs when ns.hacknet.spendHashes fails and update param type of APIs using hash upgrade (#2145) (@catloversg)
|
|
||||||
- Add ns.singularity.getUnlockedAchievements (#2156) (@UncleCeiling)
|
|
||||||
- Remove mention of unusable research "sudo.Assist" (#2187) (@catloversg)
|
|
||||||
- Change message of Singularity error and uncaught promise error (#2174) (@catloversg)
|
|
||||||
- Add ns.singularity.getHackingLevelRequirementOfProgram (#2271) (@catloversg)
|
|
||||||
- Expose gang's discount rate (#2272) (@catloversg)
|
|
||||||
- Prevent purchasing product-only research for material industries (#2283) (@catloversg)
|
|
||||||
- Fix: Stat levels are not recalculated after grafting augs or accepting Stanek's Gift (#2322) (@catloversg)
|
|
||||||
- Expose production limit of material and product (#2330) (@AnteIndustrial)
|
|
||||||
- Make some editorial changes in Black Operations' description (#2449) (@catloversg)
|
|
||||||
- Add warning when installing backdoor on backdoored server with Singularity API (#2458) (@catloversg)
|
|
||||||
- Fix: Sleeves can earn exp and purchase augmentations when enabling disableSleeveExpAndAugmentation (#2467) (@catloversg)
|
|
||||||
- Add improved challenge achievement for BN15 (#2479) (@ficocelliguy)
|
|
||||||
- Stop randomizing Bladeburner's action difficulty (#2491) (@catloversg)
|
|
||||||
- Adjusted Bladeburner's team bonus computation to make one member help (#2541) (@JoshuaCF)
|
|
||||||
- Rebalance charisma exp gain of Recruitment action (#2549) (@catloversg)
|
|
||||||
- Add APIs to get rank gain and rank loss of an action (#2572) (@catloversg)
|
|
||||||
- Reduce RAM cost of inGang and inBladeburner APIs (#2582) (@catloversg)
|
|
||||||
- Fix skillMaxUpgradeCount returning 1 at extreme skill levels (#2611) (@lstutzman)
|
|
||||||
- API: Expose charged effects of Stanek's Gift active fragments (#2638) (@catloversg)
|
|
||||||
- Apply SF override to charisma calculations (#2642) (@catloversg)
|
|
||||||
- Update description of "BN9: Challenge" achievement (#2647) (@catloversg)
|
|
||||||
- Fix: Intelligence data is incorrectly migrated when Intelligence is not unlocked (#2660, #2666) (@catloversg)
|
|
||||||
- Restrict team count of Ops/BlackOps to total team size (#2672) (@catloversg)
|
|
||||||
- Fix: DarkscapeNavigator program is not granted on BN prestige when having SF15 (#2690) (@catloversg)
|
|
||||||
- Prevent duplicate processing of boost materials (#2695) (@catloversg)
|
|
||||||
- Update error message of ns.singularity.joinFaction (#2700) (@catloversg)
|
|
||||||
- Update RAM cost of nextUpdate and similar APIs (#2702) (@catloversg)
|
|
||||||
- Update description of "You and what army?" achievement (#2703) (@catloversg)
|
|
||||||
- Rework material production limit (#2683) (@catloversg)
|
|
||||||
- Remove references to "sudo.Assist" research (#2709) (@catloversg)
|
|
||||||
|
|
||||||
### SPOILER CHANGES - DOCUMENTATION
|
|
||||||
|
|
||||||
- Update rewards of SF14 (#2142) (@catloversg)
|
|
||||||
- Fix missing and outdated info of optional parameter (#2176) (@catloversg)
|
|
||||||
- Specify param type of many Corporation APIs (#2190) (@catloversg)
|
|
||||||
- Fix typo in bitnodes.md (#2221) (@catloversg)
|
|
||||||
- Add short guide of BitNode recommendation (#2041) (@1337JiveTurkey)
|
|
||||||
- Add comprehensive guide of BitNode recommendation (#2044) (@catloversg)
|
|
||||||
- Fix outdated description of Bladeburner interface (#2226) (@catloversg)
|
|
||||||
- Clarify advice about Smart Supply in round 1 (#2233) (@catloversg)
|
|
||||||
- Fix wrong param/return type and clarify ns.sleep, ns.asleep, ns.singularity.exportGame (#2242) (@catloversg)
|
|
||||||
- Clarify ns.gang.getOtherGangInformation (#2289) (@acidduk)
|
|
||||||
- Update type of Player.factions, GangGenInfo.faction and CorpMaterialConstantData.name (#2347) (@catloversg)
|
|
||||||
- Clarification of Singularity costs inside BN4 (#2403) (@gmcew)
|
|
||||||
- Update math notations in corporation docs (#2452) (@catloversg)
|
|
||||||
- Update BitNode recommendation short guide (#2523, #2529) (@catloversg)
|
|
||||||
|
|
||||||
### CODEBASE/REFACTOR/WORKFLOW/JEST/TOOL/DEPS
|
|
||||||
|
|
||||||
- Electron: Use steamworks.js to integrate with Steamworks SDK (#1563) (@catloversg)
|
|
||||||
- Electron: Replace deprecated protocol.interceptFileProtocol with protocol.handle (#2100) (@catloversg)
|
|
||||||
- Dev menu: Add tools to set stat level and queue augmentations of faction (#2118) (@catloversg)
|
|
||||||
- Remove wrong and irrelevant comments/code (#2111) (@catloversg)
|
|
||||||
- Refactor: Change repNeededToDonate to favorNeededToDonate (#2134) (@d0sboots)
|
|
||||||
- Workflow: Build artifacts and upload to release (#2120) (@catloversg)
|
|
||||||
- Configure .editorconfig to not trim trailing whitespace in NetscriptDefinitions.d.ts (#2137) (@G4mingJon4s)
|
|
||||||
- Fix typo of "CorruptableText" (#2144) (@catloversg)
|
|
||||||
- Dev menu: Fix bugs in WD tools (#2141) (@catloversg)
|
|
||||||
- Make prettier ignore src/Documentation/pages.ts (#2185) (@catloversg)
|
|
||||||
- Workflow: Allow enabling dev mode when deploying dev build (#2184) (@catloversg)
|
|
||||||
- Refactor: Refactor message handler of Remote API (#2211) (@catloversg)
|
|
||||||
- Dev menu: Auto expand last opened tools (#2182) (@catloversg)
|
|
||||||
- Update build script to avoid unnecessary newline in generated script (#2224) (@catloversg)
|
|
||||||
- Fix inconsistent generated pages.ts (#2236) (@catloversg)
|
|
||||||
- Dev menu: Reset hacknet server list properly when setting level of SF9 (#2177) (@catloversg)
|
|
||||||
- Update dev dependencies (#2246) (@catloversg)
|
|
||||||
- Move all docs into en/ subdirectory (#1505) (@d0sboots)
|
|
||||||
- Fix ctrl-clicking after the doc_en refactor (#2256) (@d0sboots)
|
|
||||||
- Electron: Specify mime type when loading wasm files (#2262) (@catloversg)
|
|
||||||
- Electron: Build universal macOS binary (#2306) (@Snarling)
|
|
||||||
- Add Jest test to test migration from v2 to v3 (#2305) (@catloversg)
|
|
||||||
- Workflow: Split build-artifacts.yml into 2 workflows (#2309) (@catloversg)
|
|
||||||
- Workflow: Update NodeJs version in workflows (#2319) (@catloversg)
|
|
||||||
- Update NodeJS to v22 (#2318) (@mctylr-gh)
|
|
||||||
- Update api-documentor and api-extractor (#2320) (@mctylr-gh)
|
|
||||||
- Improve dev menu for augmentations (#2315) (@d0sboots)
|
|
||||||
- Add more debug info for troubleshooting corporation issues (#2327) (@catloversg)
|
|
||||||
- Update doc.sh (#2331) (@catloversg)
|
|
||||||
- Enable restoreMocks option and fix lint errors (#2333) (@catloversg)
|
|
||||||
- Workflow: Add new job to check generated docs (#2329) (@catloversg)
|
|
||||||
- Quiet Jest tests output on expected failures (#2332) (@mctylr-gh)
|
|
||||||
- Standardize error handling in netscriptGoImplementation.ts (#2335) (@ficocelliguy)
|
|
||||||
- Enable linting in test folder (#2337) (@catloversg)
|
|
||||||
- Update minor version of material UI and Emotion packages (#2339) (@mctylr-gh)
|
|
||||||
- Refactor Stanek's Gift UI code and change internal FragmentType enum (#2346) (@catloversg)
|
|
||||||
- Minor React related packages update (#2353) (@mctylr-gh)
|
|
||||||
- Remove redundant check in getServer utility function and serverExists API (#2357) (@catloversg)
|
|
||||||
- Refactor tests that add home server manually (#2358) (@catloversg)
|
|
||||||
- Update Electron version (#2360) (@catloversg)
|
|
||||||
- Prepare for save data migration of next beta version (#2369) (@catloversg)
|
|
||||||
- Rewrite infiltration to pull state out of React (#2316, #2393) (@d0sboots)
|
|
||||||
- Use getCoreBonus function when calculating server growth (#2387) (@aaaa-imcute)
|
|
||||||
- Refactor implementation of ports (#2396) (@d0sboots)
|
|
||||||
- Avoid re-entrancy issues with EventEmitter (#2397) (@d0sboots)
|
|
||||||
- Update packages-lock.json to not use vulnerable versions (#2405) (@mctylr-gh)
|
|
||||||
- Allow specifying migrator when migrating player's scripts (#2418) (@catloversg)
|
|
||||||
- Refactor code related to in-game documentation link (#2422) (@catloversg)
|
|
||||||
- Clean up "doc" folder (#2427) (@catloversg)
|
|
||||||
- Update dependencies related to monaco (#2432) (@catloversg)
|
|
||||||
- Fix typos and duplicating ms per cycle constant (#2436) (@gmcew)
|
|
||||||
- Update dependencies to fix vulnerability warnings (#2445) (@catloversg)
|
|
||||||
- Move Result to the public API (#2398) (@d0sboots)
|
|
||||||
- Generate display data for math notation at built time and remove runtime mathjax dependency (#2447) (@catloversg)
|
|
||||||
- Update SaveData type to be compatible with TS 5.9 and upgrade TS (#2457) (@catloversg)
|
|
||||||
- Update NodeJS to v24 (#2456) (@catloversg)
|
|
||||||
- Update format created by "build artifacts" workflows (#2470) (@Snarling)
|
|
||||||
- Consolidate checks under getFailureResult() (#2476) (@d0sboots)
|
|
||||||
- Fix: Global states are not reset properly between each Jest test (#2487) (@catloversg)
|
|
||||||
- Change getFailureResult to checkDarknetServer (#2494) (@d0sboots)
|
|
||||||
- Speed up by-ip lookups by introducing a new map entry (#2488) (@d0sboots)
|
|
||||||
- Refactor/adjust getPixelPosition in darknet UI code (#2501) (@d0sboots)
|
|
||||||
- Add tests for checking getAnswer and solver of coding contracts (#2503) (@catloversg)
|
|
||||||
- Refactor ImportSave component (#2505) (@catloversg)
|
|
||||||
- Show coding contract names when their tests failed (#2520) (@catloversg)
|
|
||||||
- Workflow: Fix wrong instruction of generating docs (#2522) (@catloversg)
|
|
||||||
- Update comment of LoadingScreen of ComplexPage enum (#2527) (@catloversg)
|
|
||||||
- Make getPlayer 10x faster (#2548) (@d0sboots)
|
|
||||||
- Create monaco editor instance with null model (#2563) (@catloversg)
|
|
||||||
- Update dependencies (#2576) (@catloversg)
|
|
||||||
- Remove barrel imports in Bladeburner code (#2580) (@catloversg)
|
|
||||||
- Fix React warning in IPvGO scoring explanation popup (#2581) (@catloversg)
|
|
||||||
- Update Babel core, presets and module loader for webpack (#2585) (@catloversg)
|
|
||||||
- Add script to generate webpack bundle report (#2587) (@catloversg)
|
|
||||||
- Update Electron (#2591) (@catloversg)
|
|
||||||
- Mitigate issue of forcefullyCrashRenderer (#2597) (@catloversg)
|
|
||||||
- Split Settings.ts to reduce number of imports (#2600) (@catloversg)
|
|
||||||
- Remove duplicate random alphanumeric string functions (#2601) (@catloversg)
|
|
||||||
- Update comments to reflect changes in #2603 (#2606) (@catloversg)
|
|
||||||
- Replace ipExists() linear scan with O(1) Map.has() (#2621) (@lstutzman)
|
|
||||||
- Refactor and fix issues in db.ts (#2623) (@catloversg)
|
|
||||||
- Add dependency array to TerminalInput keydown useEffect (#2620) (@lstutzman)
|
|
||||||
- Add dependency array to GameRoot useEffect (#2617) (@lstutzman)
|
|
||||||
- Dev menu: Initialize dark net data when setting SF15 level (#2632) (@catloversg)
|
|
||||||
- Use type-only imports in ArrayHelpers.ts (#2630) (@catloversg)
|
|
||||||
- Remove redundant "$" from JS/TS regex in webpack config (#2649) (@catloversg)
|
|
||||||
- Allow specifying commit hash id when building artifacts (#2652) (@catloversg)
|
|
||||||
- Fix passive event listener warning (#2671) (@catloversg)
|
|
||||||
- Ignore .DS_Store files when generating pages.ts (#2685) (@catloversg)
|
|
||||||
- Consistently check when to show intelligence skill (#2692) (@catloversg)
|
|
||||||
- Fix api-extractor warnings (#2701) (@catloversg)
|
|
||||||
- Add comments explaining redundant check in product calculation (#2705) (@catloversg)
|
|
||||||
`,
|
`,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { DarknetServer } from "../../Server/DarknetServer";
|
|||||||
import { resolveCacheFilePath } from "../../Paths/CacheFilePath";
|
import { resolveCacheFilePath } from "../../Paths/CacheFilePath";
|
||||||
import type { CacheResult } from "@nsdefs";
|
import type { CacheResult } from "@nsdefs";
|
||||||
import { addClue, cctCooldownReached } from "./effects";
|
import { addClue, cctCooldownReached } from "./effects";
|
||||||
|
import { getBitNodeMultipliers } from "../../BitNode/BitNode";
|
||||||
|
|
||||||
export const generateCacheFilename = (isPhishingCache: boolean, prefix?: string) => {
|
export const generateCacheFilename = (isPhishingCache: boolean, prefix?: string) => {
|
||||||
const filenamePrefix = prefix ?? cachePrefixes[Math.floor(Math.random() * cachePrefixes.length)];
|
const filenamePrefix = prefix ?? cachePrefixes[Math.floor(Math.random() * cachePrefixes.length)];
|
||||||
@@ -41,11 +42,15 @@ export const getRewardFromCache = (server: DarknetServer, cacheName: string, sup
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const rewards = [getMoneyReward, getProgramAndStockMarketRelatedRewards, getStockReward, getDataFileReward];
|
const rewards = [getProgramAndStockMarketRelatedRewards, getStockReward, getDataFileReward];
|
||||||
if (cacheName.endsWith(".d.cache")) {
|
if (cacheName.endsWith(".d.cache")) {
|
||||||
// only include ccts from caches generated from phishing attacks
|
// only include ccts from caches generated from phishing attacks
|
||||||
rewards.push(getCCTReward);
|
rewards.push(getCCTReward);
|
||||||
}
|
}
|
||||||
|
if (getBitNodeMultipliers(Player.bitNodeN, 1).DarknetMoneyMultiplier) {
|
||||||
|
// only include money reward if it is not disabled by the bn mults
|
||||||
|
rewards.push(getMoneyReward);
|
||||||
|
}
|
||||||
const reward = rewards[Math.floor(Math.random() * rewards.length)];
|
const reward = rewards[Math.floor(Math.random() * rewards.length)];
|
||||||
const result = reward(difficulty, server);
|
const result = reward(difficulty, server);
|
||||||
|
|
||||||
@@ -98,10 +103,14 @@ export const getStockReward = (difficulty: number): string => {
|
|||||||
initStockMarket();
|
initStockMarket();
|
||||||
}
|
}
|
||||||
const stockSymbols = Object.keys(StockSymbol);
|
const stockSymbols = Object.keys(StockSymbol);
|
||||||
const randomStock = stockSymbols[Math.floor(Math.random() * stockSymbols.length)];
|
const stock = StockMarket[stockSymbols[Math.floor(Math.random() * stockSymbols.length)]];
|
||||||
const shares = Math.floor(1 + difficulty * 5 + Math.random() * 10);
|
const maxNewShares = stock.maxShares - stock.playerShares - stock.playerShortShares;
|
||||||
StockMarket[randomStock].playerShares += shares;
|
if (maxNewShares <= 0) {
|
||||||
return `You have discovered a stock option cache containing ${shares} shares of ${randomStock}!`;
|
return getMoneyReward(difficulty);
|
||||||
|
}
|
||||||
|
const shares = Math.min(Math.floor(1 + difficulty * 5 + Math.random() * 10), maxNewShares);
|
||||||
|
stock.playerShares += shares;
|
||||||
|
return `You have discovered a stock option cache containing ${shares} shares of ${stock.symbol}!`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDataFileReward = (difficulty: number, server: DarknetServer): string => {
|
export const getDataFileReward = (difficulty: number, server: DarknetServer): string => {
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ export const handleLabyrinthPassword = (
|
|||||||
return {
|
return {
|
||||||
passwordAttempted: attemptedPassword,
|
passwordAttempted: attemptedPassword,
|
||||||
code: ResponseCodeEnum.Success,
|
code: ResponseCodeEnum.Success,
|
||||||
message: "You have discovered the end the labyrinth.",
|
message: "You have discovered the end of the labyrinth.",
|
||||||
data: labServer.password,
|
data: labServer.password,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,7 +377,7 @@ Paste the following code into the [Script](../basic/scripts.md) editor:
|
|||||||
// amount of servers
|
// amount of servers
|
||||||
while (i < ns.cloud.getServerLimit()) {
|
while (i < ns.cloud.getServerLimit()) {
|
||||||
// Check if we have enough money to purchase access to a server
|
// Check if we have enough money to purchase access to a server
|
||||||
if (ns.getServerMoneyAvailable("home") > ns.cloud.getRamLimit(ram)) {
|
if (ns.getServerMoneyAvailable("home") > ns.cloud.getServerCost(ram)) {
|
||||||
// If we have enough money, then:
|
// If we have enough money, then:
|
||||||
// 1. Purchase the server
|
// 1. Purchase the server
|
||||||
// 2. Copy our hacking script onto the newly purchased cloud server
|
// 2. Copy our hacking script onto the newly purchased cloud server
|
||||||
|
|||||||
@@ -248,9 +248,10 @@ const authenticateWithNoPassword = async (ns, hostname) => {
|
|||||||
return result.success;
|
return result.success;
|
||||||
};
|
};
|
||||||
|
|
||||||
// This lets you tab-complete putting "--tail" on the run command so you can see the script logs as it runs, if you want
|
/** This lets you tab-complete putting "--tail" on the run command so you can see the script logs as it runs, if you want
|
||||||
// If you add support to the script to take other arguments, you can add them here as well for convenience
|
* If you add support to the script to take other arguments, you can add them here as well for convenience
|
||||||
export function autocomplete(data: AutocompleteData) {
|
* @param {AutocompleteData} data */
|
||||||
|
export function autocomplete(data) {
|
||||||
return ["--tail"];
|
return ["--tail"];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ Common infinite loop when translating the server purchasing script in starting g
|
|||||||
let i = ns.cloud.getServerNames().length;
|
let i = ns.cloud.getServerNames().length;
|
||||||
|
|
||||||
while (i < ns.cloud.getServerLimit()) {
|
while (i < ns.cloud.getServerLimit()) {
|
||||||
if (ns.getServerMoneyAvailable("home") > ns.cloud.getRamLimit(ram)) {
|
if (ns.getServerMoneyAvailable("home") > ns.cloud.getServerCost(ram)) {
|
||||||
const hostname = ns.cloud.purchaseServer("cloud-server-" + i, ram);
|
const hostname = ns.cloud.purchaseServer("cloud-server-" + i, ram);
|
||||||
ns.scp("early-hack-template.js", hostname);
|
ns.scp("early-hack-template.js", hostname);
|
||||||
ns.exec("early-hack-template.js", hostname, 3);
|
ns.exec("early-hack-template.js", hostname, 3);
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ All these tools support synchronizing scripts to Bitburner and transpiling TypeS
|
|||||||
|
|
||||||
Links:
|
Links:
|
||||||
|
|
||||||
- https://github.com/bitburner-official/typescript-template
|
- [typescript-template](https://github.com/bitburner-official/typescript-template): A template for synchronizing Typescript/Javascript from your computer to the game.
|
||||||
- https://github.com/Tanimodori/viteburner
|
- [viteburner](https://github.com/Tanimodori/viteburner): Daemon tools of bitburner using vite for script transform, file syncing, RAM monitoring and more!
|
||||||
- https://github.com/shyguy1412/bb-external-editor
|
- [bb-external-editor](https://github.com/shyguy1412/bb-external-editor): This tool uses esbuild to transpile and bundle your scripts. It supports JS, TS and React as well as importing from any browser-compatible npm library out of the box.
|
||||||
|
- [BitburnerGoFilesync](https://github.com/CTNOriginals/BitburnerGoFilesync): A standalone binary cli tool that doesn't require any setup or third party libraries. It is designed to be very minimal and easy to use out of the box.
|
||||||
|
|
||||||
`typescript-template` has a small set of options and features. Its simplicity is by design. `viteburner` and `bb-external-editor` have more fancy features.
|
`typescript-template` and `BitburnerGoFilesync` both have a small set of options and features, their simplicity is by design.
|
||||||
|
`viteburner` and `bb-external-editor` have more fancy features and may offer more control for specific use cases.
|
||||||
|
|
||||||
## Troubleshooting tips
|
## Troubleshooting tips
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ import { CONSTANTS } from "../Constants";
|
|||||||
import { BladeburnerConstants } from "../Bladeburner/data/Constants";
|
import { BladeburnerConstants } from "../Bladeburner/data/Constants";
|
||||||
import type { PlayerObject } from "../PersonObjects/Player/PlayerObject";
|
import type { PlayerObject } from "../PersonObjects/Player/PlayerObject";
|
||||||
import { CovenantCampaign } from "./ui/CovenantCampaign";
|
import { CovenantCampaign } from "./ui/CovenantCampaign";
|
||||||
|
import { GangCampaign } from "./ui/GangCampaign";
|
||||||
|
import { GangConstants } from "../Gang/data/Constants";
|
||||||
|
|
||||||
interface FactionInfoParams {
|
interface FactionInfoParams {
|
||||||
infoText?: JSX.Element;
|
infoText?: JSX.Element;
|
||||||
@@ -812,3 +814,7 @@ export const FactionInfos: Record<FactionName, FactionInfo> = {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (const factionName of GangConstants.Names) {
|
||||||
|
FactionInfos[factionName].campaign = () => <GangCampaign factionName={factionName} />;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import Typography from "@mui/material/Typography";
|
|||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import { KEY } from "../../utils/KeyboardEventKey";
|
import { KEY } from "../../utils/KeyboardEventKey";
|
||||||
import { FactionName } from "@enums";
|
import { FactionName } from "@enums";
|
||||||
|
import { canCreateGang } from "../../Gang/helpers";
|
||||||
|
import { dialogBoxCreate } from "../../ui/React/DialogBox";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -33,6 +35,11 @@ export function CreateGangModal(props: IProps): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createGang(): void {
|
function createGang(): void {
|
||||||
|
const checkResult = canCreateGang(props.facName);
|
||||||
|
if (!checkResult.success) {
|
||||||
|
dialogBoxCreate(checkResult.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Player.startGang(props.facName, isHacking());
|
Player.startGang(props.facName, isHacking());
|
||||||
props.onClose();
|
props.onClose();
|
||||||
Router.toPage(Page.Gang);
|
Router.toPage(Page.Gang);
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { Player } from "@player";
|
|||||||
import { Typography, Button } from "@mui/material";
|
import { Typography, Button } from "@mui/material";
|
||||||
|
|
||||||
import { FactionWorkType } from "@enums";
|
import { FactionWorkType } from "@enums";
|
||||||
import { GangButton } from "./GangButton";
|
|
||||||
import { FactionWork } from "../../Work/FactionWork";
|
import { FactionWork } from "../../Work/FactionWork";
|
||||||
import { useCycleRerender } from "../../ui/React/hooks";
|
import { useCycleRerender } from "../../ui/React/hooks";
|
||||||
import { favorNeededToDonate } from "../formulas/donation";
|
import { favorNeededToDonate } from "../formulas/donation";
|
||||||
@@ -111,7 +110,6 @@ function MainPage({ faction, rerender, onAugmentations }: IMainProps): React.Rea
|
|||||||
{faction.name}
|
{faction.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Info faction={faction} factionInfo={factionInfo} />
|
<Info faction={faction} factionInfo={factionInfo} />
|
||||||
<GangButton faction={faction} />
|
|
||||||
{!isPlayersGang && (
|
{!isPlayersGang && (
|
||||||
<>
|
<>
|
||||||
{factionInfo.offersWork() && (
|
{factionInfo.offersWork() && (
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import { Button, Typography, Box, Paper, Tooltip } from "@mui/material";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { GangConstants } from "../../Gang/data/Constants";
|
|
||||||
import { Router } from "../../ui/GameRoot";
|
|
||||||
import { Page } from "../../ui/Router";
|
|
||||||
import { Player } from "@player";
|
|
||||||
import { Faction } from "../Faction";
|
|
||||||
import { CreateGangModal } from "./CreateGangModal";
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
faction: Faction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GangButton({ faction }: IProps): React.ReactElement {
|
|
||||||
const [gangOpen, setGangOpen] = useState(false);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!GangConstants.Names.includes(faction.name) || // not even a gang
|
|
||||||
!Player.isAwareOfGang() || // doesn't know about gang
|
|
||||||
(Player.gang && Player.getGangName() !== faction.name) // already in another gang
|
|
||||||
) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = {
|
|
||||||
enabled: false,
|
|
||||||
title: "",
|
|
||||||
tooltip: "" as string | React.ReactElement,
|
|
||||||
description: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Player.gang) {
|
|
||||||
data = {
|
|
||||||
enabled: true,
|
|
||||||
title: "Manage Gang",
|
|
||||||
tooltip: "",
|
|
||||||
description: "Manage a gang for this Faction. Gangs will earn you money and faction reputation",
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const checkResult = Player.canAccessGang();
|
|
||||||
data = {
|
|
||||||
enabled: checkResult.success,
|
|
||||||
title: "Create Gang",
|
|
||||||
tooltip: !checkResult.success ? (
|
|
||||||
<Typography>Unlocked when reaching {GangConstants.GangKarmaRequirement} karma</Typography>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
),
|
|
||||||
description: "Create a gang for this Faction. Gangs will earn you money and faction reputation",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const manageGang = (): void => {
|
|
||||||
// If player already has a gang, just go to the gang UI
|
|
||||||
if (Player.inGang()) {
|
|
||||||
return Router.toPage(Page.Gang);
|
|
||||||
}
|
|
||||||
|
|
||||||
setGangOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box>
|
|
||||||
<Paper sx={{ my: 1, p: 1 }}>
|
|
||||||
<Tooltip title={data.tooltip}>
|
|
||||||
<span>
|
|
||||||
<Button onClick={manageGang} disabled={!data.enabled}>
|
|
||||||
{data.title}
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
<Typography>{data.description}</Typography>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<CreateGangModal facName={faction.name} open={gangOpen} onClose={() => setGangOpen(false)} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { Player } from "@player";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { knowAboutBitverse } from "../../BitNode/BitNodeUtils";
|
||||||
|
import { GangConstants } from "../../Gang/data/Constants";
|
||||||
|
import { Router } from "../../ui/GameRoot";
|
||||||
|
import { Modal } from "../../ui/React/Modal";
|
||||||
|
import { Page } from "../../ui/Router";
|
||||||
|
import { FactionName } from "../Enums";
|
||||||
|
import { CreateGangModal } from "./CreateGangModal";
|
||||||
|
import { Option } from "./Option";
|
||||||
|
|
||||||
|
function GangIncompleteCampaign() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Option
|
||||||
|
buttonText={"Execute the formation plan"}
|
||||||
|
infoText={
|
||||||
|
"The tension between our faction and its rivals has been rising. The leader plans to form a specialized " +
|
||||||
|
"group under your command to strengthen our position by improving our standing and expanding our resources."
|
||||||
|
}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
></Option>
|
||||||
|
<Modal open={open} onClose={() => setOpen(false)}>
|
||||||
|
<Typography component="div">
|
||||||
|
Each time you attempt to execute the plan, it is abruptly interrupted for reasons no one can explain. You
|
||||||
|
receive the same distorted message every time:
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
#@)($*&@__Y0U__^%$#@&*()__HAV3__(&@#*$%(@
|
||||||
|
<br />
|
||||||
|
()@#*$%(__N0T__@&$#)@*(__S33N__)(*@#&$)(
|
||||||
|
<br />
|
||||||
|
@&*($#@&__TH3__#@A&#@*)(@$#@)*
|
||||||
|
<br />
|
||||||
|
%$#@&()@__TRU1H__()*@#$&()@#$
|
||||||
|
</Typography>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GangCampaign({ factionName }: { factionName: FactionName }) {
|
||||||
|
const [gangOpen, setGangOpen] = useState(false);
|
||||||
|
|
||||||
|
if (!GangConstants.Names.includes(factionName)) {
|
||||||
|
throw new Error(`Cannot create gang with ${factionName}`);
|
||||||
|
}
|
||||||
|
if (!knowAboutBitverse()) {
|
||||||
|
return <GangIncompleteCampaign />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
enabled: false,
|
||||||
|
title: "",
|
||||||
|
tooltip: "" as string | React.ReactElement,
|
||||||
|
description: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Player.gang) {
|
||||||
|
if (Player.getGangName() !== factionName) {
|
||||||
|
data.enabled = false;
|
||||||
|
data.title = "Create Gang";
|
||||||
|
data.tooltip = "You already created a gang with another faction";
|
||||||
|
} else {
|
||||||
|
data.enabled = true;
|
||||||
|
data.title = "Manage Gang";
|
||||||
|
data.description = "Manage a gang for this Faction. Gangs will earn you money and faction reputation";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const checkResult = Player.canAccessGang();
|
||||||
|
data.enabled = checkResult.success;
|
||||||
|
data.title = "Create Gang";
|
||||||
|
data.tooltip = !checkResult.success ? checkResult.message : "";
|
||||||
|
data.description = "Create a gang for this Faction. Gangs will earn you money and faction reputation";
|
||||||
|
}
|
||||||
|
|
||||||
|
const manageGang = (): void => {
|
||||||
|
// If player already has a gang, just go to the gang UI
|
||||||
|
if (Player.inGang()) {
|
||||||
|
return Router.toPage(Page.Gang);
|
||||||
|
}
|
||||||
|
|
||||||
|
setGangOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box>
|
||||||
|
<Paper sx={{ my: 1, p: 1 }}>
|
||||||
|
<Tooltip title={data.tooltip}>
|
||||||
|
<span>
|
||||||
|
<Button onClick={manageGang} disabled={!data.enabled}>
|
||||||
|
{data.title}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
<Typography>{data.description}</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<CreateGangModal facName={factionName} open={gangOpen} onClose={() => setGangOpen(false)} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ export const GangConstants = {
|
|||||||
FactionName.SpeakersForTheDead,
|
FactionName.SpeakersForTheDead,
|
||||||
FactionName.NiteSec,
|
FactionName.NiteSec,
|
||||||
FactionName.TheBlackHand,
|
FactionName.TheBlackHand,
|
||||||
] as string[],
|
] as FactionName[],
|
||||||
GangKarmaRequirement: -54000,
|
GangKarmaRequirement: -54000,
|
||||||
/** Normal number of game cycles processed at once (2 seconds) */
|
/** Normal number of game cycles processed at once (2 seconds) */
|
||||||
minCyclesToProcess: 2000 / CONSTANTS.MilliPerCycle,
|
minCyclesToProcess: 2000 / CONSTANTS.MilliPerCycle,
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Result } from "@nsdefs";
|
||||||
|
import { Player } from "@player";
|
||||||
|
import { FactionName } from "../Enums";
|
||||||
|
import { GangConstants } from "./data/Constants";
|
||||||
|
|
||||||
|
export function canCreateGang(faction: FactionName): Result {
|
||||||
|
if (Player.gang) {
|
||||||
|
return { success: false, message: "You already have a gang." };
|
||||||
|
}
|
||||||
|
const checkResult = Player.canAccessGang();
|
||||||
|
if (!checkResult.success) {
|
||||||
|
return { success: false, message: checkResult.message };
|
||||||
|
}
|
||||||
|
if (!GangConstants.Names.includes(faction)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `${faction} does not allow creating a gang. You can only do that with ${GangConstants.Names.join(
|
||||||
|
", ",
|
||||||
|
)}.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!Player.factions.includes(faction)) {
|
||||||
|
return { success: false, message: `You are not a member of ${faction}.` };
|
||||||
|
}
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
@@ -7,13 +7,13 @@ import { type InternalAPI, type NetscriptContext, setRemovedFunctions } from "..
|
|||||||
import { GangPromise, RecruitmentResult } from "../Gang/Gang";
|
import { GangPromise, RecruitmentResult } from "../Gang/Gang";
|
||||||
import { Player } from "@player";
|
import { Player } from "@player";
|
||||||
import { FactionName } from "@enums";
|
import { FactionName } from "@enums";
|
||||||
import { GangConstants } from "../Gang/data/Constants";
|
|
||||||
import { AllGangs } from "../Gang/AllGangs";
|
import { AllGangs } from "../Gang/AllGangs";
|
||||||
import { GangMemberTasks } from "../Gang/GangMemberTasks";
|
import { GangMemberTasks } from "../Gang/GangMemberTasks";
|
||||||
import { GangMemberUpgrades } from "../Gang/GangMemberUpgrades";
|
import { GangMemberUpgrades } from "../Gang/GangMemberUpgrades";
|
||||||
import { helpers } from "../Netscript/NetscriptHelpers";
|
import { helpers } from "../Netscript/NetscriptHelpers";
|
||||||
import { getEnumHelper } from "../utils/EnumHelper";
|
import { getEnumHelper } from "../utils/EnumHelper";
|
||||||
import { CONSTANTS } from "../Constants";
|
import { CONSTANTS } from "../Constants";
|
||||||
|
import { canCreateGang } from "../Gang/helpers";
|
||||||
|
|
||||||
export function NetscriptGang(): InternalAPI<IGang> {
|
export function NetscriptGang(): InternalAPI<IGang> {
|
||||||
/** Functions as an API check and also returns the gang object */
|
/** Functions as an API check and also returns the gang object */
|
||||||
@@ -40,26 +40,11 @@ export function NetscriptGang(): InternalAPI<IGang> {
|
|||||||
const gangFunctions: InternalAPI<IGang> = {
|
const gangFunctions: InternalAPI<IGang> = {
|
||||||
createGang: (ctx) => (_faction) => {
|
createGang: (ctx) => (_faction) => {
|
||||||
const faction = getEnumHelper("FactionName").nsGetMember(ctx, _faction);
|
const faction = getEnumHelper("FactionName").nsGetMember(ctx, _faction);
|
||||||
if (Player.gang) {
|
const checkResult = canCreateGang(faction);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const checkResult = Player.canAccessGang();
|
|
||||||
if (!checkResult.success) {
|
if (!checkResult.success) {
|
||||||
helpers.log(ctx, () => checkResult.message);
|
helpers.log(ctx, () => checkResult.message);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!GangConstants.Names.includes(faction)) {
|
|
||||||
helpers.log(
|
|
||||||
ctx,
|
|
||||||
() =>
|
|
||||||
`${faction} does not allow creating a gang. You can only do that with ${GangConstants.Names.join(", ")}.`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!Player.factions.includes(faction)) {
|
|
||||||
helpers.log(ctx, () => `You are not a member of ${faction}.`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isHacking = faction === FactionName.NiteSec || faction === FactionName.TheBlackHand;
|
const isHacking = faction === FactionName.NiteSec || faction === FactionName.TheBlackHand;
|
||||||
Player.startGang(faction, isHacking);
|
Player.startGang(faction, isHacking);
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ export function NetscriptGrafting(): InternalAPI<IGrafting> {
|
|||||||
Player.startFocusing();
|
Player.startFocusing();
|
||||||
Router.toPage(Page.Work);
|
Router.toPage(Page.Work);
|
||||||
} else if (wasFocusing) {
|
} else if (wasFocusing) {
|
||||||
Player.stopFocusing();
|
|
||||||
Router.toPage(Page.Terminal);
|
Router.toPage(Page.Terminal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -283,7 +283,6 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
Player.startFocusing();
|
Player.startFocusing();
|
||||||
Router.toPage(Page.Work);
|
Router.toPage(Page.Work);
|
||||||
} else if (wasFocusing) {
|
} else if (wasFocusing) {
|
||||||
Player.stopFocusing();
|
|
||||||
Router.toPage(Page.Terminal);
|
Router.toPage(Page.Terminal);
|
||||||
}
|
}
|
||||||
helpers.log(ctx, () => `Started ${classType} at ${universityName}`);
|
helpers.log(ctx, () => `Started ${classType} at ${universityName}`);
|
||||||
@@ -365,7 +364,6 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
Player.startFocusing();
|
Player.startFocusing();
|
||||||
Router.toPage(Page.Work);
|
Router.toPage(Page.Work);
|
||||||
} else if (wasFocusing) {
|
} else if (wasFocusing) {
|
||||||
Player.stopFocusing();
|
|
||||||
Router.toPage(Page.Terminal);
|
Router.toPage(Page.Terminal);
|
||||||
}
|
}
|
||||||
helpers.log(ctx, () => `Started training ${classType} at ${gymName}`);
|
helpers.log(ctx, () => `Started training ${classType} at ${gymName}`);
|
||||||
@@ -570,7 +568,6 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
Router.toPage(Page.Work);
|
Router.toPage(Page.Work);
|
||||||
return true;
|
return true;
|
||||||
} else if (Player.focus && !focus) {
|
} else if (Player.focus && !focus) {
|
||||||
Player.stopFocusing();
|
|
||||||
Router.toPage(Page.Terminal);
|
Router.toPage(Page.Terminal);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -709,7 +706,6 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
Player.startFocusing();
|
Player.startFocusing();
|
||||||
Router.toPage(Page.Work);
|
Router.toPage(Page.Work);
|
||||||
} else if (wasFocused) {
|
} else if (wasFocused) {
|
||||||
Player.stopFocusing();
|
|
||||||
Router.toPage(Page.Terminal);
|
Router.toPage(Page.Terminal);
|
||||||
}
|
}
|
||||||
helpers.log(ctx, () => `Began working at '${companyName}' with position '${jobName}'`);
|
helpers.log(ctx, () => `Began working at '${companyName}' with position '${jobName}'`);
|
||||||
@@ -834,7 +830,6 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
Player.startFocusing();
|
Player.startFocusing();
|
||||||
Router.toPage(Page.Work);
|
Router.toPage(Page.Work);
|
||||||
} else if (wasFocusing) {
|
} else if (wasFocusing) {
|
||||||
Player.stopFocusing();
|
|
||||||
Router.toPage(Page.Terminal);
|
Router.toPage(Page.Terminal);
|
||||||
}
|
}
|
||||||
helpers.log(ctx, () => `Started carrying out hacking contracts for '${faction.name}'`);
|
helpers.log(ctx, () => `Started carrying out hacking contracts for '${faction.name}'`);
|
||||||
@@ -855,7 +850,6 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
Player.startFocusing();
|
Player.startFocusing();
|
||||||
Router.toPage(Page.Work);
|
Router.toPage(Page.Work);
|
||||||
} else if (wasFocusing) {
|
} else if (wasFocusing) {
|
||||||
Player.stopFocusing();
|
|
||||||
Router.toPage(Page.Terminal);
|
Router.toPage(Page.Terminal);
|
||||||
}
|
}
|
||||||
helpers.log(ctx, () => `Started carrying out field missions for '${faction.name}'`);
|
helpers.log(ctx, () => `Started carrying out field missions for '${faction.name}'`);
|
||||||
@@ -876,7 +870,6 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
Player.startFocusing();
|
Player.startFocusing();
|
||||||
Router.toPage(Page.Work);
|
Router.toPage(Page.Work);
|
||||||
} else if (wasFocusing) {
|
} else if (wasFocusing) {
|
||||||
Player.stopFocusing();
|
|
||||||
Router.toPage(Page.Terminal);
|
Router.toPage(Page.Terminal);
|
||||||
}
|
}
|
||||||
helpers.log(ctx, () => `Started carrying out security work for '${faction.name}'`);
|
helpers.log(ctx, () => `Started carrying out security work for '${faction.name}'`);
|
||||||
@@ -1009,7 +1002,6 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
Player.startFocusing();
|
Player.startFocusing();
|
||||||
Router.toPage(Page.Work);
|
Router.toPage(Page.Work);
|
||||||
} else if (wasFocusing) {
|
} else if (wasFocusing) {
|
||||||
Player.stopFocusing();
|
|
||||||
Router.toPage(Page.Terminal);
|
Router.toPage(Page.Terminal);
|
||||||
}
|
}
|
||||||
helpers.log(ctx, () => `Began creating program: '${programName}'`);
|
helpers.log(ctx, () => `Began creating program: '${programName}'`);
|
||||||
@@ -1059,7 +1051,6 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
Player.startFocusing();
|
Player.startFocusing();
|
||||||
Router.toPage(Page.Work);
|
Router.toPage(Page.Work);
|
||||||
} else if (wasFocusing) {
|
} else if (wasFocusing) {
|
||||||
Player.stopFocusing();
|
|
||||||
Router.toPage(Page.Terminal);
|
Router.toPage(Page.Terminal);
|
||||||
}
|
}
|
||||||
return crimeTime;
|
return crimeTime;
|
||||||
|
|||||||
@@ -26,21 +26,21 @@ import { getEnumHelper } from "../utils/EnumHelper";
|
|||||||
import { CONSTANTS } from "../Constants";
|
import { CONSTANTS } from "../Constants";
|
||||||
import { getDarknetVolatilityMult } from "../DarkNet/effects/effects";
|
import { getDarknetVolatilityMult } from "../DarkNet/effects/effects";
|
||||||
|
|
||||||
export function NetscriptStockMarket(): InternalAPI<StockAPI> {
|
export const getStockFromSymbol = function (ctx: NetscriptContext, symbol: string): Stock {
|
||||||
/** Checks if the player has TIX API access. Throws an error if the player does not */
|
|
||||||
const checkTixApiAccess = function (ctx: NetscriptContext): void {
|
|
||||||
if (!Player.hasTixApiAccess) {
|
|
||||||
throw helpers.errorMessage(ctx, `You don't have TIX API Access! Cannot use ${ctx.function}()`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStockFromSymbol = function (ctx: NetscriptContext, symbol: string): Stock {
|
|
||||||
const stock = SymbolToStockMap[symbol];
|
const stock = SymbolToStockMap[symbol];
|
||||||
if (stock == null) {
|
if (stock == null) {
|
||||||
throw helpers.errorMessage(ctx, `Invalid stock symbol: '${symbol}'`);
|
throw helpers.errorMessage(ctx, `Invalid stock symbol: '${symbol}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return stock;
|
return stock;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NetscriptStockMarket(): InternalAPI<StockAPI> {
|
||||||
|
/** Checks if the player has TIX API access. Throws an error if the player does not */
|
||||||
|
const checkTixApiAccess = function (ctx: NetscriptContext): void {
|
||||||
|
if (!Player.hasTixApiAccess) {
|
||||||
|
throw helpers.errorMessage(ctx, `You don't have TIX API Access! Cannot use ${ctx.function}()`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stockFunctions: InternalAPI<StockAPI> = {
|
const stockFunctions: InternalAPI<StockAPI> = {
|
||||||
@@ -353,12 +353,3 @@ export function NetscriptStockMarket(): InternalAPI<StockAPI> {
|
|||||||
|
|
||||||
return stockFunctions;
|
return stockFunctions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStockFromSymbol = function (ctx: NetscriptContext, symbol: string): Stock {
|
|
||||||
const stock = SymbolToStockMap[symbol];
|
|
||||||
if (stock == null) {
|
|
||||||
throw helpers.errorMessage(ctx, `Invalid stock symbol: '${symbol}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return stock;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const root = "" as Directory;
|
|||||||
* #: Invalid because it might have a use in the terminal in the future.
|
* #: Invalid because it might have a use in the terminal in the future.
|
||||||
* (quote marks): Invalid to avoid conflict with quote marks used in the terminal.
|
* (quote marks): Invalid to avoid conflict with quote marks used in the terminal.
|
||||||
* (whitespace): Invalid to avoid confusion with terminal command separator */
|
* (whitespace): Invalid to avoid confusion with terminal command separator */
|
||||||
const invalidCharacters = ["/", "*", "?", "[", "]", "!", "\\", "~", "|", "#", '"', "'"];
|
export const invalidCharacters = ["/", "*", "?", "[", "]", "!", "\\", "~", "|", "#", '"', "'"] as const;
|
||||||
|
|
||||||
/** A valid character is any character that is not one of the invalid characters */
|
/** A valid character is any character that is not one of the invalid characters */
|
||||||
export const oneValidCharacter = `[^${escapeRegExp(invalidCharacters.join(""))}\\s]`;
|
export const oneValidCharacter = `[^${escapeRegExp(invalidCharacters.join(""))}\\s]`;
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import { FilePath, resolveFilePath } from "./FilePath";
|
|||||||
type WithTextExtension = string & { __fileType: "Text" };
|
type WithTextExtension = string & { __fileType: "Text" };
|
||||||
export type TextFilePath = FilePath & WithTextExtension;
|
export type TextFilePath = FilePath & WithTextExtension;
|
||||||
|
|
||||||
|
export const validTextExtensions = [".txt", ".json", ".css"];
|
||||||
|
|
||||||
/** Check extension only */
|
/** Check extension only */
|
||||||
export function hasTextExtension(path: string): path is WithTextExtension {
|
export function hasTextExtension(path: string): path is WithTextExtension {
|
||||||
return path.endsWith(".txt") || path.endsWith(".json") || path.endsWith(".css");
|
return validTextExtensions.some((extension) => path.endsWith(extension));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sanitize a player input, resolve any relative paths, and for imports add the correct extension if missing */
|
/** Sanitize a player input, resolve any relative paths, and for imports add the correct extension if missing */
|
||||||
|
|||||||
@@ -54,8 +54,9 @@ export function hasGangWith(this: PlayerObject, facName: FactionName): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function startGang(this: PlayerObject, factionName: FactionName, hacking: boolean): void {
|
export function startGang(this: PlayerObject, factionName: FactionName, hacking: boolean): void {
|
||||||
// isFactionWork handles null internally, finishWork might need to be run with true
|
if (isFactionWork(this.currentWork) && this.currentWork.factionName === factionName) {
|
||||||
if (isFactionWork(this.currentWork) && this.currentWork.factionName === factionName) this.finishWork(false);
|
this.finishWork(true);
|
||||||
|
}
|
||||||
|
|
||||||
this.gang = new Gang(factionName, hacking);
|
this.gang = new Gang(factionName, hacking);
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const TerminalHelpText: string[] = [
|
|||||||
" connect [hostname] Connects to a remote server",
|
" connect [hostname] Connects to a remote server",
|
||||||
" cp [src] [dest] Copy a file",
|
" cp [src] [dest] Copy a file",
|
||||||
" download [script/text file] Downloads scripts or text files to your computer",
|
" download [script/text file] Downloads scripts or text files to your computer",
|
||||||
|
" upload [dir] Upload scripts or text files from your computer",
|
||||||
" expr [math expression] Evaluate a mathematical expression",
|
" expr [math expression] Evaluate a mathematical expression",
|
||||||
" free Check the machine's memory (RAM) usage",
|
" free Check the machine's memory (RAM) usage",
|
||||||
" grep [opts]... pattern [file]... Search for PATTERN (string/regular expression) in each FILE and print results to terminal",
|
" grep [opts]... pattern [file]... Search for PATTERN (string/regular expression) in each FILE and print results to terminal",
|
||||||
@@ -216,6 +217,18 @@ export const HelpTexts: Record<string, string[]> = {
|
|||||||
"Download all text files: download *.txt",
|
"Download all text files: download *.txt",
|
||||||
" ",
|
" ",
|
||||||
],
|
],
|
||||||
|
upload: [
|
||||||
|
"Usage: upload [dir]",
|
||||||
|
" ",
|
||||||
|
"Uploads a directory from your computer into the game.",
|
||||||
|
" ",
|
||||||
|
"Examples:",
|
||||||
|
" ",
|
||||||
|
" upload path/to/dir",
|
||||||
|
" ",
|
||||||
|
" upload .",
|
||||||
|
" ",
|
||||||
|
],
|
||||||
expr: [
|
expr: [
|
||||||
"Usage: expr [mathematical expression]",
|
"Usage: expr [mathematical expression]",
|
||||||
" ",
|
" ",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import { check } from "./commands/check";
|
|||||||
import { connect } from "./commands/connect";
|
import { connect } from "./commands/connect";
|
||||||
import { cp } from "./commands/cp";
|
import { cp } from "./commands/cp";
|
||||||
import { download } from "./commands/download";
|
import { download } from "./commands/download";
|
||||||
|
import { upload } from "./commands/upload";
|
||||||
import { expr } from "./commands/expr";
|
import { expr } from "./commands/expr";
|
||||||
import { free } from "./commands/free";
|
import { free } from "./commands/free";
|
||||||
import { grep } from "./commands/grep";
|
import { grep } from "./commands/grep";
|
||||||
@@ -105,6 +106,7 @@ export const TerminalCommands: Record<string, (args: (string | number | boolean)
|
|||||||
connect: connect,
|
connect: connect,
|
||||||
cp: cp,
|
cp: cp,
|
||||||
download: download,
|
download: download,
|
||||||
|
upload: upload,
|
||||||
expr: expr,
|
expr: expr,
|
||||||
free: free,
|
free: free,
|
||||||
grep: grep,
|
grep: grep,
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { Terminal } from "../../Terminal";
|
||||||
|
import type { BaseServer } from "../../Server/BaseServer";
|
||||||
|
import { combinePath, isFilePath } from "../../Paths/FilePath";
|
||||||
|
import { hasTextExtension, validTextExtensions } from "../../Paths/TextFilePath";
|
||||||
|
import { hasScriptExtension, validScriptExtensions } from "../../Paths/ScriptFilePath";
|
||||||
|
import { PromptEvent } from "../../ui/React/PromptManager";
|
||||||
|
import type { ContentFilePath } from "../../Paths/ContentFile";
|
||||||
|
import { invalidCharacters } from "../../Paths/Directory";
|
||||||
|
import { pluralize } from "../../utils/I18nUtils";
|
||||||
|
|
||||||
|
function pickDirectory(): Promise<FileList | null> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "file";
|
||||||
|
input.webkitdirectory = true;
|
||||||
|
input.onchange = () => {
|
||||||
|
resolve(input.files);
|
||||||
|
};
|
||||||
|
input.oncancel = () => {
|
||||||
|
resolve(null);
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function askConfirm(txt: string): Promise<boolean> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
PromptEvent.emit({
|
||||||
|
txt,
|
||||||
|
resolve: (value: string | boolean) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
reject(new Error("PromptEvent got a string, expected boolean"));
|
||||||
|
} else {
|
||||||
|
resolve(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadAsync(args: (string | number | boolean)[], server: BaseServer) {
|
||||||
|
if (args.length !== 1) {
|
||||||
|
return Terminal.error("Incorrect usage of upload command. Usage: upload [dir]");
|
||||||
|
}
|
||||||
|
const destinationInput = String(args[0]);
|
||||||
|
const destination = Terminal.getDirectory(destinationInput);
|
||||||
|
if (destination === null) {
|
||||||
|
return Terminal.error(`Could not resolve ${destinationInput} as a Directory`);
|
||||||
|
}
|
||||||
|
const files = await pickDirectory();
|
||||||
|
if (files === null || files.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const withPath: (
|
||||||
|
| { badPath: string }
|
||||||
|
| { overwrite: ContentFilePath; file: File }
|
||||||
|
| { create: ContentFilePath; file: File }
|
||||||
|
)[] = [...files].map((f) => {
|
||||||
|
const { webkitRelativePath } = f;
|
||||||
|
/*
|
||||||
|
If the player has a directory /home/alice/foo/bar on her computer
|
||||||
|
and wants to upload the contents of the directory
|
||||||
|
and if the directory hierarchy looks like this:
|
||||||
|
/home/alice/foo/bar
|
||||||
|
├── hello
|
||||||
|
│ └── world.js
|
||||||
|
└── more
|
||||||
|
└── files.txt
|
||||||
|
`webkitRelativePath` for world.js will be "bar/hello/world.js"
|
||||||
|
`path` will be "hello/world.js"
|
||||||
|
`webkitRelativePath` for files.txt will "bar/more/files.txt"
|
||||||
|
`path` will be "more/files.txt"
|
||||||
|
*/
|
||||||
|
const path = webkitRelativePath.substring(1 + webkitRelativePath.indexOf("/"));
|
||||||
|
if (!isFilePath(path)) {
|
||||||
|
return { badPath: path };
|
||||||
|
}
|
||||||
|
const destFilePath = combinePath(destination, path);
|
||||||
|
let fileExists: boolean;
|
||||||
|
if (hasTextExtension(destFilePath)) {
|
||||||
|
fileExists = server.textFiles.has(destFilePath);
|
||||||
|
} else if (hasScriptExtension(destFilePath)) {
|
||||||
|
fileExists = server.scripts.has(destFilePath);
|
||||||
|
} else {
|
||||||
|
return { badPath: path };
|
||||||
|
}
|
||||||
|
if (fileExists) {
|
||||||
|
return {
|
||||||
|
overwrite: destFilePath,
|
||||||
|
file: f,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
create: destFilePath,
|
||||||
|
file: f,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const overwrite = withPath.filter((item) => "overwrite" in item);
|
||||||
|
const skipped = withPath.filter((item) => "badPath" in item);
|
||||||
|
const create = withPath.filter((item) => "create" in item);
|
||||||
|
const destForPrint = destination === "" ? "/" : destination;
|
||||||
|
const lines = [`Upload files to ${destForPrint}?`];
|
||||||
|
if (overwrite.length !== 0) {
|
||||||
|
lines.push(
|
||||||
|
"",
|
||||||
|
`${pluralize(overwrite.length, "file")} will be overwritten:`,
|
||||||
|
...overwrite.map(({ overwrite }) => overwrite),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (skipped.length !== 0) {
|
||||||
|
const extensions = [...validScriptExtensions, ...validTextExtensions];
|
||||||
|
lines.push(
|
||||||
|
"",
|
||||||
|
`Characters ${invalidCharacters
|
||||||
|
.filter((v) => v !== "/")
|
||||||
|
.join(" ")} and whitespace are not allowed in file paths.`,
|
||||||
|
`Only file extensions ${extensions.join(", ")} are allowed.`,
|
||||||
|
"A file name must have at least one character before the extension.",
|
||||||
|
"",
|
||||||
|
`${pluralize(skipped.length, "file")} will be skipped due to prohibited file paths:`,
|
||||||
|
...skipped.map(({ badPath }) => badPath),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (create.length !== 0) {
|
||||||
|
lines.push("", `${pluralize(create.length, "new file")} will be created:`, ...create.map(({ create }) => create));
|
||||||
|
}
|
||||||
|
if (!(await askConfirm(lines.join("\n")))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Terminal.print(`Starting to upload files to ${destForPrint}`);
|
||||||
|
for (const item of [...overwrite, ...create]) {
|
||||||
|
const destFilePath = "create" in item ? item.create : item.overwrite;
|
||||||
|
let text: string | undefined = undefined;
|
||||||
|
try {
|
||||||
|
text = await item.file.text();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
Terminal.error(`Failed to upload ${destFilePath}. Error: ${error}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
server.writeToContentFile(destFilePath, text);
|
||||||
|
}
|
||||||
|
Terminal.print(`Successfully uploaded files to ${destForPrint}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function upload(args: (string | number | boolean)[], server: BaseServer): void {
|
||||||
|
uploadAsync(args, server).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
Terminal.error(`Error while uploading files. Error: ${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -185,6 +185,7 @@ export async function getTabCompletionPossibilities(terminalText: string, baseDi
|
|||||||
|
|
||||||
case "cd":
|
case "cd":
|
||||||
case "ls":
|
case "ls":
|
||||||
|
case "upload":
|
||||||
if (onFirstCommandArg && !relativeDir) addDirectories();
|
if (onFirstCommandArg && !relativeDir) addDirectories();
|
||||||
return possibilities;
|
return possibilities;
|
||||||
|
|
||||||
|
|||||||
@@ -266,6 +266,13 @@ export function GameRoot(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// If the current page is Page.Work, the player is focusing on their current work. Switching to another page ends
|
||||||
|
// that focus, so we must call Player.stopFocusing() immediately after Router.toPage() to keep Player.focus in
|
||||||
|
// sync. Instead of repeating this logic wherever Router.toPage() is called, we should centralize the check and
|
||||||
|
// the Player.stopFocusing() call here.
|
||||||
|
if (pageWithContext.page === Page.Work && page !== Page.Work && Player.currentWork && Player.focus) {
|
||||||
|
Player.stopFocusing();
|
||||||
|
}
|
||||||
setNextPage({ page, ...context } as PageWithContext);
|
setNextPage({ page, ...context } as PageWithContext);
|
||||||
},
|
},
|
||||||
back: () => {
|
back: () => {
|
||||||
|
|||||||
@@ -563,8 +563,8 @@ export async function main(ns) {
|
|||||||
materials for all NS APIs.
|
materials for all NS APIs.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
The <DocumentationLink page="help/beginner_faq.md">FAQ</DocumentationLink> contains questions often asked
|
The <DocumentationLink page="help/faq.md">FAQ</DocumentationLink> contains questions often asked by
|
||||||
by beginners of the game.
|
beginners of the game.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<Typography fontWeight="fontWeightBold">
|
<Typography fontWeight="fontWeightBold">
|
||||||
|
|||||||
@@ -219,7 +219,6 @@ export function WorkInProgressRoot(): React.ReactElement {
|
|||||||
},
|
},
|
||||||
unfocus: () => {
|
unfocus: () => {
|
||||||
Router.toPage(Page.City);
|
Router.toPage(Page.City);
|
||||||
Player.stopFocusing();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
title: `You are attempting ${crime.workName}`,
|
title: `You are attempting ${crime.workName}`,
|
||||||
@@ -266,7 +265,6 @@ export function WorkInProgressRoot(): React.ReactElement {
|
|||||||
},
|
},
|
||||||
unfocus: () => {
|
unfocus: () => {
|
||||||
Router.toPage(Page.Location, { location: Locations[classWork.location] });
|
Router.toPage(Page.Location, { location: Locations[classWork.location] });
|
||||||
Player.stopFocusing();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
title: (
|
title: (
|
||||||
@@ -303,7 +301,6 @@ export function WorkInProgressRoot(): React.ReactElement {
|
|||||||
},
|
},
|
||||||
unfocus: () => {
|
unfocus: () => {
|
||||||
Router.toPage(Page.Terminal);
|
Router.toPage(Page.Terminal);
|
||||||
Player.stopFocusing();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
title: (
|
title: (
|
||||||
@@ -334,7 +331,6 @@ export function WorkInProgressRoot(): React.ReactElement {
|
|||||||
},
|
},
|
||||||
unfocus: () => {
|
unfocus: () => {
|
||||||
Router.toPage(Page.Terminal);
|
Router.toPage(Page.Terminal);
|
||||||
Player.stopFocusing();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
title: (
|
title: (
|
||||||
@@ -388,7 +384,6 @@ export function WorkInProgressRoot(): React.ReactElement {
|
|||||||
},
|
},
|
||||||
unfocus: () => {
|
unfocus: () => {
|
||||||
Router.toPage(Page.Faction, { faction });
|
Router.toPage(Page.Faction, { faction });
|
||||||
Player.stopFocusing();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
title: (
|
title: (
|
||||||
@@ -439,7 +434,6 @@ export function WorkInProgressRoot(): React.ReactElement {
|
|||||||
Router.toPage(Page.Job);
|
Router.toPage(Page.Job);
|
||||||
},
|
},
|
||||||
unfocus: () => {
|
unfocus: () => {
|
||||||
Player.stopFocusing();
|
|
||||||
Router.toPage(Page.Job);
|
Router.toPage(Page.Job);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -61,9 +61,11 @@ import { DarknetServer } from "../../../src/Server/DarknetServer";
|
|||||||
import { isDirectoryPath } from "../../../src/Paths/Directory";
|
import { isDirectoryPath } from "../../../src/Paths/Directory";
|
||||||
import { isFilePath } from "../../../src/Paths/FilePath";
|
import { isFilePath } from "../../../src/Paths/FilePath";
|
||||||
import { LAB_CACHE_NAME } from "../../../src/DarkNet/effects/labyrinth";
|
import { LAB_CACHE_NAME } from "../../../src/DarkNet/effects/labyrinth";
|
||||||
import { generateCacheFilename } from "../../../src/DarkNet/effects/cacheFiles";
|
import { generateCacheFilename, getStockReward } from "../../../src/DarkNet/effects/cacheFiles";
|
||||||
import { getAllDarknetServers } from "../../../src/DarkNet/utils/darknetNetworkUtils";
|
import { getAllDarknetServers } from "../../../src/DarkNet/utils/darknetNetworkUtils";
|
||||||
import { prestigeAugmentation } from "../../../src/Prestige";
|
import { prestigeAugmentation } from "../../../src/Prestige";
|
||||||
|
import { initStockMarket, StockMarket } from "../../../src/StockMarket/StockMarket";
|
||||||
|
import { StockSymbol } from "@enums";
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
initGameEnvironment();
|
initGameEnvironment();
|
||||||
@@ -839,3 +841,48 @@ describe("Clue filename generator", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Stock cache reward", () => {
|
||||||
|
test("stock reward does not exceed maxShares and falls back to money reward", () => {
|
||||||
|
initStockMarket();
|
||||||
|
|
||||||
|
const remaining = 3;
|
||||||
|
// Fill every stock to near capacity so no matter which one is randomly picked, it triggers clamping
|
||||||
|
for (const stockName of Object.keys(StockSymbol)) {
|
||||||
|
const stock = StockMarket[stockName];
|
||||||
|
stock.playerShares = stock.maxShares - remaining;
|
||||||
|
stock.playerShortShares = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use high difficulty to ensure the unclamped share count would exceed remaining
|
||||||
|
const difficulty = 100;
|
||||||
|
const result = getStockReward(difficulty);
|
||||||
|
|
||||||
|
// Should have awarded at most `remaining` shares
|
||||||
|
expect(result).toContain(`${remaining} shares`);
|
||||||
|
|
||||||
|
// Verify the chosen stock was clamped to exactly maxShares
|
||||||
|
for (const stockName of Object.keys(StockSymbol)) {
|
||||||
|
const stock = StockMarket[stockName];
|
||||||
|
expect(stock.playerShares).toBeLessThanOrEqual(stock.maxShares);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("stock reward falls back to money when stock is fully owned", () => {
|
||||||
|
initStockMarket();
|
||||||
|
|
||||||
|
// Fill every stock to max capacity
|
||||||
|
for (const stockName of Object.keys(StockSymbol)) {
|
||||||
|
const stock = StockMarket[stockName];
|
||||||
|
stock.playerShares = stock.maxShares;
|
||||||
|
stock.playerShortShares = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moneyBefore = Player.money;
|
||||||
|
const result = getStockReward(5);
|
||||||
|
|
||||||
|
// Should have fallen back to a money reward
|
||||||
|
expect(result).toContain("discovered a cache with");
|
||||||
|
expect(Player.money).toBeGreaterThan(moneyBefore);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { FactionName } from "@enums";
|
import { FactionName } from "@enums";
|
||||||
import { Player } from "@player";
|
import { Player } from "@player";
|
||||||
import { Gang } from "../../../src/Gang/Gang";
|
|
||||||
import { AllGangs } from "../../../src/Gang/AllGangs";
|
import { AllGangs } from "../../../src/Gang/AllGangs";
|
||||||
import { getNS, initGameEnvironment, setupBasicTestingEnvironment } from "../Utilities";
|
import { getNS, getWorkerScriptAndNS, initGameEnvironment, setupBasicTestingEnvironment } from "../Utilities";
|
||||||
|
import { GangConstants } from "../../../src/Gang/data/Constants";
|
||||||
|
import { joinFaction } from "../../../src/Faction/FactionHelpers";
|
||||||
|
import { Factions } from "../../../src/Faction/Factions";
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
initGameEnvironment();
|
initGameEnvironment();
|
||||||
@@ -10,12 +12,16 @@ beforeAll(() => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setupBasicTestingEnvironment();
|
setupBasicTestingEnvironment();
|
||||||
// Give the player a gang so gang API is accessible
|
Player.sourceFiles.set(2, 3);
|
||||||
Player.gang = new Gang(FactionName.SlumSnakes, false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ns.gang.getAllGangInformation", () => {
|
describe("ns.gang.getAllGangInformation", () => {
|
||||||
it("should return territory and power info for all gangs including the player's", () => {
|
beforeEach(() => {
|
||||||
|
// Give the player a gang so gang API is accessible
|
||||||
|
Player.startGang(FactionName.SlumSnakes, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return territory and power info for all gangs including the player's", () => {
|
||||||
const ns = getNS();
|
const ns = getNS();
|
||||||
const info = ns.gang.getAllGangInformation();
|
const info = ns.gang.getAllGangInformation();
|
||||||
const gangNames = Object.keys(info);
|
const gangNames = Object.keys(info);
|
||||||
@@ -35,7 +41,7 @@ describe("ns.gang.getAllGangInformation", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return copies, not references to the original AllGangs data", () => {
|
test("should return copies, not references to the original AllGangs data", () => {
|
||||||
const ns = getNS();
|
const ns = getNS();
|
||||||
const info = ns.gang.getAllGangInformation();
|
const info = ns.gang.getAllGangInformation();
|
||||||
|
|
||||||
@@ -44,3 +50,55 @@ describe("ns.gang.getAllGangInformation", () => {
|
|||||||
expect(AllGangs[FactionName.SlumSnakes].power).not.toBe(999999);
|
expect(AllGangs[FactionName.SlumSnakes].power).not.toBe(999999);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("createGang", () => {
|
||||||
|
test("Success", () => {
|
||||||
|
const ns = getNS();
|
||||||
|
Player.karma = GangConstants.GangKarmaRequirement;
|
||||||
|
joinFaction(Factions[FactionName.SlumSnakes]);
|
||||||
|
expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(true);
|
||||||
|
});
|
||||||
|
describe("Failure", () => {
|
||||||
|
test("Already have a gang", () => {
|
||||||
|
const { ws, ns } = getWorkerScriptAndNS();
|
||||||
|
Player.karma = GangConstants.GangKarmaRequirement;
|
||||||
|
joinFaction(Factions[FactionName.SlumSnakes]);
|
||||||
|
expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(true);
|
||||||
|
|
||||||
|
expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(false);
|
||||||
|
expect(ws.scriptRef.logs[0]).toMatch("You already have a gang");
|
||||||
|
expect(ns.gang.createGang(FactionName.Tetrads)).toBe(false);
|
||||||
|
expect(ws.scriptRef.logs[1]).toMatch("You already have a gang");
|
||||||
|
});
|
||||||
|
test("Disabled by advanced options", () => {
|
||||||
|
const { ws, ns } = getWorkerScriptAndNS();
|
||||||
|
Player.bitNodeOptions.disableGang = true;
|
||||||
|
expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(false);
|
||||||
|
expect(ws.scriptRef.logs[0]).toMatch("Gang is disabled by advanced options");
|
||||||
|
});
|
||||||
|
test("Not have Source-File 2", () => {
|
||||||
|
const { ws, ns } = getWorkerScriptAndNS();
|
||||||
|
Player.sourceFiles.set(2, 0);
|
||||||
|
expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(false);
|
||||||
|
expect(ws.scriptRef.logs[0]).toMatch("You do not have Source-File 2");
|
||||||
|
});
|
||||||
|
test("Not enough karma", () => {
|
||||||
|
const { ws, ns } = getWorkerScriptAndNS();
|
||||||
|
expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(false);
|
||||||
|
expect(ws.scriptRef.logs[0]).toMatch("Your karma must be less than or equal to");
|
||||||
|
});
|
||||||
|
test("Invalid gang faction", () => {
|
||||||
|
const { ws, ns } = getWorkerScriptAndNS();
|
||||||
|
Player.karma = GangConstants.GangKarmaRequirement;
|
||||||
|
joinFaction(Factions[FactionName.SlumSnakes]);
|
||||||
|
expect(ns.gang.createGang(FactionName.Illuminati)).toBe(false);
|
||||||
|
expect(ws.scriptRef.logs[0]).toMatch("does not allow creating a gang");
|
||||||
|
});
|
||||||
|
test("Not a faction member", () => {
|
||||||
|
const { ws, ns } = getWorkerScriptAndNS();
|
||||||
|
Player.karma = GangConstants.GangKarmaRequirement;
|
||||||
|
expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(false);
|
||||||
|
expect(ws.scriptRef.logs[0]).toMatch("You are not a member of");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -178,9 +178,9 @@ describe("getTabCompletionPossibilities", function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("completes the ls and cd commands", async () => {
|
it("completes the ls, cd and upload commands", async () => {
|
||||||
writeFiles();
|
writeFiles();
|
||||||
for (const command of ["ls", "cd"]) {
|
for (const command of ["ls", "cd", "upload"]) {
|
||||||
const options = await getTabCompletionPossibilities(`${command} `, root);
|
const options = await getTabCompletionPossibilities(`${command} `, root);
|
||||||
expect(options.sort()).toEqual(["folder1/", "anotherFolder/"].sort());
|
expect(options.sort()).toEqual(["folder1/", "anotherFolder/"].sort());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user