CODEBASE: Validate AllGangs and StockMarket after loading with JSON.parse (#1783)

This commit is contained in:
catloversg 2024-11-27 16:13:47 +07:00 committed by GitHub
parent 7d03a9ef32
commit faed78cf2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 550 additions and 170 deletions

269
package-lock.json generated

@ -24,6 +24,7 @@
"acorn": "^8.11.3",
"acorn-jsx-walk": "^2.0.0",
"acorn-walk": "^8.3.2",
"ajv": "^8.17.1",
"arg": "^5.0.2",
"bcryptjs": "^2.4.3",
"better-react-mathjax": "^2.0.3",
@ -2348,6 +2349,23 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/eslintrc/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@eslint/eslintrc/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@ -2381,6 +2399,13 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/@eslint/eslintrc/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@ -3525,6 +3550,30 @@
"resolve": "~1.19.0"
}
},
"node_modules/@microsoft/api-extractor-model/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@microsoft/api-extractor-model/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/@microsoft/api-extractor-model/node_modules/resolve": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
@ -3665,12 +3714,6 @@
"node": ">=8"
}
},
"node_modules/@microsoft/api-extractor/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"node_modules/@microsoft/api-extractor/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@ -3780,12 +3823,6 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@microsoft/tsdoc-config/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"node_modules/@mui/base": {
"version": "5.0.0-beta.18",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.18.tgz",
@ -5772,15 +5809,15 @@
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
@ -5804,37 +5841,6 @@
}
}
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@ -6185,22 +6191,6 @@
"webpack": ">=5"
}
},
"node_modules/babel-loader/node_modules/ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/babel-loader/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
@ -6213,12 +6203,6 @@
"ajv": "^8.8.2"
}
},
"node_modules/babel-loader/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"node_modules/babel-loader/node_modules/schema-utils": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
@ -7153,22 +7137,6 @@
"webpack": "^5.1.0"
}
},
"node_modules/copy-webpack-plugin/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/copy-webpack-plugin/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
@ -7201,12 +7169,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"node_modules/copy-webpack-plugin/node_modules/path-type": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz",
@ -8566,6 +8528,23 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/eslint/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -8673,6 +8652,13 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/eslint/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/eslint/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -8970,8 +8956,7 @@
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-glob": {
"version": "3.3.2",
@ -9016,8 +9001,7 @@
"node_modules/fast-uri": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz",
"integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==",
"dev": true
"integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw=="
},
"node_modules/fastest-levenshtein": {
"version": "1.0.16",
@ -12964,10 +12948,10 @@
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
@ -16216,7 +16200,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -16465,6 +16448,40 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/schema-utils/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/schema-utils/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/schema-utils/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@ -18213,22 +18230,6 @@
"webpack": "^4.0.0 || ^5.0.0"
}
},
"node_modules/webpack-dev-middleware/node_modules/ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/webpack-dev-middleware/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
@ -18241,12 +18242,6 @@
"ajv": "^8.8.2"
}
},
"node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"node_modules/webpack-dev-middleware/node_modules/schema-utils": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
@ -18325,22 +18320,6 @@
}
}
},
"node_modules/webpack-dev-server/node_modules/ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/webpack-dev-server/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
@ -18353,12 +18332,6 @@
"ajv": "^8.8.2"
}
},
"node_modules/webpack-dev-server/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"node_modules/webpack-dev-server/node_modules/schema-utils": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",

@ -24,6 +24,7 @@
"acorn": "^8.11.3",
"acorn-jsx-walk": "^2.0.0",
"acorn-walk": "^8.3.2",
"ajv": "^8.17.1",
"arg": "^5.0.2",
"bcryptjs": "^2.4.3",
"better-react-mathjax": "^2.0.3",

