mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-16 06:18:42 +02:00
BUGFIX: Coding contract UI does not handle error properly when answer format is invalid (#2171)
This commit is contained in:
@@ -5,6 +5,7 @@ import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver
|
||||
import { CodingContractEvent } from "../ui/React/CodingContractModal";
|
||||
import { ContractFilePath, resolveContractFilePath } from "../Paths/ContractFilePath";
|
||||
import { assertObject } from "../utils/TypeAssertion";
|
||||
import { Result } from "../types";
|
||||
|
||||
// Numeric enum
|
||||
/** Enum representing the different types of rewards a Coding Contract can give */
|
||||
@@ -21,6 +22,7 @@ export enum CodingContractResult {
|
||||
Success,
|
||||
Failure,
|
||||
Cancelled,
|
||||
InvalidFormat,
|
||||
}
|
||||
|
||||
/** A class that represents the type of reward a contract gives */
|
||||
@@ -67,7 +69,9 @@ export class CodingContract {
|
||||
reward: ICodingContractReward | null = null,
|
||||
) {
|
||||
const path = resolveContractFilePath(fn);
|
||||
if (!path) throw new Error(`Bad file path while creating a coding contract: ${fn}`);
|
||||
if (!path) {
|
||||
throw new Error(`Bad file path while creating a coding contract: ${fn}`);
|
||||
}
|
||||
if (!CodingContractTypes[type]) {
|
||||
throw new Error(`Error: invalid contract type: ${type} please contact developer`);
|
||||
}
|
||||
@@ -100,33 +104,59 @@ export class CodingContract {
|
||||
}
|
||||
|
||||
/** Checks if the answer is in the correct format. */
|
||||
isValid(answer: unknown): boolean {
|
||||
if (typeof answer === "string") answer = CodingContractTypes[this.type].convertAnswer(answer);
|
||||
return CodingContractTypes[this.type].validateAnswer(answer);
|
||||
isValid(answer: unknown): Result<{ answer: unknown }> {
|
||||
if (typeof answer === "string") {
|
||||
try {
|
||||
answer = CodingContractTypes[this.type].convertAnswer(answer);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `The answer is not in the right format for contract '${this.type}'. Reason: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
const result = CodingContractTypes[this.type].validateAnswer(answer);
|
||||
if (!result) {
|
||||
return {
|
||||
success: false,
|
||||
message: `The answer is not in the right format for contract '${this.type}'. Got: ${answer}`,
|
||||
};
|
||||
}
|
||||
return { success: true, answer };
|
||||
}
|
||||
|
||||
isSolution(solution: unknown): boolean {
|
||||
const type = CodingContractTypes[this.type];
|
||||
if (typeof solution === "string") solution = type.convertAnswer(solution);
|
||||
if (!this.isValid(solution)) return false;
|
||||
|
||||
return type.solver(this.state, solution);
|
||||
isSolution(solution: unknown): {
|
||||
result: Exclude<CodingContractResult, CodingContractResult.Cancelled>;
|
||||
message?: string;
|
||||
} {
|
||||
const validationResult = this.isValid(solution);
|
||||
if (!validationResult.success) {
|
||||
return { result: CodingContractResult.InvalidFormat, message: validationResult.message };
|
||||
}
|
||||
/**
|
||||
* We sometimes need to convert the given solution by calling CodingContractType.convertAnswer() (e.g., Square Root
|
||||
* contract) before using it. The conversion is done in CodingContract.isValid().
|
||||
*/
|
||||
solution = validationResult.answer;
|
||||
return {
|
||||
result: CodingContractTypes[this.type].solver(this.state, solution)
|
||||
? CodingContractResult.Success
|
||||
: CodingContractResult.Failure,
|
||||
};
|
||||
}
|
||||
|
||||
/** Creates a popup to prompt the player to solve the problem */
|
||||
async prompt(): Promise<CodingContractResult> {
|
||||
return new Promise<CodingContractResult>((resolve) => {
|
||||
async prompt(): Promise<{ result: CodingContractResult; message?: string }> {
|
||||
return new Promise((resolve) => {
|
||||
CodingContractEvent.emit({
|
||||
c: this,
|
||||
onClose: () => {
|
||||
resolve(CodingContractResult.Cancelled);
|
||||
resolve({ result: CodingContractResult.Cancelled });
|
||||
},
|
||||
onAttempt: (val: string) => {
|
||||
if (this.isSolution(val)) {
|
||||
resolve(CodingContractResult.Success);
|
||||
} else {
|
||||
resolve(CodingContractResult.Failure);
|
||||
}
|
||||
resolve(this.isSolution(val));
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Player } from "@player";
|
||||
import { CodingContract } from "../CodingContract/Contract";
|
||||
import { CodingContract, CodingContractResult } from "../CodingContract/Contract";
|
||||
import { CodingContractObject, CodingContract as ICodingContract } from "@nsdefs";
|
||||
import { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper";
|
||||
import { helpers } from "../Netscript/NetscriptHelpers";
|
||||
@@ -7,6 +7,7 @@ import { CodingContractName } from "@enums";
|
||||
import { generateDummyContract } from "../CodingContract/ContractGenerator";
|
||||
import { isCodingContractName } from "../CodingContract/ContractTypes";
|
||||
import { type BaseServer } from "../Server/BaseServer";
|
||||
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
|
||||
|
||||
export function NetscriptCodingContract(): InternalAPI<ICodingContract> {
|
||||
const getCodingContract = function (ctx: NetscriptContext, hostname: string, filename: string): CodingContract {
|
||||
@@ -25,26 +26,51 @@ export function NetscriptCodingContract(): InternalAPI<ICodingContract> {
|
||||
contract: CodingContract,
|
||||
answer: unknown,
|
||||
): string {
|
||||
if (contract.isSolution(answer)) {
|
||||
const reward = Player.gainCodingContractReward(contract.reward, contract.getDifficulty());
|
||||
helpers.log(ctx, () => `Successfully completed Coding Contract '${contract.fn}'. Reward: ${reward}`);
|
||||
server.removeContract(contract.fn);
|
||||
return reward;
|
||||
const validationResult = contract.isValid(answer);
|
||||
if (!validationResult.success) {
|
||||
throw helpers.errorMessage(ctx, validationResult.message);
|
||||
}
|
||||
|
||||
if (++contract.tries >= contract.getMaxNumTries()) {
|
||||
helpers.log(ctx, () => `Coding Contract attempt '${contract.fn}' failed. Contract is now self-destructing`);
|
||||
server.removeContract(contract.fn);
|
||||
} else {
|
||||
helpers.log(
|
||||
ctx,
|
||||
() =>
|
||||
`Coding Contract attempt '${contract.fn}' failed. ${
|
||||
contract.getMaxNumTries() - contract.tries
|
||||
} attempt(s) remaining.`,
|
||||
);
|
||||
const resultOfCheckingSolution = contract.isSolution(answer);
|
||||
switch (resultOfCheckingSolution.result) {
|
||||
case CodingContractResult.Success: {
|
||||
const reward = Player.gainCodingContractReward(contract.reward, contract.getDifficulty());
|
||||
helpers.log(ctx, () => `Successfully completed Coding Contract '${contract.fn}'. Reward: ${reward}`);
|
||||
server.removeContract(contract.fn);
|
||||
return reward;
|
||||
}
|
||||
/**
|
||||
* This should never happen. If the answer format is invalid, it should already be handled by the call to
|
||||
* contract.isValid() above.
|
||||
*/
|
||||
case CodingContractResult.InvalidFormat: {
|
||||
exceptionAlert(
|
||||
new Error(
|
||||
`contract.isSolution() returns unexpected InvalidFormat result. Type: ${contract.type}. Answer: ${answer}`,
|
||||
),
|
||||
true,
|
||||
);
|
||||
return "";
|
||||
}
|
||||
case CodingContractResult.Failure: {
|
||||
if (++contract.tries >= contract.getMaxNumTries()) {
|
||||
helpers.log(ctx, () => `Coding Contract attempt '${contract.fn}' failed. Contract is now self-destructing`);
|
||||
server.removeContract(contract.fn);
|
||||
} else {
|
||||
helpers.log(
|
||||
ctx,
|
||||
() =>
|
||||
`Coding Contract attempt '${contract.fn}' failed. ${
|
||||
contract.getMaxNumTries() - contract.tries
|
||||
} attempt(s) remaining.`,
|
||||
);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
default: {
|
||||
const __: never = resultOfCheckingSolution.result;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -53,15 +79,8 @@ export function NetscriptCodingContract(): InternalAPI<ICodingContract> {
|
||||
const filename = helpers.string(ctx, "filename", _filename);
|
||||
const host = _host ? helpers.string(ctx, "host", _host) : ctx.workerScript.hostname;
|
||||
const contract = getCodingContract(ctx, host, filename);
|
||||
|
||||
if (!contract.isValid(answer))
|
||||
throw helpers.errorMessage(
|
||||
ctx,
|
||||
`Answer is not in the right format for contract '${contract.type}'. Got: ${answer}`,
|
||||
);
|
||||
|
||||
const serv = helpers.getServer(ctx, host);
|
||||
return attemptContract(ctx, serv, contract, answer);
|
||||
const server = helpers.getServer(ctx, host);
|
||||
return attemptContract(ctx, server, contract, answer);
|
||||
},
|
||||
getContractType: (ctx) => (_filename, _host?) => {
|
||||
const filename = helpers.string(ctx, "filename", _filename);
|
||||
|
||||
@@ -503,40 +503,51 @@ export class Terminal {
|
||||
return this.error("There's already a Coding Contract in Progress");
|
||||
}
|
||||
|
||||
const serv = Player.getCurrentServer();
|
||||
const contract = serv.getContract(contractPath);
|
||||
if (!contract) return this.error("No such contract");
|
||||
const server = Player.getCurrentServer();
|
||||
const contract = server.getContract(contractPath);
|
||||
if (!contract) {
|
||||
return this.error("No such contract");
|
||||
}
|
||||
|
||||
this.contractOpen = true;
|
||||
const res = await contract.prompt();
|
||||
const promptResult = await contract.prompt();
|
||||
|
||||
//Check if the contract still exists by the time the promise is fulfilled
|
||||
if (serv.getContract(contractPath) == null) {
|
||||
if (server.getContract(contractPath) == null) {
|
||||
this.contractOpen = false;
|
||||
return this.error("Contract no longer exists (Was it solved by a script?)");
|
||||
}
|
||||
|
||||
switch (res) {
|
||||
switch (promptResult.result) {
|
||||
case CodingContractResult.Success:
|
||||
if (contract.reward !== null) {
|
||||
const reward = Player.gainCodingContractReward(contract.reward, contract.getDifficulty());
|
||||
this.print(`Contract SUCCESS - ${reward}`);
|
||||
}
|
||||
serv.removeContract(contract);
|
||||
server.removeContract(contract);
|
||||
break;
|
||||
case CodingContractResult.InvalidFormat:
|
||||
this.error(
|
||||
`Contract FAILED - ${
|
||||
promptResult.message ?? `The answer is not in the right format for contract '${contract.type}'`
|
||||
}`,
|
||||
);
|
||||
break;
|
||||
case CodingContractResult.Failure:
|
||||
++contract.tries;
|
||||
if (contract.tries >= contract.getMaxNumTries()) {
|
||||
this.error("Contract FAILED - Contract is now self-destructing");
|
||||
serv.removeContract(contract);
|
||||
server.removeContract(contract);
|
||||
} else {
|
||||
this.error(`Contract FAILED - ${contract.getMaxNumTries() - contract.tries} tries remaining`);
|
||||
}
|
||||
break;
|
||||
case CodingContractResult.Cancelled:
|
||||
default:
|
||||
this.print("Contract cancelled");
|
||||
break;
|
||||
default: {
|
||||
const __: never = promptResult.result;
|
||||
}
|
||||
}
|
||||
this.contractOpen = false;
|
||||
}
|
||||
|
||||
@@ -24,22 +24,26 @@ export function CodingContractModal(): React.ReactElement {
|
||||
const [answer, setAnswer] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
CodingContractEvent.subscribe((props) => setContract(props));
|
||||
});
|
||||
return CodingContractEvent.subscribe((props) => setContract(props));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
contract?.onClose();
|
||||
};
|
||||
}, [contract]);
|
||||
|
||||
if (contract === null) return <></>;
|
||||
if (contract === null) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
function onChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
setAnswer(event.target.value);
|
||||
}
|
||||
|
||||
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
|
||||
if (contract === null) return;
|
||||
if (contract === null) {
|
||||
return;
|
||||
}
|
||||
const value = event.currentTarget.value;
|
||||
|
||||
if (event.key === KEY.ENTER && value !== "") {
|
||||
@@ -51,15 +55,22 @@ export function CodingContractModal(): React.ReactElement {
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
if (contract === null) return;
|
||||
if (contract === null) {
|
||||
return;
|
||||
}
|
||||
contract.onClose();
|
||||
setContract(null);
|
||||
}
|
||||
|
||||
const contractType = CodingContractTypes[contract.c.type];
|
||||
const description = [];
|
||||
for (const [i, value] of contractType.desc(contract.c.getData()).split("\n").entries())
|
||||
description.push(<span key={i} dangerouslySetInnerHTML={{ __html: value + "<br />" }}></span>);
|
||||
for (const [i, value] of contractType.desc(contract.c.getData()).split("\n").entries()) {
|
||||
description.push(
|
||||
<span key={i}>
|
||||
{value} <br />
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Modal open={contract !== null} onClose={close}>
|
||||
<CopyableText variant="h4" value={contract.c.type} />
|
||||
|
||||
Reference in New Issue
Block a user