Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CORPORATION: Add new API to check if player can create corporation #1598

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions markdown/bitburner.corporation.cancreatecorporation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Corporation](./bitburner.corporation.md) &gt; [canCreateCorporation](./bitburner.corporation.cancreatecorporation.md)

## Corporation.canCreateCorporation() method

Return whether the player can create a corporation. Does not require API access.

**Signature:**

```typescript
canCreateCorporation(selfFund: boolean): CreatingCorporationCheckResult;
```

## Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| selfFund | boolean | true if you want to self-fund, false otherwise |

**Returns:**

[CreatingCorporationCheckResult](./bitburner.creatingcorporationcheckresult.md)

Result of the check

## Remarks

RAM cost: 0 GB

14 changes: 6 additions & 8 deletions markdown/bitburner.corporation.createcorporation.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

## Corporation.createCorporation() method

Create a Corporation.
Create a Corporation. You should use [canCreateCorporation](./bitburner.corporation.cancreatecorporation.md) to check if you are unsure you can do it, because it throws an error in these cases:

- Use seed money outside BitNode 3.

- Be in a BitNode that has CorporationSoftcap (a BitNode modifier) less than 0.15.

**Signature:**

Expand All @@ -16,7 +20,7 @@ createCorporation(corporationName: string, selfFund: boolean): boolean;

| Parameter | Type | Description |
| --- | --- | --- |
| corporationName | string | Name of the corporation |
| corporationName | string | Name of the corporation. It must be a non-empty string. |
| selfFund | boolean | If you want to self-fund. Defaults to true, false will only work in BitNode 3. |

**Returns:**
Expand All @@ -29,9 +33,3 @@ true if created and false if not

RAM cost: 20 GB

This function throws an error if:
catloversg marked this conversation as resolved.
Show resolved Hide resolved

- Use seed money outside BitNode 3.

- Be in a BitNode that has CorporationSoftcap (a BN modifier) less than 0.15. Use [getBitNodeMultipliers](./bitburner.ns.getbitnodemultipliers.md) to get the value of this modifier.

2 changes: 1 addition & 1 deletion markdown/bitburner.corporation.hascorporation.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Corporation.hasCorporation() method

Returns whether the player has a corporation. Does not require API access.
Return whether the player has a corporation. Does not require API access.

**Signature:**

Expand Down
5 changes: 3 additions & 2 deletions markdown/bitburner.corporation.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export interface Corporation extends WarehouseAPI, OfficeAPI
| [acceptInvestmentOffer()](./bitburner.corporation.acceptinvestmentoffer.md) | Accept the investment offer. The value of offer is based on current corporation valuation. |
| [bribe(factionName, amountCash)](./bitburner.corporation.bribe.md) | Bribe a faction. |
| [buyBackShares(amount)](./bitburner.corporation.buybackshares.md) | Buyback shares. Spend money from the player's wallet to transfer shares from public traders to the CEO. |
| [createCorporation(corporationName, selfFund)](./bitburner.corporation.createcorporation.md) | Create a Corporation. |
| [canCreateCorporation(selfFund)](./bitburner.corporation.cancreatecorporation.md) | Return whether the player can create a corporation. Does not require API access. |
| [createCorporation(corporationName, selfFund)](./bitburner.corporation.createcorporation.md) | <p>Create a Corporation. You should use [canCreateCorporation](./bitburner.corporation.cancreatecorporation.md) to check if you are unsure you can do it, because it throws an error in these cases:</p><p>- Use seed money outside BitNode 3.</p><p>- Be in a BitNode that has CorporationSoftcap (a BitNode modifier) less than 0.15.</p> |
| [expandCity(divisionName, city)](./bitburner.corporation.expandcity.md) | Expand to a new city. |
| [expandIndustry(industryType, divisionName)](./bitburner.corporation.expandindustry.md) | Expand to a new industry. |
| [getBonusTime()](./bitburner.corporation.getbonustime.md) | Get bonus time. Bonus time is accumulated when the game is offline or if the game is inactive in the browser. Bonus time makes the corporation progress faster. |
Expand All @@ -34,7 +35,7 @@ export interface Corporation extends WarehouseAPI, OfficeAPI
| [getUpgradeLevel(upgradeName)](./bitburner.corporation.getupgradelevel.md) | Get the level of a levelable upgrade. |
| [getUpgradeLevelCost(upgradeName)](./bitburner.corporation.getupgradelevelcost.md) | Get the cost to unlock the next level of a levelable upgrade. |
| [goPublic(numShares)](./bitburner.corporation.gopublic.md) | Go public. |
| [hasCorporation()](./bitburner.corporation.hascorporation.md) | Returns whether the player has a corporation. Does not require API access. |
| [hasCorporation()](./bitburner.corporation.hascorporation.md) | Return whether the player has a corporation. Does not require API access. |
| [hasUnlock(upgradeName)](./bitburner.corporation.hasunlock.md) | Check if you have a one-time unlockable upgrade. |
| [issueDividends(rate)](./bitburner.corporation.issuedividends.md) | Issue dividends. |
| [issueNewShares(amount)](./bitburner.corporation.issuenewshares.md) | Issue new shares. |
Expand Down
23 changes: 23 additions & 0 deletions markdown/bitburner.creatingcorporationcheckresult.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [CreatingCorporationCheckResult](./bitburner.creatingcorporationcheckresult.md)