@ -1,12 +1,14 @@
import { FactionName } from "@enums";
import { Reviver } from "../utils/GenericReviver";
import { JsonSchemaValidator } from "../JsonSchema/JsonSchemaValidator";
import { dialogBoxCreate } from "../ui/React/DialogBox";
interface GangTerritory {
power: number;
territory: number;
}
function getDefaultAllGangs() {
export function getDefaultAllGangs() {
return {
[FactionName.SlumSnakes]: {
power: 1,
@ -46,7 +48,26 @@ export function resetGangs(): void {
}
export function loadAllGangs(saveString: string): void {
AllGangs = JSON.parse(saveString, Reviver);
let allGangsData: unknown;
let validate;
try {
allGangsData = JSON.parse(saveString, Reviver);
validate = JsonSchemaValidator.AllGangs;
if (!validate(allGangsData)) {
console.error("validate.errors:", validate.errors);
// validate.errors is an array of objects, so we need to use JSON.stringify.
throw new Error(JSON.stringify(validate.errors));
}
} catch (error) {
console.error(error);
console.error("Invalid AllGangsSave:", saveString);
resetGangs();
setTimeout(() => {
dialogBoxCreate(`Cannot load data of AllGangs. AllGangs is reset. Error: ${error}.`);
}, 1000);
return;
}
AllGangs = allGangsData;
}
export function getClashWinChance(thisGang: string, otherGang: string): number {

@ -0,0 +1,35 @@
import type { JSONSchemaType } from "ajv";
import type { AllGangs } from "../../Gang/AllGangs";
import { GangConstants } from "../../Gang/data/Constants";
/**
* If we add/remove gangs, we must change 4 things:
* - src\Gang\AllGangs.ts: getDefaultAllGangs
* - src\Gang\data\Constants.ts: GangConstants.Names
* - src\Gang\data\power.ts: PowerMultiplier
* - Save file migration code.
*
* Gang code assumes that save data contains exactly gangs defined in these places.
*/
export const AllGangsSchema: JSONSchemaType<typeof AllGangs> = {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
patternProperties: {
".*": {
type: "object",
properties: {
power: {
type: "number",
},
territory: {
type: "number",
},
},
required: ["power", "territory"],
},
},
propertyNames: {
enum: GangConstants.Names,
},
required: GangConstants.Names,
};

@ -0,0 +1,141 @@
import { OrderType, PositionType } from "@enums";
import { getKeyList } from "../../utils/helpers/getKeyList";
import { Stock } from "../../StockMarket/Stock";
import { Order } from "../../StockMarket/Order";
const stockObjectProperties = getKeyList(Stock);
const orderObjectProperties = getKeyList(Order);
/**
* It's intentional to not use JSONSchemaType here. The data structure of StockMarket is not suitable for the usage of
* JSONSchemaType. These are 2 biggest problems:
* - IStockMarket is an intersection type. In our case, satisfying TS's type-checking for JSONSchemaType is too hard.
* - Stock and Order are classes, not interfaces or types. In those classes, we have functions, but functions are not
* supported by JSON (without ugly hacks). Let's use the "Order" class as an example. The Order class has the toJSON
* function, so ajv forces us to define the "toJSON" property. If we define it, we have to give it a "type". In
* JavaScript, a function is just an object, so the naive solution is to use {type:"object"}. However, the generated
* validation code uses "typeof" to check the data. "typeof functionName" is "function", not "object", so ajv sees it as
* invalid data. There are some ways to work around this problem, but all of them are complicated. In the end,
* JSONSchemaType is only a utility type. If our schema is designed and tested properly, after the validation, we can
* typecast the data.
*/
export const StockMarketSchema = {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
patternProperties: {
/**
* IStockMarket is an intersection type combining:
* - Record<string, Stock> [1]
* - {lastUpdate: number;Orders: IOrderBook;storedCycles: number;ticksUntilCycle: number;} [2]
*
* StockMarketSchema contains:
* - patternProperties: Defines [1]. The following regex matches all properties that are not in [2]. It defines the
* map of "Full stock name -> Stock".
* - properties: Define [2].
*
* Note that with [1], our code allows unknown stocks. Let's say the player loads a save file with this entry in
* [1]: UnknownCorp123 -> Stock with symbol UCP123. Although this stock is not in our list of "valid" stocks, we
* still process it normally. By "tolerating" unknown stocks, we allow loading a save file created in:
* - Old versions with unsupported stocks: In very old versions (v1.2.0 and older ones), the "full stock name" of
* "Joe's Guns" is "Joes Guns".
* - New versions with unknown stocks.
*/
"^(?!(lastUpdate|Orders|storedCycles|ticksUntilCycle))": {
type: "object",
properties: {
b: {
type: "boolean",
},
cap: {
type: "number",
},
lastPrice: {
type: "number",
},
maxShares: {
type: "number",
},
mv: {
type: "number",
},
name: {
type: "string",
},
otlkMag: {
type: "number",
},
otlkMagForecast: {
type: "number",
},
playerAvgPx: {
type: "number",
},
playerAvgShortPx: {
type: "number",
},
playerShares: {
type: "number",
},
playerShortShares: {
type: "number",
},
price: {
type: "number",
},
shareTxForMovement: {
type: "number",
},
shareTxUntilMovement: {
type: "number",
},
spreadPerc: {
type: "number",
},
symbol: {
type: "string",
},
totalShares: {
type: "number",
},
},
required: [...stockObjectProperties],
},
},
properties: {
lastUpdate: { type: "number" },
Orders: {
type: "object",
patternProperties: {
".*": {
type: "array",
items: {
type: "object",
properties: {
pos: {
type: "string",
enum: [PositionType.Long, PositionType.Short],
},
price: {
type: "number",
},
shares: {
type: "number",
},
stockSymbol: {
type: "string",
},
type: {
type: "string",
enum: [OrderType.LimitBuy, OrderType.LimitSell, OrderType.StopBuy, OrderType.StopSell],
},
},
required: [...orderObjectProperties],
},
},
},
},
storedCycles: { type: "number" },
ticksUntilCycle: { type: "number" },
},
required: ["lastUpdate", "Orders", "storedCycles", "ticksUntilCycle"],
};

@ -0,0 +1,10 @@
import Ajv from "ajv";
import { AllGangsSchema } from "./Data/AllGangsSchema";
import { StockMarketSchema } from "./Data/StockMarketSchema";
const ajv = new Ajv();
export const JsonSchemaValidator = {
AllGangs: ajv.compile(AllGangsSchema),
StockMarket: ajv.compile(StockMarketSchema),
};

@ -16,13 +16,19 @@ import { Reviver } from "../utils/GenericReviver";
import { NetscriptContext } from "../Netscript/APIWrapper";
import { helpers } from "../Netscript/NetscriptHelpers";
import { getRandomIntInclusive } from "../utils/helpers/getRandomIntInclusive";
import { JsonSchemaValidator } from "../JsonSchema/JsonSchemaValidator";
import { Player } from "../Player";
export let StockMarket: IStockMarket = {
export function getDefaultEmptyStockMarket(): IStockMarket {
return {
lastUpdate: 0,
Orders: {},
storedCycles: 0,
ticksUntilCycle: 0,
} as IStockMarket; // Maps full stock name -> Stock object
} as IStockMarket;
}
export let StockMarket = getDefaultEmptyStockMarket(); // Maps full stock name -> Stock object
// Gross type, needs to be addressed
export const SymbolToStockMap: Record<string, Stock> = {}; // Maps symbol -> Stock object
@ -130,23 +136,35 @@ export function cancelOrder(params: ICancelOrderParams, ctx?: NetscriptContext):
}
export function loadStockMarket(saveString: string): void {
if (saveString === "") {
StockMarket = {
lastUpdate: 0,
Orders: {},
storedCycles: 0,
ticksUntilCycle: 0,
} as IStockMarket;
} else StockMarket = JSON.parse(saveString, Reviver);
let stockMarketData: unknown;
let validate;
try {
stockMarketData = JSON.parse(saveString, Reviver);
validate = JsonSchemaValidator.StockMarket;
if (!validate(stockMarketData)) {
console.error("validate.errors:", validate.errors);
// validate.errors is an array of objects, so we need to use JSON.stringify.
throw new Error(JSON.stringify(validate.errors));
}
} catch (error) {
console.error(error);
console.error("Invalid StockMarketSave:", saveString);
deleteStockMarket();
if (Player.hasWseAccount) {
initStockMarket();
}
const errorMessage = `Cannot load data of StockMarket. StockMarket is reset.`;
setTimeout(() => {
dialogBoxCreate(errorMessage);
}, 1000);
return;
}
// Typecasting here is fine because we validated the loaded data.
StockMarket = stockMarketData as IStockMarket;
}
export function deleteStockMarket(): void {
StockMarket = {
lastUpdate: 0,
Orders: {},
storedCycles: 0,
ticksUntilCycle: 0,
} as IStockMarket;
StockMarket = getDefaultEmptyStockMarket();
}
export function initStockMarket(): void {

@ -0,0 +1,41 @@
import { getDefaultAllGangs } from "../../../src/Gang/AllGangs";
import { JsonSchemaValidator } from "../../../src/JsonSchema/JsonSchemaValidator";
describe("Success", () => {
test("Default AllGangs", () => {
const defaultAllGangs = getDefaultAllGangs();
expect(JsonSchemaValidator.AllGangs(defaultAllGangs)).toStrictEqual(true);
});
});
describe("Failure", () => {
test("Do not have all gangs", () => {
const defaultAllGangs = getDefaultAllGangs() as Record<string, unknown>;
delete defaultAllGangs["Slum Snakes"];
expect(JsonSchemaValidator.AllGangs(defaultAllGangs)).toStrictEqual(false);
});
test("Have an unexpected gang", () => {
const defaultAllGangs = getDefaultAllGangs() as Record<string, unknown>;
defaultAllGangs["CyberSec"] = {
power: 1,
territory: 1 / 7,
};
expect(JsonSchemaValidator.AllGangs(defaultAllGangs)).toStrictEqual(false);
});
test("Have invalid power", () => {
const defaultAllGangs = getDefaultAllGangs() as Record<string, unknown>;
defaultAllGangs["Slum Snakes"] = {
power: "1",
territory: 1 / 7,
};
expect(JsonSchemaValidator.AllGangs(defaultAllGangs)).toStrictEqual(false);
});
test("Have invalid territory", () => {
const defaultAllGangs = getDefaultAllGangs() as Record<string, unknown>;
defaultAllGangs["Slum Snakes"] = {
power: 1,
territory: "1 / 7",
};
expect(JsonSchemaValidator.AllGangs(defaultAllGangs)).toStrictEqual(false);
});
});

@ -0,0 +1,140 @@
import { JsonSchemaValidator } from "../../../src/JsonSchema/JsonSchemaValidator";
import {
deleteStockMarket,
getDefaultEmptyStockMarket,
initStockMarket,
StockMarket,
} from "../../../src/StockMarket/StockMarket";
deleteStockMarket();
initStockMarket();
// Get the clone of StockMarket immediately after calling deleteStockMarket() and initStockMarket().
const defaultStockMarket = structuredClone(StockMarket);
/**
* We must call this function to get the data for testing. Do not use the module-scoped "StockMarket" from
* src/StockMarket/StockMarket. Jest tests run in parallel, so our tests may mutate that variable and result in wrong
* tests.
*/
function getCloneOfDefaultStockMarket(): Record<string, unknown> {
return structuredClone(defaultStockMarket);
}
function getCloneOfSampleStock() {
return {
name: "ECorp",
symbol: "ECP",
price: 24618.907733048673,
lastPrice: 24702.20549043557,
playerShares: 2,
playerAvgPx: 19893.251484918932,
playerShortShares: 0,
playerAvgShortPx: 0,
mv: 0.44,
b: true,
otlkMag: 17.783525574804138,
otlkMagForecast: 68.11871382128285,
cap: 148683699,
spreadPerc: 0.5,
shareTxForMovement: 76967,
shareTxUntilMovement: 76967,
totalShares: 140600000,
maxShares: 28100000,
};
}
function getCloneOfSampleOrders() {
return {
ECP: [
{
stockSymbol: "ECP",
shares: 1,
price: 1000,
type: "Limit Buy Order",
pos: "L",
},
],
};
}
type SampleOrders = ReturnType<typeof getCloneOfSampleOrders>;
describe("Success", () => {
test("Default empty StockMarket", () => {
expect(JsonSchemaValidator.StockMarket(getDefaultEmptyStockMarket())).toStrictEqual(true);
});
test("Default StockMarket", () => {
expect(JsonSchemaValidator.StockMarket(getCloneOfDefaultStockMarket())).toStrictEqual(true);
});
test("StockMarket with Orders", () => {
const stockMarket = getCloneOfDefaultStockMarket();
stockMarket.Orders = getCloneOfSampleOrders();
expect(JsonSchemaValidator.StockMarket(stockMarket)).toStrictEqual(true);
});
});
describe("Failure", () => {
test("Have unexpected property", () => {
const stockMarket = getCloneOfDefaultStockMarket();
stockMarket.test = "";
expect(JsonSchemaValidator.StockMarket(stockMarket)).toStrictEqual(false);
});
test("Do not have lastUpdate property", () => {
const stockMarket = getCloneOfDefaultStockMarket();
delete stockMarket.lastUpdate;
expect(JsonSchemaValidator.StockMarket(stockMarket)).toStrictEqual(false);
});
describe("Invalid stock", () => {
test("Have invalid type of stock", () => {
const stockMarket = getCloneOfDefaultStockMarket();
stockMarket.ECorp = [];
expect(JsonSchemaValidator.StockMarket(stockMarket)).toStrictEqual(false);
});
const sampleStock = getCloneOfSampleStock();
for (const [key, value] of Object.entries(sampleStock)) {
if (typeof value === "string") {
continue;
}
test(`Have invalid ${key}`, () => {
const stockMarket = getCloneOfDefaultStockMarket();
stockMarket.ECorp = getCloneOfSampleStock();
(stockMarket.ECorp as { [_: string]: unknown })[key] = "test";
expect(JsonSchemaValidator.StockMarket(stockMarket)).toStrictEqual(false);
});
}
});
describe("Invalid Orders", () => {
test("Have invalid type of Orders", () => {
const stockMarket = getCloneOfDefaultStockMarket();
stockMarket.Orders = [];
expect(JsonSchemaValidator.StockMarket(stockMarket)).toStrictEqual(false);
});
test("Have invalid Order: Invalid order type", () => {
const stockMarket = getCloneOfDefaultStockMarket();
stockMarket.Orders = getCloneOfSampleOrders();
(stockMarket.Orders as SampleOrders).ECP[0].type = "Limit Buy Order1";
expect(JsonSchemaValidator.StockMarket(stockMarket)).toStrictEqual(false);
});
test("Have invalid Order: Invalid position type", () => {
const stockMarket = getCloneOfDefaultStockMarket();
stockMarket.Orders = getCloneOfSampleOrders();
(stockMarket.Orders as SampleOrders).ECP[0].pos = "L1";
expect(JsonSchemaValidator.StockMarket(stockMarket)).toStrictEqual(false);
});
test("Have invalid Order: Invalid shares", () => {
const stockMarket = getCloneOfDefaultStockMarket();
stockMarket.Orders = getCloneOfSampleOrders();
((stockMarket.Orders as SampleOrders).ECP[0].shares as unknown) = "1";
expect(JsonSchemaValidator.StockMarket(stockMarket)).toStrictEqual(false);
});
test("Have invalid Order: Invalid price", () => {
const stockMarket = getCloneOfDefaultStockMarket();
stockMarket.Orders = getCloneOfSampleOrders();
((stockMarket.Orders as SampleOrders).ECP[0].price as unknown) = "1000";
expect(JsonSchemaValidator.StockMarket(stockMarket)).toStrictEqual(false);
});
});
});