mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-17 23:08:36 +02:00
Compare commits
3 Commits
d1b6acc57a
...
63aa4d2a45
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63aa4d2a45 | ||
|
|
abdf3082ca | ||
|
|
8dcccdc5bb |
@@ -25,5 +25,77 @@
|
||||
<div>
|
||||
<h1>Close me when operation is completed.</h1>
|
||||
</div>
|
||||
<!-- Use esm for top-level await -->
|
||||
<script type="module">
|
||||
const databaseName = "bitburnerSave";
|
||||
// Check src/db.ts to see why the current max version is 2. If the database version is greater than this value, it
|
||||
// means that the code in this file is outdated.
|
||||
const maxDatabaseVersion = 2;
|
||||
const databases = await window.indexedDB.databases();
|
||||
const database = databases.find((info) => info.name === databaseName);
|
||||
if (!database) {
|
||||
alert("There is no save data");
|
||||
// This is the simplest way to stop execution in top-level code without using a labeled block or IIFE.
|
||||
throw new Error("There is no save data");
|
||||
}
|
||||
if (database.version === undefined || database.version > maxDatabaseVersion) {
|
||||
alert(`Invalid database version: ${database.version}`);
|
||||
throw new Error(`Invalid database version: ${database.version}`);
|
||||
}
|
||||
// Do NOT specify the version. We must open the database at the current version; otherwise, we will trigger
|
||||
// onupgradeneeded.
|
||||
const dbRequest = window.indexedDB.open(databaseName);
|
||||
dbRequest.onerror = (event) => {
|
||||
console.error(event.target.error);
|
||||
alert(event.target.error);
|
||||
};
|
||||
dbRequest.onsuccess = () => {
|
||||
const db = dbRequest.result;
|
||||
try {
|
||||
if (!db.objectStoreNames.contains("savestring")) {
|
||||
alert("There is no save data");
|
||||
return;
|
||||
}
|
||||
const transaction = db.transaction(["savestring"], "readonly");
|
||||
const objectStore = transaction.objectStore("savestring");
|
||||
const request = objectStore.get("save");
|
||||
request.onsuccess = () => {
|
||||
if (request.result == null) {
|
||||
alert("There is no save data");
|
||||
return;
|
||||
}
|
||||
let isBinaryFormat;
|
||||
if (request.result instanceof Uint8Array) {
|
||||
// All modules in the Electron folder are CommonJS, so importing them here would be really difficult. The
|
||||
// isBinaryFormat function is very small, so let's inline it here.
|
||||
isBinaryFormat = true;
|
||||
const magicBytesOfDeflateGzip = [0x1f, 0x8b, 0x08];
|
||||
for (let i = 0; i < magicBytesOfDeflateGzip.length; ++i) {
|
||||
if (magicBytesOfDeflateGzip[i] !== request.result[i]) {
|
||||
isBinaryFormat = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
isBinaryFormat = false;
|
||||
}
|
||||
const extension = isBinaryFormat ? "json.gz" : "json";
|
||||
const filename = `bitburnerSave_${Date.now()}.${extension}`;
|
||||
const blob = new Blob([request.result]);
|
||||
const anchorElement = document.createElement("a");
|
||||
const url = URL.createObjectURL(blob);
|
||||
anchorElement.href = url;
|
||||
anchorElement.download = filename;
|
||||
anchorElement.click();
|
||||
setTimeout(function () {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert(error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -258,7 +258,6 @@ app.on("ready", async () => {
|
||||
await window.loadFile("export.html");
|
||||
window.show();
|
||||
setStopProcessHandler(window);
|
||||
await utils.exportSave(window);
|
||||
} else {
|
||||
window = await startWindow(process.argv.includes("--no-scripts"));
|
||||
if (global.steamworksError) {
|
||||
|
||||
@@ -90,36 +90,6 @@ function showErrorBox(title, error) {
|
||||
dialog.showErrorBox(title, `${error.name}\n\n${error.message}`);
|
||||
}
|
||||
|
||||
function exportSaveFromIndexedDb() {
|
||||
return new Promise((resolve) => {
|
||||
const dbRequest = indexedDB.open("bitburnerSave");
|
||||
dbRequest.onsuccess = () => {
|
||||
const db = dbRequest.result;
|
||||
const transaction = db.transaction(["savestring"], "readonly");
|
||||
const store = transaction.objectStore("savestring");
|
||||
const request = store.get("save");
|
||||
request.onsuccess = () => {
|
||||
const file = new Blob([request.result], { type: "text/plain" });
|
||||
const a = document.createElement("a");
|
||||
const url = URL.createObjectURL(file);
|
||||
a.href = url;
|
||||
a.download = "save.json";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(function () {
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
resolve();
|
||||
}, 0);
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function exportSave(window) {
|
||||
await window.webContents.executeJavaScript(`${exportSaveFromIndexedDb.toString()}; exportSaveFromIndexedDb();`, true);
|
||||
}
|
||||
|
||||
async function writeTerminal(window, message, type = null) {
|
||||
await window.webContents.executeJavaScript(`window.appNotifier.terminal("${message}", "${type}");`, true);
|
||||
}
|
||||
@@ -186,7 +156,6 @@ function initializeLogLevelConfig() {
|
||||
module.exports = {
|
||||
reloadAndKill,
|
||||
showErrorBox,
|
||||
exportSave,
|
||||
attachUnresponsiveAppHandler,
|
||||
detachUnresponsiveAppHandler,
|
||||
writeTerminal,
|
||||
|
||||
@@ -83,6 +83,41 @@ export function removeBracketsFromArrayString(str: string): string {
|
||||
return strCpy;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function only performs very simple checks to add the outermost optional brackets. Callers must perform other
|
||||
* preprocessing steps and postprocessing validations. For example:
|
||||
* - "[ [0, 1]]" will be incorrectly wrapped and converted to "[[[0,1]]]". Callers need to remove redundant whitespace.
|
||||
* - "[[1,2],3]" is not an array of arrays, but this function will return it as is. Callers need to call validateAnswer.
|
||||
*
|
||||
* Note:
|
||||
* - "" will always be converted to an empty array ([]).
|
||||
* - When parsing an array of arrays (isArrayOfArray = true), "[]" will be converted to an empty array ([]), not an
|
||||
* array containing an empty array ([[]]).
|
||||
*/
|
||||
export function parseArrayString(answer: string, isArrayOfArray = false): unknown {
|
||||
let modifiedAnswer = answer.trim();
|
||||
|
||||
if (isArrayOfArray && modifiedAnswer === "[]") {
|
||||
return [];
|
||||
}
|
||||
|
||||
// If it doesn't start with any bracket, it's definitely "naked".
|
||||
if (!modifiedAnswer.startsWith("[")) {
|
||||
modifiedAnswer = `[${modifiedAnswer}]`;
|
||||
} else if (isArrayOfArray && !modifiedAnswer.startsWith("[[")) {
|
||||
// If it's supposed to be an array of arrays but only has one "[".
|
||||
modifiedAnswer = `[${modifiedAnswer}]`;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(modifiedAnswer);
|
||||
} catch (error) {
|
||||
console.error(`Invalid answer: ${answer}`);
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function removeQuotesFromString(str: string): string {
|
||||
let strCpy: string = str;
|
||||
if (strCpy.startsWith('"') || strCpy.startsWith("'")) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { filterTruthy } from "../../utils/helpers/ArrayHelpers";
|
||||
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
|
||||
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
|
||||
import { CodingContractTypes, removeBracketsFromArrayString, removeQuotesFromString } from "../ContractTypes";
|
||||
import { CodingContractTypes, parseArrayString } from "../ContractTypes";
|
||||
import { CodingContractName } from "@enums";
|
||||
|
||||
export const findAllValidMathExpressions: Pick<CodingContractTypes, CodingContractName.FindAllValidMathExpressions> = {
|
||||
@@ -107,10 +106,12 @@ export const findAllValidMathExpressions: Pick<CodingContractTypes, CodingContra
|
||||
return result.every((sol) => solutions.has(sol));
|
||||
},
|
||||
convertAnswer: (ans) => {
|
||||
const sanitized = removeBracketsFromArrayString(ans).split(",");
|
||||
return filterTruthy(sanitized).map((s) => removeQuotesFromString(s.replace(/\s/g, "")));
|
||||
const parsedAnswer = parseArrayString(ans);
|
||||
if (!findAllValidMathExpressions[CodingContractName.FindAllValidMathExpressions].validateAnswer(parsedAnswer)) {
|
||||
return null;
|
||||
}
|
||||
return parsedAnswer;
|
||||
},
|
||||
validateAnswer: (ans): ans is string[] =>
|
||||
typeof ans === "object" && Array.isArray(ans) && ans.every((s) => typeof s === "string"),
|
||||
validateAnswer: (ans): ans is string[] => Array.isArray(ans) && ans.every((s) => typeof s === "string"),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CodingContractName } from "@enums";
|
||||
import { CodingContractTypes, removeBracketsFromArrayString } from "../ContractTypes";
|
||||
import { CodingContractTypes, parseArrayString } from "../ContractTypes";
|
||||
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
|
||||
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
|
||||
|
||||
@@ -67,10 +67,12 @@ export const generateIPAddresses: Pick<CodingContractTypes, CodingContractName.G
|
||||
return ret.length === answer.length && ret.every((ip) => answer.includes(ip));
|
||||
},
|
||||
convertAnswer: (ans) => {
|
||||
const sanitized = removeBracketsFromArrayString(ans).replace(/\s/g, "");
|
||||
return sanitized.split(",").map((ip) => ip.replace(/^(?<quote>['"])([\d.]*)\k<quote>$/g, "$2"));
|
||||
const parsedAnswer = parseArrayString(ans);
|
||||
if (!generateIPAddresses[CodingContractName.GenerateIPAddresses].validateAnswer(parsedAnswer)) {
|
||||
return null;
|
||||
}
|
||||
return parsedAnswer;
|
||||
},
|
||||
validateAnswer: (ans): ans is string[] =>
|
||||
typeof ans === "object" && Array.isArray(ans) && ans.every((s) => typeof s === "string"),
|
||||
validateAnswer: (ans): ans is string[] => Array.isArray(ans) && ans.every((s) => typeof s === "string"),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
|
||||
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
|
||||
import { CodingContractTypes } from "../ContractTypes";
|
||||
import { parseArrayString, CodingContractTypes } from "../ContractTypes";
|
||||
import { CodingContractName } from "@enums";
|
||||
|
||||
export const largestRectangle: Pick<CodingContractTypes, CodingContractName.LargestRectangleInAMatrix> = {
|
||||
@@ -152,13 +152,7 @@ Answer: [[0,0],[3,1]]
|
||||
return userArea === (solution[1][0] - solution[0][0] + 1) * (solution[1][1] - solution[0][1] + 1);
|
||||
},
|
||||
convertAnswer: (ans) => {
|
||||
let parsedAnswer: unknown;
|
||||
try {
|
||||
parsedAnswer = JSON.parse(ans);
|
||||
} catch (error) {
|
||||
console.error("Invalid answer:", error);
|
||||
return null;
|
||||
}
|
||||
const parsedAnswer = parseArrayString(ans.replace(/\s/g, ""), true);
|
||||
if (!largestRectangle[CodingContractName.LargestRectangleInAMatrix].validateAnswer(parsedAnswer)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
|
||||
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
|
||||
import { CodingContractTypes, convert2DArrayToString, removeBracketsFromArrayString } from "../ContractTypes";
|
||||
import { CodingContractTypes, convert2DArrayToString, parseArrayString } from "../ContractTypes";
|
||||
import { CodingContractName } from "@enums";
|
||||
|
||||
export const mergeOverlappingIntervals: Pick<CodingContractTypes, CodingContractName.MergeOverlappingIntervals> = {
|
||||
@@ -70,20 +70,13 @@ export const mergeOverlappingIntervals: Pick<CodingContractTypes, CodingContract
|
||||
return result.length === answer.length && result.every((a, i) => a[0] === answer[i][0] && a[1] === answer[i][1]);
|
||||
},
|
||||
convertAnswer: (ans) => {
|
||||
const arrayRegex = /\[\d+,\d+\]/g;
|
||||
const matches = ans.replace(/\s/g, "").match(arrayRegex);
|
||||
if (matches === null) return null;
|
||||
const arr = matches.map((a) =>
|
||||
removeBracketsFromArrayString(a)
|
||||
.split(",")
|
||||
.map((n) => parseInt(n)),
|
||||
);
|
||||
// An inline function is needed here, so that TS knows this returns true if it matches the type
|
||||
if (((a: number[][]): a is [number, number][] => a.every((n) => n.length === 2))(arr)) return arr;
|
||||
return null;
|
||||
const parsedAnswer = parseArrayString(ans.replace(/\s/g, ""), true);
|
||||
if (!mergeOverlappingIntervals[CodingContractName.MergeOverlappingIntervals].validateAnswer(parsedAnswer)) {
|
||||
return null;
|
||||
}
|
||||
return parsedAnswer;
|
||||
},
|
||||
validateAnswer: (ans): ans is [number, number][] =>
|
||||
typeof ans === "object" &&
|
||||
Array.isArray(ans) &&
|
||||
ans.every((a) => Array.isArray(a) && a.length === 2 && a.every((n) => typeof n === "number")),
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CodingContractTypes, removeBracketsFromArrayString } from "../ContractTypes";
|
||||
import { CodingContractTypes, parseArrayString } from "../ContractTypes";
|
||||
import { CodingContractName } from "@enums";
|
||||
|
||||
export const proper2ColoringOfAGraph: Pick<CodingContractTypes, CodingContractName.Proper2ColoringOfAGraph> = {
|
||||
@@ -121,14 +121,12 @@ export const proper2ColoringOfAGraph: Pick<CodingContractTypes, CodingContractNa
|
||||
return data[1].every(([a, b]) => answer[a] !== answer[b]);
|
||||
},
|
||||
convertAnswer: (ans) => {
|
||||
const sanitized = removeBracketsFromArrayString(ans).replace(/\s/g, "");
|
||||
if (sanitized === "") return [];
|
||||
const arr = sanitized.split(",").map((s) => parseInt(s, 10));
|
||||
// An inline function is needed here, so that TS knows this returns true if it matches the type
|
||||
if (((a): a is (1 | 0)[] => !a.some((n) => n !== 1 && n !== 0))(arr)) return arr;
|
||||
return null;
|
||||
const parsedAnswer = parseArrayString(ans.replace(/\s/g, ""));
|
||||
if (!proper2ColoringOfAGraph[CodingContractName.Proper2ColoringOfAGraph].validateAnswer(parsedAnswer)) {
|
||||
return null;
|
||||
}
|
||||
return parsedAnswer;
|
||||
},
|
||||
validateAnswer: (ans): ans is (1 | 0)[] =>
|
||||
typeof ans === "object" && Array.isArray(ans) && !ans.some((a) => a !== 1 && a !== 0),
|
||||
validateAnswer: (ans): ans is (1 | 0)[] => Array.isArray(ans) && !ans.some((a) => a !== 1 && a !== 0),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
|
||||
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
|
||||
import { CodingContractTypes, removeBracketsFromArrayString, removeQuotesFromString } from "../ContractTypes";
|
||||
import { CodingContractTypes, parseArrayString } from "../ContractTypes";
|
||||
import { CodingContractName } from "@enums";
|
||||
|
||||
export const sanitizeParenthesesInExpression: Pick<
|
||||
@@ -113,10 +113,16 @@ export const sanitizeParenthesesInExpression: Pick<
|
||||
return res.every((sol) => answer.includes(sol));
|
||||
},
|
||||
convertAnswer: (ans) => {
|
||||
const sanitized = removeBracketsFromArrayString(ans).split(",");
|
||||
return sanitized.map((s) => removeQuotesFromString(s.replace(/\s/g, "")));
|
||||
const parsedAnswer = parseArrayString(ans);
|
||||
if (
|
||||
!sanitizeParenthesesInExpression[CodingContractName.SanitizeParenthesesInExpression].validateAnswer(
|
||||
parsedAnswer,
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return parsedAnswer;
|
||||
},
|
||||
validateAnswer: (ans): ans is string[] =>
|
||||
typeof ans === "object" && Array.isArray(ans) && ans.every((s) => typeof s === "string"),
|
||||
validateAnswer: (ans): ans is string[] => Array.isArray(ans) && ans.every((s) => typeof s === "string"),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ export const shortestPathInAGrid: Pick<CodingContractTypes, CodingContractName.S
|
||||
" [[0,1],\n",
|
||||
" [1,0]]\n",
|
||||
"\n",
|
||||
"Answer: ''",
|
||||
`Answer: ""`,
|
||||
].join(" ");
|
||||
},
|
||||
difficulty: 7,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CodingContractName } from "@enums";
|
||||
import { removeBracketsFromArrayString, type CodingContractTypes } from "../ContractTypes";
|
||||
import { parseArrayString, type CodingContractTypes } from "../ContractTypes";
|
||||
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
|
||||
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
|
||||
|
||||
@@ -127,10 +127,12 @@ export const spiralizeMatrix: Pick<CodingContractTypes, CodingContractName.Spira
|
||||
return spiral.length === answer.length && spiral.every((n, i) => n === answer[i]);
|
||||
},
|
||||
convertAnswer: (ans) => {
|
||||
const sanitized = removeBracketsFromArrayString(ans).replace(/\s/g, "").split(",");
|
||||
return sanitized.map((s) => parseInt(s));
|
||||
const parsedAnswer = parseArrayString(ans);
|
||||
if (!spiralizeMatrix[CodingContractName.SpiralizeMatrix].validateAnswer(parsedAnswer)) {
|
||||
return null;
|
||||
}
|
||||
return parsedAnswer;
|
||||
},
|
||||
validateAnswer: (ans): ans is number[] =>
|
||||
typeof ans === "object" && Array.isArray(ans) && ans.every((n) => typeof n === "number"),
|
||||
validateAnswer: (ans): ans is number[] => Array.isArray(ans) && ans.every((n) => typeof n === "number"),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -40,24 +40,92 @@ The [`getContractTypes`](../../../../../markdown/bitburner.codingcontract.getcon
|
||||
|
||||
## Submitting Solutions
|
||||
|
||||
### General rules
|
||||
|
||||
Different contract problem types will require different types of solutions.
|
||||
Some may be numbers, others may be strings or arrays.
|
||||
|
||||
If a contract asks for a specific solution format, then use that.
|
||||
Otherwise, follow these rules when submitting solutions:
|
||||
|
||||
- String-type solutions should **not** have quotation marks surrounding the string (unless specifically asked for).
|
||||
- String-type solutions (e.g., Shortest Path in a Grid) should **not** have quotation marks surrounding the string (unless specifically asked for). For example, if your answer is `foo` (3 characters: f, o, o), just submit those 3 characters. Don't submit `"foo"` (5 characters).
|
||||
Only quotation marks that are part of the actual string solution should be included.
|
||||
- With array-of-strings solutions (e.g., Generate IP Addresses), you need to use double quotes surrounding the string values. Don't use single quotes (`''`) or backticks (\`\`). For example, if your answer is an array containing `foo` (3 characters: f, o, o) and `bar` (3 characters: b, a, r), you should submit `["foo", "bar"]`. Don't submit `['foo', 'bar']`.
|
||||
- Array-type solutions should be submitted with each element in the array separated by commas.
|
||||
Brackets are optional.
|
||||
For example, both of the following are valid solution formats:
|
||||
- `1,2,3`
|
||||
- `[1,2,3]`
|
||||
- If the solution is a multidimensional array, then all arrays that are not the outer-most array DO require the brackets.
|
||||
For example, an array of arrays can be submitted as one of the following:
|
||||
- Numeric solutions should be submitted normally, as expected.
|
||||
- Read the description carefully. Some contracts (e.g., the "Square Root" contract) clearly specify the expected solution format.
|
||||
- If the solution format is not a string, you should not convert the answer to a string. Read the next sections carefully if you do so.
|
||||
|
||||
### String conversion
|
||||
|
||||
For convenience (e.g., submitting the answer via the UI) and backward compatibility, the game accepts a string answer even when
|
||||
the solution format is not a string. In these cases, the game converts your string answer to the expected format. However,
|
||||
this conversion has many pitfalls.
|
||||
|
||||
String conversion only matters when you submit the answer via the UI (your answer, typed in the text box, is always a string). When you call the `ns.codingcontract.attempt` API, you should never convert your non-string answer to a string unless specifically asked for.
|
||||
|
||||
First, with arrays, the outermost pair of brackets is optional. For example, both of the following are valid solution formats:
|
||||
|
||||
- `1,2,3`
|
||||
- `[1,2,3]`
|
||||
|
||||
Note:
|
||||
|
||||
- If the solution is a multidimensional array, then all arrays that are not the outermost array DO require the brackets. For example, an array of arrays can be submitted as one of the following:
|
||||
- `[1,2],[3,4]`
|
||||
- `[[1,2],[3,4]]`
|
||||
- The empty string is converted to an empty array.
|
||||
- `"[]"` (the string that contains only 2 bracket characters; the double quotes are not part of that string) is converted to an empty array.
|
||||
|
||||
Numeric solutions should be submitted normally, as expected
|
||||
Second, in the UI:
|
||||
|
||||
- If your answer is an empty string, you must leave the text box empty. Do NOT use `""`, `''`, or \`\`.
|
||||
- If the answer is a non-empty string, type it as is. For example, if your answer is the word `foo`, type `foo` (3 characters: f, o, o). Do NOT add any types of quotes.
|
||||
- If the answer is an array that contains strings, use double quotes for strings. Do NOT use single quotes or backticks. For example, if your answer is an array containing the word `foo`, type `["foo"]` (7 characters: square bracket, double quote, f, o, o, double quote, square bracket). The brackets are optional, as stated above, but we recommend including them.
|
||||
|
||||
### Tips
|
||||
|
||||
If a contract does not expect a string, you should not submit a string. For contracts that do not expect a string
|
||||
solution, your answer should never be a string, so if you submit a string, it means that you converted your non-string
|
||||
answer to a string. This is usually the wrong thing to do.
|
||||
|
||||
Remember, string conversion is for UI convenience and backward compatibility. If you use NS APIs, do not perform any
|
||||
string conversion unless specifically asked for.
|
||||
|
||||
For example, suppose a contract requires the answer to be an array containing strings, and you determine that those
|
||||
strings are `foo` and `bar`. Your code should look like this:
|
||||
|
||||
```js
|
||||
const firstString = "foo";
|
||||
const secondString = "bar";
|
||||
const answer = [firstString, secondString];
|
||||
ns.codingcontract.attempt(answer, "filename.cct");
|
||||
```
|
||||
|
||||
There is no conversion!
|
||||
|
||||
In the "General rules" section above, with array-of-strings solutions, we say `Don't use single quotes or backticks`.
|
||||
However, this code works:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```js
|
||||
const firstString = 'foo'; // Single quotes
|
||||
const secondString = 'bar'; // Single quotes
|
||||
const answer = [firstString, secondString];
|
||||
ns.codingcontract.attempt(answer, "filename.cct");
|
||||
```
|
||||
|
||||
Why is that?
|
||||
|
||||
In this code, you submit an array containing 2 strings. In JS, `"foo"` and `'foo'` are the same string. However, if you
|
||||
submit your answer as a string, you need to convert your array to a string, and the string `["foo", "bar"]` is not the
|
||||
same as the string `['foo', 'bar']`.
|
||||
|
||||
Internally, we use `JSON.parse` to convert the string answer, and `['foo', 'bar']` is not a valid string representation
|
||||
of an array. In JSON, a string needs to be enclosed by double quotes. Using single quotes or backticks is not allowed.
|
||||
|
||||
This is another example of why you should not convert your answer to a string when not requested. If you submit your
|
||||
array as it is, you do not need to care about the quote types.
|
||||
|
||||
## Rewards
|
||||
|
||||
|
||||
@@ -217,7 +217,11 @@ export function TerminalInput(): React.ReactElement {
|
||||
const ref = terminalInput.current;
|
||||
if (event.ctrlKey || event.metaKey) return;
|
||||
if (event.key === KEY.C && (event.ctrlKey || event.metaKey)) return; // trying to copy
|
||||
|
||||
// Don't steal focus from other input elements (e.g., prompt dialogs)
|
||||
const target = event.target;
|
||||
if ((target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) && target !== ref) {
|
||||
return;
|
||||
}
|
||||
if (ref) ref.focus();
|
||||
}
|
||||
document.addEventListener("keydown", keyDown);
|
||||
|
||||
24
src/db.ts
24
src/db.ts
@@ -1,5 +1,12 @@
|
||||
import type { SaveData } from "./types";
|
||||
|
||||
export class IndexedDBVersionError extends Error {
|
||||
constructor(message: string, options: ErrorOptions) {
|
||||
super(message, options);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
function getDB(): Promise<IDBObjectStore> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!window.indexedDB) {
|
||||
@@ -9,18 +16,29 @@ function getDB(): Promise<IDBObjectStore> {
|
||||
* DB is called bitburnerSave
|
||||
* Object store is called savestring
|
||||
* key for the Object store is called save
|
||||
* Version `1` is important
|
||||
* Version `2` is important. When increasing the version, remember to update the code in electron/export.html.
|
||||
*
|
||||
* Version 1 is the initial version. We found a bug that caused the database to be missing the expected object
|
||||
* store. In order to add the missing object store, we need to either increase the database version or delete and
|
||||
* recreate the database. Increasing the version number is simpler. For more information, please check
|
||||
* https://github.com/bitburner-official/bitburner-src/pull/2590
|
||||
*/
|
||||
const indexedDbRequest: IDBOpenDBRequest = window.indexedDB.open("bitburnerSave", 1);
|
||||
const indexedDbRequest: IDBOpenDBRequest = window.indexedDB.open("bitburnerSave", 2);
|
||||
|
||||
// This is called when there's no db to begin with. It's important, don't remove it.
|
||||
indexedDbRequest.onupgradeneeded = function (this: IDBRequest<IDBDatabase>) {
|
||||
const db = this.result;
|
||||
if (db.objectStoreNames.contains("savestring")) {
|
||||
return;
|
||||
}
|
||||
db.createObjectStore("savestring");
|
||||
};
|
||||
|
||||
indexedDbRequest.onerror = function (this: IDBRequest<IDBDatabase>) {
|
||||
reject(new Error("Failed to get IDB", { cause: this.error }));
|
||||
if (this.error?.name === "VersionError") {
|
||||
reject(new IndexedDBVersionError(this.error.message, { cause: this.error }));
|
||||
}
|
||||
reject(this.error ?? new Error("Failed to get IDB"));
|
||||
};
|
||||
|
||||
indexedDbRequest.onsuccess = function (this: IDBRequest<IDBDatabase>) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect } from "react";
|
||||
|
||||
import { Typography, Link, Button, ButtonGroup, Tooltip, Box, Paper, TextField } from "@mui/material";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { load } from "../../db";
|
||||
import { IndexedDBVersionError, load } from "../../db";
|
||||
import { Router } from "../GameRoot";
|
||||
import { Page } from "../Router";
|
||||
import { type CrashReport, newIssueUrl, getCrashReport, isSaveDataFromNewerVersions } from "../../utils/ErrorHelper";
|
||||
@@ -112,14 +112,18 @@ export function RecoveryRoot({ softReset, crashReport, resetError }: IProps): Re
|
||||
</Typography>
|
||||
);
|
||||
} else if (
|
||||
sourceError instanceof JSONReviverError &&
|
||||
isSaveDataFromNewerVersions(loadedSaveObjectMiniDump.VersionSave)
|
||||
(sourceError instanceof JSONReviverError && isSaveDataFromNewerVersions(loadedSaveObjectMiniDump.VersionSave)) ||
|
||||
sourceError instanceof IndexedDBVersionError
|
||||
) {
|
||||
instructions = (
|
||||
<Typography variant="h5" color={Settings.theme.warning}>
|
||||
Your save data is from a newer version (Version number: {loadedSaveObjectMiniDump.VersionSave}). The current
|
||||
version number is {CONSTANTS.VersionNumber}.
|
||||
<br />
|
||||
{loadedSaveObjectMiniDump.VersionSave !== undefined && (
|
||||
<>
|
||||
Your save data is from a newer version (Version number: {loadedSaveObjectMiniDump.VersionSave}). The current
|
||||
version number is {CONSTANTS.VersionNumber}.
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
Please check if you are using the correct build. This may happen when you load the save data of the dev build
|
||||
(Steam Beta or https://bitburner-official.github.io/bitburner-src) on the stable build.
|
||||
</Typography>
|
||||
|
||||
@@ -582,5 +582,47 @@ export const breakingChanges300: VersionBreakingChange = {
|
||||
"They now use the hostname as provided.",
|
||||
showWarning: false,
|
||||
},
|
||||
{
|
||||
brokenAPIs: [
|
||||
{ name: "ns.codingcontract.attempt" },
|
||||
|
||||
{ name: "FindAllValidMathExpressions" },
|
||||
{ name: "GenerateIPAddresses" },
|
||||
{ name: "LargestRectangleInAMatrix" },
|
||||
{ name: "MergeOverlappingIntervals" },
|
||||
{ name: "Proper2ColoringOfAGraph" },
|
||||
{ name: "SanitizeParenthesesInExpression" },
|
||||
{ name: "SpiralizeMatrix" },
|
||||
|
||||
{ name: "Find All Valid Math Expressions" },
|
||||
{ name: "Generate IP Addresses" },
|
||||
{ name: "Largest Rectangle in a Matrix" },
|
||||
{ name: "Merge Overlapping Intervals" },
|
||||
{ name: "Proper 2-Coloring of a Graph" },
|
||||
{ name: "Sanitize Parentheses in Expression" },
|
||||
{ name: "Spiralize Matrix" },
|
||||
],
|
||||
info:
|
||||
"If you pass a string to ns.codingcontract.attempt() for contracts that require a non-string answer, the\n" +
|
||||
"game will convert that string to the expected format. This string conversion was inconsistent and had many\n" +
|
||||
`undocumented behaviors. Now the rules are consistent and well-documented. Please check the "Coding Contracts"\n` +
|
||||
"page for more information.\n" +
|
||||
"There are 7 contracts that are affected by this change:\n" +
|
||||
"- Find All Valid Math Expressions\n" +
|
||||
"- Generate IP Addresses\n" +
|
||||
"- Largest Rectangle in a Matrix\n" +
|
||||
"- Merge Overlapping Intervals\n" +
|
||||
"- Proper 2-Coloring of a Graph\n" +
|
||||
"- Sanitize Parentheses in Expression\n" +
|
||||
"- Spiralize Matrix\n" +
|
||||
"Note that this change only affects the string conversion. The solution format of these contracts is an array,\n" +
|
||||
"so if you pass the solution, which is an array, as is, you will not have any problems.\n" +
|
||||
`If your code converts the array to a string, you should check these contracts, especially the "Sanitize\n` +
|
||||
`Parentheses in Expression" contract and others that require a string array.\n` +
|
||||
`- Sanitize Parentheses in Expression: Previously, if you passed an empty string to this contract, it was\n` +
|
||||
"converted to an array containing an empty string. Now, it's converted to an empty array.\n" +
|
||||
`- Read the "General rules", "String conversion", and "Tips" sections on the "Coding Contracts" page carefully.`,
|
||||
showWarning: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -629,6 +629,8 @@ Error: ${e}`,
|
||||
person.persistentIntelligenceData.exp = person.exp.intelligence;
|
||||
person.overrideIntelligence();
|
||||
}
|
||||
}
|
||||
if (ver < 48) {
|
||||
showAPIBreaks("3.0.0", breakingChanges300);
|
||||
}
|
||||
}
|
||||
|
||||
173
test/jest/CodingContract/AnswerConversion.test.ts
Normal file
173
test/jest/CodingContract/AnswerConversion.test.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { CodingContractTypes, parseArrayString } from "../../../src/CodingContract/ContractTypes";
|
||||
import { CodingContractName } from "../../../src/Enums";
|
||||
import { getRecordEntries } from "../../../src/Types/Record";
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, "error").mockImplementation(jest.fn());
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("Utility functions", () => {
|
||||
test("parseArrayString", () => {
|
||||
expect(parseArrayString("")).toStrictEqual([]);
|
||||
expect(parseArrayString("[]")).toStrictEqual([]);
|
||||
|
||||
expect(parseArrayString(`""`)).toStrictEqual([""]);
|
||||
expect(parseArrayString(`[""]`)).toStrictEqual([""]);
|
||||
expect(parseArrayString(`"foo"`)).toStrictEqual(["foo"]);
|
||||
expect(parseArrayString(`["foo"]`)).toStrictEqual(["foo"]);
|
||||
|
||||
expect(parseArrayString("0")).toStrictEqual([0]);
|
||||
expect(parseArrayString("0,1")).toStrictEqual([0, 1]);
|
||||
expect(parseArrayString("[0,1]")).toStrictEqual([0, 1]);
|
||||
|
||||
expect(parseArrayString(`[[]]`, true)).toStrictEqual([[]]);
|
||||
expect(parseArrayString(`[[0]]`, true)).toStrictEqual([[0]]);
|
||||
expect(parseArrayString(`[[0,1],[2,3]]`, true)).toStrictEqual([
|
||||
[0, 1],
|
||||
[2, 3],
|
||||
]);
|
||||
|
||||
// Incorrectly wrapped as documented
|
||||
expect(parseArrayString(`[ [0]]`, true)).toStrictEqual([[[0]]]);
|
||||
// Preprocessing redundant whitespace
|
||||
expect(parseArrayString(`[ [0]]`.replace(/\s/g, ""), true)).toStrictEqual([[0]]);
|
||||
// Return as-is as documented
|
||||
expect(parseArrayString(`[[1,2],3]`, true)).toStrictEqual([[1, 2], 3]);
|
||||
|
||||
expect(parseArrayString("[")).toStrictEqual(null);
|
||||
expect(parseArrayString("]")).toStrictEqual(null);
|
||||
expect(parseArrayString("foo")).toStrictEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Array", () => {
|
||||
for (const [name, cct] of getRecordEntries(CodingContractTypes)) {
|
||||
if (
|
||||
![
|
||||
CodingContractName.FindAllValidMathExpressions,
|
||||
CodingContractName.GenerateIPAddresses,
|
||||
CodingContractName.MergeOverlappingIntervals,
|
||||
CodingContractName.Proper2ColoringOfAGraph,
|
||||
CodingContractName.SanitizeParenthesesInExpression,
|
||||
CodingContractName.SpiralizeMatrix,
|
||||
CodingContractName.LargestRectangleInAMatrix,
|
||||
].includes(name)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
test(name, () => {
|
||||
expect(cct.convertAnswer("[")).toStrictEqual(null);
|
||||
expect(cct.convertAnswer("]")).toStrictEqual(null);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("String array", () => {
|
||||
for (const [name, cct] of getRecordEntries(CodingContractTypes)) {
|
||||
if (
|
||||
![
|
||||
CodingContractName.FindAllValidMathExpressions,
|
||||
CodingContractName.GenerateIPAddresses,
|
||||
CodingContractName.SanitizeParenthesesInExpression,
|
||||
].includes(name)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
test(name, () => {
|
||||
expect(cct.convertAnswer("")).toStrictEqual([]);
|
||||
expect(cct.convertAnswer("[]")).toStrictEqual([]);
|
||||
|
||||
expect(cct.convertAnswer(`""`)).toStrictEqual([""]);
|
||||
expect(cct.convertAnswer(`[""]`)).toStrictEqual([""]);
|
||||
expect(cct.convertAnswer(`"",""`)).toStrictEqual(["", ""]);
|
||||
expect(cct.convertAnswer(`["",""]`)).toStrictEqual(["", ""]);
|
||||
expect(cct.convertAnswer(`"foo"`)).toStrictEqual(["foo"]);
|
||||
expect(cct.convertAnswer(`"foo","bar"`)).toStrictEqual(["foo", "bar"]);
|
||||
expect(cct.convertAnswer(` "foo", "bar" `)).toStrictEqual(["foo", "bar"]);
|
||||
|
||||
expect(cct.convertAnswer(`"foo`)).toStrictEqual(null);
|
||||
expect(cct.convertAnswer(`foo"`)).toStrictEqual(null);
|
||||
expect(cct.convertAnswer(`'foo'`)).toStrictEqual(null);
|
||||
expect(cct.convertAnswer(`\`foo\``)).toStrictEqual(null);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Number array", () => {
|
||||
for (const [name, cct] of getRecordEntries(CodingContractTypes)) {
|
||||
if (![CodingContractName.Proper2ColoringOfAGraph, CodingContractName.SpiralizeMatrix].includes(name)) {
|
||||
continue;
|
||||
}
|
||||
test(name, () => {
|
||||
expect(cct.convertAnswer("")).toStrictEqual([]);
|
||||
expect(cct.convertAnswer("[]")).toStrictEqual([]);
|
||||
|
||||
expect(cct.convertAnswer("0")).toStrictEqual([0]);
|
||||
expect(cct.convertAnswer("[0]")).toStrictEqual([0]);
|
||||
expect(cct.convertAnswer("0,1")).toStrictEqual([0, 1]);
|
||||
expect(cct.convertAnswer("[0,1]")).toStrictEqual([0, 1]);
|
||||
expect(cct.convertAnswer("[ 0, 1]")).toStrictEqual([0, 1]);
|
||||
|
||||
// Common issues with parseInt in old implementations of convertAnswer
|
||||
expect(cct.convertAnswer("123abc")).toStrictEqual(null);
|
||||
expect(cct.convertAnswer(`"0"`)).toStrictEqual(null);
|
||||
expect(cct.convertAnswer("null")).toStrictEqual(null);
|
||||
expect(cct.convertAnswer("undefined")).toStrictEqual(null);
|
||||
if (name !== CodingContractName.Proper2ColoringOfAGraph) {
|
||||
expect(cct.convertAnswer("12.34")).toStrictEqual([12.34]);
|
||||
expect(cct.convertAnswer("1e3")).toStrictEqual([1000]);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Array of arrays", () => {
|
||||
test(CodingContractName.MergeOverlappingIntervals, () => {
|
||||
const cct = CodingContractTypes[CodingContractName.MergeOverlappingIntervals];
|
||||
// "" => []. With the current generate() and getAnswer(), both data and answer cannot be empty arrays, but in
|
||||
// theory, if the input is an empty array, the output is also an empty array. The fact that the input cannot be an
|
||||
// empty array is only an implementation detail of the internal functions, so an empty array is still a potentially
|
||||
// correct answer.
|
||||
expect(cct.convertAnswer("")).toStrictEqual([]);
|
||||
// "[]" => []
|
||||
expect(cct.convertAnswer("[]")).toStrictEqual([]);
|
||||
|
||||
expect(cct.convertAnswer("[0,0]")).toStrictEqual([[0, 0]]);
|
||||
expect(cct.convertAnswer("[0, 0]")).toStrictEqual([[0, 0]]);
|
||||
expect(cct.convertAnswer("[[0, 0]]")).toStrictEqual([[0, 0]]);
|
||||
expect(cct.convertAnswer("[ [0, 0]]")).toStrictEqual([[0, 0]]);
|
||||
expect(cct.convertAnswer("[1, 2], [3, 4]")).toStrictEqual([
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
]);
|
||||
expect(cct.convertAnswer("[[1, 2], [3, 4]]")).toStrictEqual([
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
]);
|
||||
expect(cct.convertAnswer("[[]]")).toStrictEqual(null);
|
||||
});
|
||||
test(CodingContractName.LargestRectangleInAMatrix, () => {
|
||||
const cct = CodingContractTypes[CodingContractName.LargestRectangleInAMatrix];
|
||||
expect(cct.convertAnswer("[0,0],[0,1]")).toStrictEqual([
|
||||
[0, 0],
|
||||
[0, 1],
|
||||
]);
|
||||
expect(cct.convertAnswer("[[0,0],[0,1]]")).toStrictEqual([
|
||||
[0, 0],
|
||||
[0, 1],
|
||||
]);
|
||||
expect(cct.convertAnswer("[ [0,0], [0,1]]")).toStrictEqual([
|
||||
[0, 0],
|
||||
[0, 1],
|
||||
]);
|
||||
// "" => [], and an empty array is invalid.
|
||||
expect(cct.convertAnswer("")).toStrictEqual(null);
|
||||
// "[]" => [], and an empty array is invalid.
|
||||
expect(cct.convertAnswer("[]")).toStrictEqual(null);
|
||||
expect(cct.convertAnswer("[0,0]")).toStrictEqual(null);
|
||||
expect(cct.convertAnswer("[[]]")).toStrictEqual(null);
|
||||
});
|
||||
});
|
||||
96
test/jest/Terminal/terminalFocus.test.ts
Normal file
96
test/jest/Terminal/terminalFocus.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Tests for the terminal's document-level keydown handler and its interaction
|
||||
* with other input elements (e.g., prompt dialogs).
|
||||
*
|
||||
* The terminal registers a document-level keydown listener that calls focus()
|
||||
* on the terminal input to redirect all keyboard input there. This test
|
||||
* verifies that the handler does NOT steal focus from other input elements
|
||||
* like the text prompt dialog.
|
||||
*
|
||||
* See: https://github.com/bitburner-official/bitburner-src/issues/924
|
||||
*/
|
||||
|
||||
describe("Terminal focus behavior", () => {
|
||||
let terminalInput: HTMLInputElement;
|
||||
let promptInput: HTMLInputElement;
|
||||
|
||||
beforeEach(() => {
|
||||
terminalInput = document.createElement("input");
|
||||
terminalInput.id = "terminal-input";
|
||||
promptInput = document.createElement("input");
|
||||
promptInput.id = "prompt-input";
|
||||
document.body.append(terminalInput, promptInput);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
terminalInput.remove();
|
||||
promptInput.remove();
|
||||
});
|
||||
|
||||
// Simulates the terminal's keydown handler with the fix applied:
|
||||
// skip focus redirect if the event target is an input/textarea that isn't the terminal
|
||||
function fixedKeyDown(event: KeyboardEvent) {
|
||||
if (event.ctrlKey || event.metaKey) return;
|
||||
const target = event.target;
|
||||
if ((target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) && target !== terminalInput) {
|
||||
return;
|
||||
}
|
||||
terminalInput.focus();
|
||||
}
|
||||
|
||||
it("should not steal focus from prompt input on keydown", () => {
|
||||
document.addEventListener("keydown", fixedKeyDown);
|
||||
|
||||
// User opens a prompt dialog and the text field gets focus
|
||||
promptInput.focus();
|
||||
expect(document.activeElement).toBe(promptInput);
|
||||
|
||||
// User types in the prompt — focus should stay there
|
||||
promptInput.dispatchEvent(new KeyboardEvent("keydown", { key: "a", bubbles: true }));
|
||||
expect(document.activeElement).toBe(promptInput);
|
||||
|
||||
// Multiple keystrokes should all stay in the prompt
|
||||
promptInput.dispatchEvent(new KeyboardEvent("keydown", { key: "b", bubbles: true }));
|
||||
promptInput.dispatchEvent(new KeyboardEvent("keydown", { key: "c", bubbles: true }));
|
||||
expect(document.activeElement).toBe(promptInput);
|
||||
|
||||
document.removeEventListener("keydown", fixedKeyDown);
|
||||
});
|
||||
|
||||
it("should still redirect focus to terminal when typing on the page background", () => {
|
||||
document.addEventListener("keydown", fixedKeyDown);
|
||||
|
||||
// No input has focus — user clicks on empty page area, then types
|
||||
document.body.focus();
|
||||
expect(document.activeElement).toBe(document.body);
|
||||
|
||||
// Keydown from body should redirect to terminal
|
||||
document.body.dispatchEvent(new KeyboardEvent("keydown", { key: "a", bubbles: true }));
|
||||
expect(document.activeElement).toBe(terminalInput);
|
||||
|
||||
document.removeEventListener("keydown", fixedKeyDown);
|
||||
});
|
||||
|
||||
it("should still redirect focus when the terminal input itself has focus", () => {
|
||||
document.addEventListener("keydown", fixedKeyDown);
|
||||
|
||||
terminalInput.focus();
|
||||
terminalInput.dispatchEvent(new KeyboardEvent("keydown", { key: "a", bubbles: true }));
|
||||
expect(document.activeElement).toBe(terminalInput);
|
||||
|
||||
document.removeEventListener("keydown", fixedKeyDown);
|
||||
});
|
||||
|
||||
it("should not steal focus from textarea elements either", () => {
|
||||
const textarea = document.createElement("textarea");
|
||||
document.body.appendChild(textarea);
|
||||
document.addEventListener("keydown", fixedKeyDown);
|
||||
|
||||
textarea.focus();
|
||||
textarea.dispatchEvent(new KeyboardEvent("keydown", { key: "x", bubbles: true }));
|
||||
expect(document.activeElement).toBe(textarea);
|
||||
|
||||
document.removeEventListener("keydown", fixedKeyDown);
|
||||
textarea.remove();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user