## CreatingCorporationCheckResult enum


**Signature:**

```typescript
declare enum CreatingCorporationCheckResult
```

## Enumeration Members

| Member | Value | Description |
| --- | --- | --- |
| CorporationExists | <code>&quot;CorporationExists&quot;</code> | |
| DisabledBySoftCap | <code>&quot;DisabledBySoftCap&quot;</code> | |
| NoSf3OrDisabled | <code>&quot;NoSf3OrDisabled&quot;</code> | |
| Success | <code>&quot;Success&quot;</code> | |
| UseSeedMoneyOutsideBN3 | <code>&quot;UseSeedMoneyOutsideBN3&quot;</code> | |

1 change: 1 addition & 0 deletions markdown/bitburner.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
| [BladeburnerSkillName](./bitburner.bladeburnerskillname.md) | Skill names type of Bladeburner |
| [CityName](./bitburner.cityname.md) | Names of all cities |
| [CompanyName](./bitburner.companyname.md) | Names of all companies |
| [CreatingCorporationCheckResult](./bitburner.creatingcorporationcheckresult.md) | |
| [CrimeType](./bitburner.crimetype.md) | |
| [FactionWorkType](./bitburner.factionworktype.md) | |
| [GymLocationName](./bitburner.gymlocationname.md) | Locations of gym |
Expand Down
31 changes: 18 additions & 13 deletions src/Corporation/Actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { OfficeSpace } from "./OfficeSpace";
import { Material } from "./Material";
import { Product } from "./Product";
import { Warehouse } from "./Warehouse";
import { FactionName, IndustryType } from "@enums";
import { CreatingCorporationCheckResult, FactionName, IndustryType } from "@enums";
import { ResearchMap } from "./ResearchMap";
import { isRelevantMaterial } from "./ui/Helpers";
import { CityName } from "@enums";
Expand All @@ -22,27 +22,32 @@ import {
buybackSharesFailureReason,
issueNewSharesFailureReason,
costOfCreatingCorporation,
canCreateCorporation,
} from "./helpers";
import { PositiveInteger } from "../types";
import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
import { Factions } from "../Faction/Factions";

export function createCorporation(corporationName: string, selfFund: boolean, restart: boolean): boolean {
if (!Player.canAccessCorporation()) {
return false;
}
if (Player.corporation && !restart) {
return false;
const checkResult = canCreateCorporation(selfFund, restart);
switch (checkResult) {
case CreatingCorporationCheckResult.Success:
break;
case CreatingCorporationCheckResult.NoSf3OrDisabled:
case CreatingCorporationCheckResult.CorporationExists:
return false;
case CreatingCorporationCheckResult.UseSeedMoneyOutsideBN3:
case CreatingCorporationCheckResult.DisabledBySoftCap:
// In order to maintaining backward compatibility, we have to throw an error in these cases.
throw new Error(checkResult);
default: {
// Verify if switch statement is exhaustive
checkResult satisfies never;
}
}

if (!corporationName) {
return false;
}
if (Player.bitNodeN !== 3 && !selfFund) {
throw new Error("Cannot use seed funds outside of BitNode 3");
}
if (currentNodeMults.CorporationSoftcap < 0.15) {
throw new Error(`You cannot create a corporation in BitNode ${Player.bitNodeN}`);
}

if (selfFund) {
const cost = costOfCreatingCorporation(restart);
Expand Down
8 changes: 8 additions & 0 deletions src/Corporation/Enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,11 @@ export type CorpProductResearchName = Member<typeof CorpProductResearchName>;

export const CorpResearchName = { ...CorpProductResearchName, ...CorpBaseResearchName };
export type CorpResearchName = Member<typeof CorpResearchName>;

export enum CreatingCorporationCheckResult {
Success = "Success",
NoSf3OrDisabled = "NoSf3OrDisabled",
CorporationExists = "CorporationExists",
UseSeedMoneyOutsideBN3 = "UseSeedMoneyOutsideBN3",
DisabledBySoftCap = "DisabledBySoftCap",
}
38 changes: 38 additions & 0 deletions src/Corporation/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,44 @@ import { formatShares } from "../ui/formatNumber";
import { Corporation } from "./Corporation";
import { CorpUpgrade } from "./data/CorporationUpgrades";
import * as corpConstants from "./data/Constants";
import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
import { CreatingCorporationCheckResult } from "@enums";

export function convertCreatingCorporationCheckResultToMessage(checkResult: CreatingCorporationCheckResult): string {
switch (checkResult) {
case CreatingCorporationCheckResult.Success:
return "Success";
case CreatingCorporationCheckResult.NoSf3OrDisabled:
return "You don't have SF3 or Corporation is disabled by an advanced option";
case CreatingCorporationCheckResult.CorporationExists:
return "Corporation exists";
case CreatingCorporationCheckResult.UseSeedMoneyOutsideBN3:
return "You cannot use seed money outside BitNode 3";
case CreatingCorporationCheckResult.DisabledBySoftCap:
return "You cannot create a corporation in this BitNode";
default: {
// Verify if switch statement is exhaustive
checkResult satisfies never;
}
}
return String(checkResult);
}

export function canCreateCorporation(selfFund: boolean, restart: boolean): CreatingCorporationCheckResult {
if (!Player.canAccessCorporation()) {
return CreatingCorporationCheckResult.NoSf3OrDisabled;
}
if (Player.corporation && !restart) {
return CreatingCorporationCheckResult.CorporationExists;
}
if (Player.bitNodeN !== 3 && !selfFund) {
return CreatingCorporationCheckResult.UseSeedMoneyOutsideBN3;
}
if (currentNodeMults.CorporationSoftcap < 0.15) {
return CreatingCorporationCheckResult.DisabledBySoftCap;
}
return CreatingCorporationCheckResult.Success;
}

export function costOfCreatingCorporation(restart: boolean): number {
if (restart && !Player.corporation?.seedFunded) {
Expand Down
1 change: 1 addition & 0 deletions src/Netscript/RamCostGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ const grafting = {

const corporation = {
hasCorporation: 0,
canCreateCorporation: 0,
createCorporation: RamCostConstants.CorporationAction,
hasUnlock: RamCostConstants.CorporationInfo,
getUnlockCost: RamCostConstants.CorporationInfo,
Expand Down
31 changes: 22 additions & 9 deletions src/NetscriptFunctions/Corporation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,20 @@ import {
} from "../Corporation/Actions";
import { CorpUnlocks } from "../Corporation/data/CorporationUnlocks";
import { CorpUpgrades } from "../Corporation/data/CorporationUpgrades";
import { CorpUnlockName, CorpUpgradeName, CorpEmployeeJob, CityName } from "@enums";
import { CorpUnlockName, CorpUpgradeName, CorpEmployeeJob, CityName, CreatingCorporationCheckResult } from "@enums";
import { IndustriesData, IndustryResearchTrees } from "../Corporation/data/IndustryData";
import * as corpConstants from "../Corporation/data/Constants";
import { ResearchMap } from "../Corporation/ResearchMap";
import { InternalAPI, NetscriptContext, setRemovedFunctions } from "../Netscript/APIWrapper";
import { helpers } from "../Netscript/NetscriptHelpers";
import { getEnumHelper } from "../utils/EnumHelper";
import { MaterialInfo } from "../Corporation/MaterialInfo";
import { calculateOfficeSizeUpgradeCost, calculateUpgradeCost } from "../Corporation/helpers";
import {
calculateOfficeSizeUpgradeCost,
calculateUpgradeCost,
canCreateCorporation,
convertCreatingCorporationCheckResultToMessage,
} from "../Corporation/helpers";
import { PositiveInteger } from "../types";
import { getRecordKeys } from "../Types/Record";

Expand Down Expand Up @@ -588,6 +593,21 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
...warehouseAPI,
...officeAPI,
hasCorporation: () => () => !!Player.corporation,
canCreateCorporation: (ctx) => (_selfFund) => {
const selfFund = !!_selfFund;
const checkResult = canCreateCorporation(selfFund, false);
if (checkResult !== CreatingCorporationCheckResult.Success) {
helpers.log(ctx, () => convertCreatingCorporationCheckResultToMessage(checkResult));
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Right as I started reviewing this, I was thinking it might be better to expose (programmatically) why you can't create a corp. Now I see you've gone and made a nice enum for handling it internally. What do you think? True/false is a much simpler API, so I'm sort of on the fence.

If you went that route, you'd want to expose the enum, which would lock us in on these strings.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning an enum instead of true/false is a good idea. I'm all for it. However, I also have the same concern about "string-messages-in-enum-API". That downside looks too big to me.

Copy link
Collaborator

@d0sboots d0sboots Aug 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we made the enum just symbolic constants/names, and kept the string messages internal to the one place that needs them (logging)?

Edit: Just throwing ideas out here, ultimately I'd be OK with true/false, but since we've seen how hard it is to correct APIs IMO it's worth extra think time when adding them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by "symbolic constants/names"? For an enum, we need it to be in this form:

export enum TestEnum1 {
  One = "string 1",
  Two = "string 2",
}

We cannot use it like these ones:

export enum TestEnum2 {
  One,
  Two,
}

export enum TestEnum3 {
  One = 1,
  Two = 2,
}

EnumHelper.ts will throw a fit at that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking like

export enum CorporationCheckResult {
  Success = "Success",
  NoSf3OrDisabled = "NoSf3OrDisabled",
  CorporationExists = "CorporationExists",
  UseSeedMoneyOutsideBN3 = "UseSeedMoneyOutsideBN3",
  DisabledBySoftCap = "DisabledBySoftCap",
}

I.e. the enum values are just restating the names. The values aren't any more useful than the keys here, the point is that it fits the rest of the enum architecture and we aren't worried about value stability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Snarling We are doing an unusual thing in the API here (return an enum instead of true/false), so I want to hear your opinion.

Copy link
Contributor

@Alpheus Alpheus Oct 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context/side-issue: the typing of the new NS function and conversion to JSDoc

We should treat access through the top-most NS apis as a serialization boundary: return types should be symmetrically encodable and decodable to a primitive protocol like JSON or structuredClone.

There's plenty of RAM dodging use cases, backwards compatibility with netscript and some exotic messaging use cases through ports that rely on this.

An Enum is useful to convey choice internally, without the primitive being exposed. That way you can expand the primitive in scope in case you run out of choices. But that always ends up needing helpers if the enum results do get stored in a save file, end up being used for cross-script messaging or printed to a console or logs.

If you're going to use this for a user-facing API, then the use case will predominantly be to check the return value in an if or switch statement. If that's going to be a primitive then the only option left to keep it serializable is to hint it to the string subtype representation of the enum. But without the discriminated union for the enum itself,
ie.:

canCreateCorporation(selfFund: boolean): `${CreatingCorporationCheckResult}`; // ✅ 
canCreateCorporation(selfFund: boolean): CreatingCorporationCheckResult // ❌
canCreateCorporation(selfFund: boolean): CreatingCorporationCheckResult | `${CreatingCorporationCheckResult}` // ❌

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of discussion about whether you should/should not use Typescript's Enums. The general (outside-world) consensus seems to be "maybe not, because of (this list of issues)," but we have chosen to continue using them. We have a lot of Enums in the NS api, they are by no means rare, and they can be serialized just fine. (Although this is a more novel use of them, so I agree with catlover in asking for clarification.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Novel? I figured we were already using the toasts like this:

export type ToastVariantValues = `${ToastVariant}`;

in reference to

toast(msg: string, variant?: ToastVariantValues, duration?: number | null): void;

Is that not a good precedent?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "novel" part is creating an enum with keys=values.

return checkResult;
},
createCorporation:
(ctx) =>
(_corporationName, _selfFund = true): boolean => {
const corporationName = helpers.string(ctx, "corporationName", _corporationName);
const selfFund = !!_selfFund;
return createCorporation(corporationName, selfFund, false);
},
getConstants: () => () => {
/* TODO 2.2: possibly just rework the whole corp constants structure to be more readable, and just use
* structuredClone to provide it directly to player.
Expand Down Expand Up @@ -691,13 +711,6 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
});
return data;
},
createCorporation:
(ctx) =>
(_corporationName, _selfFund = true): boolean => {
const corporationName = helpers.string(ctx, "corporationName", _corporationName);
const selfFund = !!_selfFund;
return createCorporation(corporationName, selfFund, false);
},
hasUnlock: (ctx) => (_unlockName) => {
checkAccess(ctx);
const unlockName = getEnumHelper("CorpUnlockName").nsGetMember(ctx, _unlockName, "unlockName");
Expand Down
34 changes: 27 additions & 7 deletions src/ScriptEditor/NetscriptDefinitions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8673,13 +8673,24 @@ export interface WarehouseAPI {
hasWarehouse(divisionName: string, city: CityName | `${CityName}`): boolean;
}

/**
* @public
*/
declare enum CreatingCorporationCheckResult {
Success = "Success",
NoSf3OrDisabled = "NoSf3OrDisabled",
CorporationExists = "CorporationExists",
UseSeedMoneyOutsideBN3 = "UseSeedMoneyOutsideBN3",
DisabledBySoftCap = "DisabledBySoftCap",
}

/**
* Corporation API
* @public
*/
export interface Corporation extends WarehouseAPI, OfficeAPI {
/**
* Returns whether the player has a corporation. Does not require API access.
* Return whether the player has a corporation. Does not require API access.
*
* @remarks
* RAM cost: 0 GB
Expand All @@ -8689,19 +8700,28 @@ export interface Corporation extends WarehouseAPI, OfficeAPI {
hasCorporation(): boolean;

/**
* Create a Corporation.
* Return whether the player can create a corporation. Does not require API access.
*
* @remarks
* RAM cost: 20 GB
* RAM cost: 0 GB
*
* This function throws an error if:
* @param selfFund - true if you want to self-fund, false otherwise
* @returns Result of the check
*/
canCreateCorporation(selfFund: boolean): CreatingCorporationCheckResult;

/**
* Create a Corporation. You should use {@link Corporation.canCreateCorporation | canCreateCorporation} to check if
* you are unsure you can do it, because it throws an error in these cases:
*
* - Use seed money outside BitNode 3.
*
* - Be in a BitNode that has CorporationSoftcap (a BN modifier) less than 0.15. Use
* {@link NS.getBitNodeMultipliers | getBitNodeMultipliers} to get the value of this modifier.
* - Be in a BitNode that has CorporationSoftcap (a BitNode modifier) less than 0.15.
*
* @remarks
* RAM cost: 20 GB
*
* @param corporationName - Name of the corporation
* @param corporationName - Name of the corporation. It must be a non-empty string.
* @param selfFund - If you want to self-fund. Defaults to true, false will only work in BitNode 3.
* @returns true if created and false if not
*/
Expand Down