isaacscript 3.16.0 → 3.16.1-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/checkForWindowsTerminalBugs.js +57 -0
- package/{src → dist}/classes/Config.js +1 -6
- package/dist/classes/ValidatedConfig.js +1 -0
- package/{src → dist}/commands/check/check.js +60 -68
- package/dist/commands/check/check.test.js +86 -0
- package/dist/commands/copy/copy.js +29 -0
- package/dist/commands/init/checkIfProjectPathExists.js +21 -0
- package/dist/commands/init/checkModSubdirectory.js +9 -0
- package/dist/commands/init/checkModTargetDirectory.js +24 -0
- package/dist/commands/init/createProject.js +199 -0
- package/dist/commands/init/getAuthorName.js +22 -0
- package/dist/commands/init/getModsDir.js +48 -0
- package/dist/commands/init/getProjectPath.js +84 -0
- package/dist/commands/init/init.js +73 -0
- package/dist/commands/init/installVSCodeExtensions.js +27 -0
- package/dist/commands/init/promptSaveSlot.js +17 -0
- package/{src → dist}/commands/init/promptVSCode.js +5 -10
- package/dist/commands/monitor/copyWatcherMod.js +31 -0
- package/dist/commands/monitor/modDirectorySyncer/modDirectorySyncer.js +44 -0
- package/dist/commands/monitor/monitor.js +154 -0
- package/{src → dist}/commands/monitor/notifyGame.js +14 -22
- package/{src → dist}/commands/monitor/saveDatWriter/saveDatWriter.js +24 -27
- package/dist/commands/monitor/saveDatWriter/types.js +1 -0
- package/dist/commands/monitor/spawnModDirectorySyncer.js +31 -0
- package/dist/commands/monitor/spawnSaveDatWriter.js +29 -0
- package/dist/commands/monitor/spawnTSTLWatcher.js +74 -0
- package/dist/commands/monitor/touchWatcherSaveDatFiles.js +15 -0
- package/dist/commands/publish/isaacscriptMod.js +90 -0
- package/dist/commands/publish/prePublish.js +105 -0
- package/dist/commands/publish/publish.js +39 -0
- package/dist/commands/publish/validate.js +46 -0
- package/dist/common.js +65 -0
- package/dist/configFile.js +54 -0
- package/dist/constants.js +61 -0
- package/dist/customStage.js +298 -0
- package/dist/dev.js +10 -0
- package/{src/enums → dist/enums/copied}/DoorSlot.js +5 -6
- package/dist/enums/copied/DoorSlotFlag.js +35 -0
- package/{src/enums → dist/enums/copied}/RoomShape.js +5 -6
- package/{src → dist}/exec.js +19 -28
- package/dist/git.js +133 -0
- package/dist/interfaces/GitHubCLIHostsYAML.js +1 -0
- package/dist/interfaces/IsaacScriptTSConfig.js +1 -0
- package/dist/interfaces/ShadersXML.js +1 -0
- package/dist/interfaces/copied/CustomStageTSConfig.js +4 -0
- package/dist/interfaces/copied/JSONRoomsFile.js +4 -0
- package/dist/main.js +132 -0
- package/dist/objects/copied/doorSlotToDoorSlotFlag.js +16 -0
- package/dist/objects/copied/roomShapeToDoorSlotCoordinates.js +112 -0
- package/dist/packageManager.js +68 -0
- package/{src → dist}/parseArgs.js +36 -15
- package/dist/prompt.js +79 -0
- package/dist/tsconfig.js +98 -0
- package/dist/types/Command.js +4 -0
- package/dist/utils.js +8 -0
- package/dist/validateInIsaacScriptProject.js +29 -0
- package/dist/validateNodeVersion.js +20 -0
- package/file-templates/dynamic/.github/workflows/setup/action.yml +21 -0
- package/file-templates/dynamic/README.mod.md +4 -3
- package/file-templates/dynamic/_gitignore +0 -2
- package/file-templates/dynamic/package.mod.json +8 -0
- package/file-templates/dynamic/package.ts.json +5 -1
- package/file-templates/{dynamic → static}/.github/workflows/ci.yml +10 -38
- package/file-templates/static/scripts/tsconfig.json +28 -0
- package/file-templates/static-mod/.vscode/settings.json +1 -1
- package/file-templates/static-mod/{_cspell.json → _cspell.jsonc} +2 -2
- package/file-templates/static-mod/_eslintrc.cjs +1 -1
- package/file-templates/static-mod/prettier.config.mjs +6 -1
- package/file-templates/static-mod/scripts/lint.ts +36 -0
- package/file-templates/static-mod/tsconfig.json +5 -5
- package/file-templates/static-ts/.vscode/settings.json +1 -1
- package/file-templates/static-ts/{_cspell.json → _cspell.jsonc} +5 -5
- package/file-templates/static-ts/_eslintrc.cjs +1 -1
- package/file-templates/static-ts/knip.jsonc +6 -0
- package/file-templates/static-ts/prettier.config.mjs +6 -1
- package/file-templates/static-ts/scripts/lint.ts +30 -0
- package/file-templates/static-ts/tsconfig.json +3 -3
- package/package.json +27 -9
- package/plugins/addCrashDebugStatements.cjs +56 -0
- package/plugins/addCrashDebugStatements.ts +39 -0
- package/{src/plugins/addIsaacScriptCommentHeader.js → plugins/addIsaacScriptCommentHeader.cjs} +1 -1
- package/plugins/addIsaacScriptCommentHeader.ts +42 -0
- package/{src/plugins/addIsaacScriptCommentHeaderCommon.js → plugins/addIsaacScriptCommentHeaderCommon.cjs} +6 -4
- package/plugins/addIsaacScriptCommentHeaderCommon.ts +59 -0
- package/{src/plugins/noExtendedEnums.js → plugins/noExtendedEnums.cjs} +25 -4
- package/plugins/noExtendedEnums.ts +83 -0
- package/plugins/tsconfig.json +28 -0
- package/schemas/tsconfig-isaacscript-section-schema.json +1 -0
- package/file-templates/static/nuke.sh +0 -53
- package/file-templates/static/update.sh +0 -26
- package/file-templates/static-mod/build.sh +0 -22
- package/file-templates/static-mod/lint.sh +0 -43
- package/file-templates/static-mod/publish.sh +0 -10
- package/file-templates/static-mod/run.sh +0 -10
- package/file-templates/static-ts/build.sh +0 -53
- package/file-templates/static-ts/lint.sh +0 -35
- package/file-templates/static-ts/publish.sh +0 -10
- package/file-templates/static-ts/run.sh +0 -42
- package/src/checkForWindowsTerminalBugs.js +0 -71
- package/src/checkForWindowsTerminalBugs.js.map +0 -1
- package/src/classes/Config.js.map +0 -1
- package/src/classes/ValidatedConfig.js +0 -3
- package/src/classes/ValidatedConfig.js.map +0 -1
- package/src/commands/check/check.js.map +0 -1
- package/src/commands/check/check.test.js +0 -87
- package/src/commands/check/check.test.js.map +0 -1
- package/src/commands/copy/copy.js +0 -38
- package/src/commands/copy/copy.js.map +0 -1
- package/src/commands/init/checkIfProjectPathExists.js +0 -28
- package/src/commands/init/checkIfProjectPathExists.js.map +0 -1
- package/src/commands/init/checkModSubdirectory.js +0 -16
- package/src/commands/init/checkModSubdirectory.js.map +0 -1
- package/src/commands/init/checkModTargetDirectory.js +0 -31
- package/src/commands/init/checkModTargetDirectory.js.map +0 -1
- package/src/commands/init/createProject.js +0 -220
- package/src/commands/init/createProject.js.map +0 -1
- package/src/commands/init/getAuthorName.js +0 -27
- package/src/commands/init/getAuthorName.js.map +0 -1
- package/src/commands/init/getModsDir.js +0 -58
- package/src/commands/init/getModsDir.js.map +0 -1
- package/src/commands/init/getProjectPath.js +0 -89
- package/src/commands/init/getProjectPath.js.map +0 -1
- package/src/commands/init/init.js +0 -78
- package/src/commands/init/init.js.map +0 -1
- package/src/commands/init/installVSCodeExtensions.js +0 -35
- package/src/commands/init/installVSCodeExtensions.js.map +0 -1
- package/src/commands/init/promptSaveSlot.js +0 -22
- package/src/commands/init/promptSaveSlot.js.map +0 -1
- package/src/commands/init/promptVSCode.js.map +0 -1
- package/src/commands/monitor/copyWatcherMod.js +0 -39
- package/src/commands/monitor/copyWatcherMod.js.map +0 -1
- package/src/commands/monitor/modDirectorySyncer/modDirectorySyncer.js +0 -47
- package/src/commands/monitor/modDirectorySyncer/modDirectorySyncer.js.map +0 -1
- package/src/commands/monitor/monitor.js +0 -263
- package/src/commands/monitor/monitor.js.map +0 -1
- package/src/commands/monitor/notifyGame.js.map +0 -1
- package/src/commands/monitor/saveDatWriter/saveDatWriter.js.map +0 -1
- package/src/commands/monitor/saveDatWriter/types.js +0 -3
- package/src/commands/monitor/saveDatWriter/types.js.map +0 -1
- package/src/commands/monitor/spawnSaveDatWriter.js +0 -35
- package/src/commands/monitor/spawnSaveDatWriter.js.map +0 -1
- package/src/commands/monitor/touchWatcherSaveDatFiles.js +0 -21
- package/src/commands/monitor/touchWatcherSaveDatFiles.js.map +0 -1
- package/src/commands/publish/isaacscriptMod.js +0 -97
- package/src/commands/publish/isaacscriptMod.js.map +0 -1
- package/src/commands/publish/prePublish.js +0 -120
- package/src/commands/publish/prePublish.js.map +0 -1
- package/src/commands/publish/publish.js +0 -42
- package/src/commands/publish/publish.js.map +0 -1
- package/src/commands/publish/validate.js +0 -58
- package/src/commands/publish/validate.js.map +0 -1
- package/src/common.js +0 -69
- package/src/common.js.map +0 -1
- package/src/configFile.js +0 -64
- package/src/configFile.js.map +0 -1
- package/src/constants.js +0 -67
- package/src/constants.js.map +0 -1
- package/src/customStage.js +0 -304
- package/src/customStage.js.map +0 -1
- package/src/dev.js +0 -19
- package/src/dev.js.map +0 -1
- package/src/enums/DoorSlot.js.map +0 -1
- package/src/enums/DoorSlotFlag.js +0 -37
- package/src/enums/DoorSlotFlag.js.map +0 -1
- package/src/enums/PackageManager.js +0 -10
- package/src/enums/PackageManager.js.map +0 -1
- package/src/enums/RoomShape.js.map +0 -1
- package/src/exec.js.map +0 -1
- package/src/file.js +0 -247
- package/src/file.js.map +0 -1
- package/src/git.js +0 -154
- package/src/git.js.map +0 -1
- package/src/interfaces/GitHubCLIHostsYAML.js +0 -3
- package/src/interfaces/GitHubCLIHostsYAML.js.map +0 -1
- package/src/interfaces/IsaacScriptTSConfig.js +0 -3
- package/src/interfaces/IsaacScriptTSConfig.js.map +0 -1
- package/src/interfaces/ShadersXML.js +0 -3
- package/src/interfaces/ShadersXML.js.map +0 -1
- package/src/interfaces/copied/CustomStageTSConfig.js +0 -4
- package/src/interfaces/copied/CustomStageTSConfig.js.map +0 -1
- package/src/interfaces/copied/JSONRoomsFile.js +0 -4
- package/src/interfaces/copied/JSONRoomsFile.js.map +0 -1
- package/src/isaacScriptCommonTS.js +0 -233
- package/src/isaacScriptCommonTS.js.map +0 -1
- package/src/json.js +0 -42
- package/src/json.js.map +0 -1
- package/src/main.js +0 -127
- package/src/main.js.map +0 -1
- package/src/objects/doorSlotToDoorSlotFlag.js +0 -17
- package/src/objects/doorSlotToDoorSlotFlag.js.map +0 -1
- package/src/objects/roomShapeDoorToSlotCoordinates.js +0 -115
- package/src/objects/roomShapeDoorToSlotCoordinates.js.map +0 -1
- package/src/packageManager.js +0 -153
- package/src/packageManager.js.map +0 -1
- package/src/parseArgs.js.map +0 -1
- package/src/plugins/addCrashDebugStatements.js +0 -32
- package/src/plugins/addCrashDebugStatements.js.map +0 -1
- package/src/plugins/addIsaacScriptCommentHeader.js.map +0 -1
- package/src/plugins/addIsaacScriptCommentHeaderCommon.js.map +0 -1
- package/src/plugins/noExtendedEnums.js.map +0 -1
- package/src/prompt.js +0 -87
- package/src/prompt.js.map +0 -1
- package/src/tsconfig.js +0 -103
- package/src/tsconfig.js.map +0 -1
- package/src/types/Command.js +0 -9
- package/src/types/Command.js.map +0 -1
- package/src/utils.js +0 -15
- package/src/utils.js.map +0 -1
- package/src/validateInIsaacScriptProject.js +0 -36
- package/src/validateInIsaacScriptProject.js.map +0 -1
- package/src/validateNodeVersion.js +0 -29
- package/src/validateNodeVersion.js.map +0 -1
- package/src/version.js +0 -20
- package/src/version.js.map +0 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { PACKAGE_JSON, fatalError, getPackageJSONVersion, getPackageManagerInstallCommand, isFile, readFile, updatePackageJSON, writeFile, } from "isaacscript-common-node";
|
|
2
|
+
import { CONSTANTS_TS_PATH, METADATA_XML_PATH, VERSION_TXT_PATH, } from "../../constants.js";
|
|
3
|
+
import { execShell, execShellString } from "../../exec.js";
|
|
4
|
+
import { gitCommitAllAndPush } from "../../git.js";
|
|
5
|
+
import { getPackageManagerUsedForExistingProject } from "../../packageManager.js";
|
|
6
|
+
/**
|
|
7
|
+
* Before uploading the project, we want to update dependencies, increment the version, and perform
|
|
8
|
+
* some other steps.
|
|
9
|
+
*/
|
|
10
|
+
export async function prePublish(args, typeScript) {
|
|
11
|
+
const skipUpdate = args.skipUpdate === true;
|
|
12
|
+
const skipLint = args.skipLint === true;
|
|
13
|
+
const dryRun = args.dryRun === true;
|
|
14
|
+
const verbose = args.verbose === true;
|
|
15
|
+
const packageManager = getPackageManagerUsedForExistingProject(args);
|
|
16
|
+
execShellString("git pull --rebase");
|
|
17
|
+
execShellString("git push");
|
|
18
|
+
await updateDependencies(skipUpdate, dryRun, packageManager, verbose);
|
|
19
|
+
incrementVersion(args, typeScript);
|
|
20
|
+
tryRunNPMScript("build", verbose);
|
|
21
|
+
if (!skipLint) {
|
|
22
|
+
tryRunNPMScript("lint", verbose);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function updateDependencies(skipUpdate, dryRun, packageManager, verbose) {
|
|
26
|
+
if (skipUpdate) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
console.log(`Updating dependencies in the "${PACKAGE_JSON}" file...`);
|
|
30
|
+
const hasNewDependencies = await updatePackageJSON(undefined);
|
|
31
|
+
if (hasNewDependencies) {
|
|
32
|
+
const command = getPackageManagerInstallCommand(packageManager);
|
|
33
|
+
execShellString(command, verbose);
|
|
34
|
+
if (!dryRun) {
|
|
35
|
+
gitCommitAllAndPush("chore: update dependencies", verbose);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function incrementVersion(args, typeScript) {
|
|
40
|
+
const skipIncrement = args.skipIncrement === true;
|
|
41
|
+
const verbose = args.verbose === true;
|
|
42
|
+
if (skipIncrement) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const versionCommandArgument = getVersionCommandArgument(args);
|
|
46
|
+
// We always use `npm` here to avoid differences with the version command between package
|
|
47
|
+
// managers. The "--no-git-tag-version" flag will prevent npm from both making a commit and adding
|
|
48
|
+
// a tag.
|
|
49
|
+
execShellString(`npm version ${versionCommandArgument} --no-git-tag-version`, verbose);
|
|
50
|
+
if (!typeScript) {
|
|
51
|
+
const version = getPackageJSONVersion(undefined);
|
|
52
|
+
writeVersionToConstantsTS(version);
|
|
53
|
+
writeVersionToMetadataXML(version);
|
|
54
|
+
writeVersionToVersionTXT(version);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function getVersionCommandArgument(args) {
|
|
58
|
+
const { setVersion } = args;
|
|
59
|
+
if (setVersion !== undefined) {
|
|
60
|
+
// They want to use a specific version, which was manually specified.
|
|
61
|
+
return setVersion;
|
|
62
|
+
}
|
|
63
|
+
const major = args.major === true;
|
|
64
|
+
if (major) {
|
|
65
|
+
return "major";
|
|
66
|
+
}
|
|
67
|
+
const minor = args.minor === true;
|
|
68
|
+
if (minor) {
|
|
69
|
+
return "minor";
|
|
70
|
+
}
|
|
71
|
+
const patch = args.patch === true;
|
|
72
|
+
if (patch) {
|
|
73
|
+
return "patch";
|
|
74
|
+
}
|
|
75
|
+
// Default to a patch version.
|
|
76
|
+
return "patch";
|
|
77
|
+
}
|
|
78
|
+
function writeVersionToConstantsTS(version) {
|
|
79
|
+
if (!isFile(CONSTANTS_TS_PATH)) {
|
|
80
|
+
console.log('Skipping writing the version to "constants.ts" since it was not found.');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const constantsTS = readFile(CONSTANTS_TS_PATH);
|
|
84
|
+
const newConstantsTS = constantsTS.replace(/const VERSION = ".+"/, `const VERSION = "${version}"`);
|
|
85
|
+
writeFile(CONSTANTS_TS_PATH, newConstantsTS);
|
|
86
|
+
console.log(`The version of ${version} was written to: ${CONSTANTS_TS_PATH}`);
|
|
87
|
+
}
|
|
88
|
+
function writeVersionToMetadataXML(version) {
|
|
89
|
+
const metadataXML = readFile(METADATA_XML_PATH);
|
|
90
|
+
const newMetadataXML = metadataXML.replace(/<version>.+<\/version>/, `<version>${version}</version>`);
|
|
91
|
+
writeFile(METADATA_XML_PATH, newMetadataXML);
|
|
92
|
+
console.log(`The version of ${version} was written to: ${METADATA_XML_PATH}`);
|
|
93
|
+
}
|
|
94
|
+
function writeVersionToVersionTXT(version) {
|
|
95
|
+
writeFile(VERSION_TXT_PATH, version);
|
|
96
|
+
console.log(`The version of ${version} was written to: ${VERSION_TXT_PATH}`);
|
|
97
|
+
}
|
|
98
|
+
function tryRunNPMScript(scriptName, verbose) {
|
|
99
|
+
console.log(`Running: ${scriptName}`);
|
|
100
|
+
const { exitStatus, stdout } = execShell("npm", ["run", scriptName], verbose, true);
|
|
101
|
+
if (exitStatus !== 0) {
|
|
102
|
+
execShellString("git reset --hard", verbose); // Revert the version changes.
|
|
103
|
+
fatalError(`Failed to run "${scriptName}":`, stdout);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getPackageJSONField, getPackageJSONVersion, } from "isaacscript-common-node";
|
|
2
|
+
import { execShellString } from "../../exec.js";
|
|
3
|
+
import { getReleaseGitCommitMessage, gitCommitAllAndPush } from "../../git.js";
|
|
4
|
+
import { getInputString } from "../../prompt.js";
|
|
5
|
+
import { publishIsaacScriptMod } from "./isaacscriptMod.js";
|
|
6
|
+
import { prePublish } from "./prePublish.js";
|
|
7
|
+
import { validate } from "./validate.js";
|
|
8
|
+
export async function publish(args, config, typeScript) {
|
|
9
|
+
const { setVersion } = args;
|
|
10
|
+
const verbose = args.verbose === true;
|
|
11
|
+
validate(typeScript, setVersion, verbose);
|
|
12
|
+
await prePublish(args, typeScript);
|
|
13
|
+
await (typeScript
|
|
14
|
+
? publishTypeScriptProject(args)
|
|
15
|
+
: publishIsaacScriptMod(args, config));
|
|
16
|
+
}
|
|
17
|
+
async function publishTypeScriptProject(args) {
|
|
18
|
+
const dryRun = args.dryRun === true;
|
|
19
|
+
const verbose = args.verbose === true;
|
|
20
|
+
const projectName = getPackageJSONField(undefined, "name");
|
|
21
|
+
const version = getPackageJSONVersion(undefined);
|
|
22
|
+
if (dryRun) {
|
|
23
|
+
execShellString("git reset --hard", verbose); // Revert the version changes.
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
const releaseGitCommitMessage = getReleaseGitCommitMessage(version);
|
|
27
|
+
gitCommitAllAndPush(releaseGitCommitMessage, verbose);
|
|
28
|
+
let command = "npm publish --access=public";
|
|
29
|
+
if (args.otp === true) {
|
|
30
|
+
const otpCode = await getInputString("Type in the two-factor OTP code tied to the npm account. (Just press enter if you do not have 2FA enabled on your npm account.)");
|
|
31
|
+
if (otpCode !== "") {
|
|
32
|
+
command += ` --otp=${otpCode}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
execShellString(command, verbose);
|
|
36
|
+
}
|
|
37
|
+
const dryRunSuffix = dryRun ? " (dry-run)" : "";
|
|
38
|
+
console.log(`Published ${projectName} version ${version} successfully${dryRunSuffix}.`);
|
|
39
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { PACKAGE_JSON, fatalError, isFile, isGitRepository, isGitRepositoryClean, isLoggedInToNPM, } from "isaacscript-common-node";
|
|
3
|
+
import { CWD, PROJECT_NAME } from "../../constants.js";
|
|
4
|
+
import { execPowershell } from "../../exec.js";
|
|
5
|
+
export function validate(typeScript, setVersion, verbose) {
|
|
6
|
+
if (!isGitRepository(CWD)) {
|
|
7
|
+
fatalError("Failed to publish since the current working directory is not inside of a git repository.");
|
|
8
|
+
}
|
|
9
|
+
if (!isGitRepositoryClean(CWD)) {
|
|
10
|
+
fatalError("Failed to publish since the Git repository was dirty. Before publishing, you must push any current changes to git. (Version commits should not contain any code changes.)");
|
|
11
|
+
}
|
|
12
|
+
if (!isFile(PACKAGE_JSON)) {
|
|
13
|
+
fatalError(`Failed to find the "${PACKAGE_JSON}" file in the current working directory.`);
|
|
14
|
+
}
|
|
15
|
+
if (setVersion !== undefined && /^\d+\.\d+\.\d+$/.exec(setVersion) === null) {
|
|
16
|
+
fatalError(chalk.red(`The version of "${setVersion}" does not match the semantic versioning format.`));
|
|
17
|
+
}
|
|
18
|
+
if (typeScript) {
|
|
19
|
+
validateTypeScriptProject();
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
validateIsaacScriptMod(verbose);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function validateTypeScriptProject() {
|
|
26
|
+
if (!isLoggedInToNPM()) {
|
|
27
|
+
fatalError('Failed to publish since you are not logged in to npm. Try doing "npm login".');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function validateIsaacScriptMod(verbose) {
|
|
31
|
+
validateIsaacScriptOtherCopiesNotRunning(verbose);
|
|
32
|
+
}
|
|
33
|
+
function validateIsaacScriptOtherCopiesNotRunning(verbose) {
|
|
34
|
+
if (process.platform !== "win32") {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// From: https://securityboulevard.com/2020/01/get-process-list-with-command-line-arguments/
|
|
38
|
+
const stdout = execPowershell("Get-WmiObject Win32_Process -Filter \"name = 'node.exe'\" | Select-Object -ExpandProperty CommandLine", verbose);
|
|
39
|
+
const lines = stdout.split("\r\n");
|
|
40
|
+
const otherCopiesOfRunningIsaacScript = lines.filter((line) => line.includes("node.exe") &&
|
|
41
|
+
line.includes("isaacscript") &&
|
|
42
|
+
!line.includes("isaacscript publish"));
|
|
43
|
+
if (otherCopiesOfRunningIsaacScript.length > 0) {
|
|
44
|
+
fatalError(chalk.red(`Other copies of ${PROJECT_NAME} appear to be running. You must close those copies before publishing.`));
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/common.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// These are functions copied from `isaacscript-common`.
|
|
2
|
+
import { fatalError } from "isaacscript-common-node";
|
|
3
|
+
import { parseIntSafe } from "isaacscript-common-ts";
|
|
4
|
+
import { DOOR_SLOT_TO_DOOR_SLOT_FLAG } from "./objects/copied/doorSlotToDoorSlotFlag.js";
|
|
5
|
+
import { ROOM_SHAPE_TO_DOOR_SLOT_COORDINATES } from "./objects/copied/roomShapeToDoorSlotCoordinates.js";
|
|
6
|
+
/** This is copied from `isaacscript-common`. */
|
|
7
|
+
export function getJSONRoomDoorSlotFlags(jsonRoom) {
|
|
8
|
+
const roomShapeString = jsonRoom.$.shape;
|
|
9
|
+
const roomShapeNumber = parseIntSafe(roomShapeString);
|
|
10
|
+
if (roomShapeNumber === undefined) {
|
|
11
|
+
fatalError(`Failed to parse the "shape" field of a custom stage room: ${roomShapeString}`);
|
|
12
|
+
}
|
|
13
|
+
const roomShape = roomShapeNumber;
|
|
14
|
+
let doorSlotFlags = 0;
|
|
15
|
+
for (const door of jsonRoom.door) {
|
|
16
|
+
const existsString = door.$.exists;
|
|
17
|
+
if (existsString !== "True" && existsString !== "False") {
|
|
18
|
+
fatalError(`Failed to parse the "exists" field of a custom stage room door: ${existsString}`);
|
|
19
|
+
}
|
|
20
|
+
if (existsString === "False") {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const xString = door.$.x;
|
|
24
|
+
const x = parseIntSafe(xString);
|
|
25
|
+
if (x === undefined) {
|
|
26
|
+
fatalError(`Failed to parse the "x" field of a custom stage room door: ${xString}`);
|
|
27
|
+
}
|
|
28
|
+
const yString = door.$.y;
|
|
29
|
+
const y = parseIntSafe(yString);
|
|
30
|
+
if (y === undefined) {
|
|
31
|
+
fatalError(`Failed to parse the "y" field of a custom stage room door: ${yString}`);
|
|
32
|
+
}
|
|
33
|
+
const doorSlot = getRoomShapeDoorSlot(roomShape, x, y);
|
|
34
|
+
if (doorSlot === undefined) {
|
|
35
|
+
fatalError(`Failed to retrieve the door slot for a custom stage room door at coordinates: [${x}, ${y}]`);
|
|
36
|
+
}
|
|
37
|
+
const doorSlotFlag = doorSlotToDoorSlotFlag(doorSlot);
|
|
38
|
+
doorSlotFlags = addFlag(doorSlotFlags, doorSlotFlag);
|
|
39
|
+
}
|
|
40
|
+
return doorSlotFlags;
|
|
41
|
+
}
|
|
42
|
+
/** This is copied from `isaacscript-common`. */
|
|
43
|
+
function getRoomShapeDoorSlot(roomShape, x, y) {
|
|
44
|
+
// The type assertion is necessary for some reason.
|
|
45
|
+
const doorSlotCoordinates = ROOM_SHAPE_TO_DOOR_SLOT_COORDINATES[roomShape];
|
|
46
|
+
for (const [doorSlotString, coordinates] of Object.entries(doorSlotCoordinates)) {
|
|
47
|
+
const doorSlot = parseIntSafe(doorSlotString);
|
|
48
|
+
const [doorX, doorY] = coordinates;
|
|
49
|
+
if (x === doorX && y === doorY) {
|
|
50
|
+
return doorSlot;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
/** This is copied from `isaacscript-common`. */
|
|
56
|
+
function doorSlotToDoorSlotFlag(doorSlot) {
|
|
57
|
+
return DOOR_SLOT_TO_DOOR_SLOT_FLAG[doorSlot];
|
|
58
|
+
}
|
|
59
|
+
/** This is copied from `isaacscript-common`. */
|
|
60
|
+
function addFlag(flags, ...flagsToAdd) {
|
|
61
|
+
for (const flagToAdd of flagsToAdd) {
|
|
62
|
+
flags |= flagToAdd; // eslint-disable-line no-bitwise,no-param-reassign
|
|
63
|
+
}
|
|
64
|
+
return flags;
|
|
65
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { fatalError, getJSONC, isFile, writeFile, } from "isaacscript-common-node";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Config } from "./classes/Config.js";
|
|
4
|
+
import { getModsDir } from "./commands/init/getModsDir.js";
|
|
5
|
+
import { promptSaveSlot } from "./commands/init/promptSaveSlot.js";
|
|
6
|
+
import { CONFIG_FILE_NAME, CONFIG_FILE_PATH, CWD } from "./constants.js";
|
|
7
|
+
const NUM_INDENT_SPACES = 2;
|
|
8
|
+
export async function getConfigFromFile(args, typeScript) {
|
|
9
|
+
const yes = args.yes === true;
|
|
10
|
+
const dev = args.dev === true;
|
|
11
|
+
const existingConfig = getExistingConfig();
|
|
12
|
+
if (existingConfig !== undefined) {
|
|
13
|
+
return existingConfig;
|
|
14
|
+
}
|
|
15
|
+
// No config file exists, so prompt the user for some information and create one.
|
|
16
|
+
const modsDirectory = await getModsDir(args, typeScript);
|
|
17
|
+
const saveSlot = await promptSaveSlot(args, yes);
|
|
18
|
+
const config = new Config(modsDirectory, saveSlot, dev);
|
|
19
|
+
createConfigFile(CWD, config, typeScript);
|
|
20
|
+
return config;
|
|
21
|
+
}
|
|
22
|
+
function getExistingConfig() {
|
|
23
|
+
if (!isFile(CONFIG_FILE_PATH)) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const config = getJSONC(CONFIG_FILE_PATH);
|
|
27
|
+
validateMandatoryConfigFields(config);
|
|
28
|
+
return config;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Even though some fields are always initialized in the class, it may not be necessarily exist in
|
|
32
|
+
* the JSON file.
|
|
33
|
+
*/
|
|
34
|
+
function validateMandatoryConfigFields(config) {
|
|
35
|
+
if (config["modsDirectory"] === undefined) {
|
|
36
|
+
errorMissing("modsDirectory", "This should be equal to the directory where Isaac mods live on your system.");
|
|
37
|
+
}
|
|
38
|
+
if (config["saveSlot"] === undefined) {
|
|
39
|
+
errorMissing("saveSlot", "This should be equal to the save slot that you test your mods on.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function errorMissing(field, description) {
|
|
43
|
+
fatalError(`The "${CONFIG_FILE_NAME}" file is missing a "${field}" value. ${description} Please add it.`);
|
|
44
|
+
}
|
|
45
|
+
export function createConfigFile(projectPath, config, typeScript) {
|
|
46
|
+
if (typeScript) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const configFilePath = path.join(projectPath, CONFIG_FILE_NAME);
|
|
50
|
+
const configContents = JSON.stringify(config, undefined, NUM_INDENT_SPACES);
|
|
51
|
+
// Add a newline at the end to satisfy Prettier.
|
|
52
|
+
const configContentsWithNewline = `${configContents}\n`;
|
|
53
|
+
writeFile(configFilePath, configContentsWithNewline);
|
|
54
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { PACKAGE_JSON, TSCONFIG_JSON, dirName } from "isaacscript-common-node";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const __dirname = dirName();
|
|
5
|
+
// Miscellaneous
|
|
6
|
+
export const CWD = process.cwd();
|
|
7
|
+
export const CURRENT_DIRECTORY_NAME = path.basename(CWD);
|
|
8
|
+
export const HOME_DIR = os.homedir();
|
|
9
|
+
export const FILE_SYNCED_MESSAGE = "File synced:";
|
|
10
|
+
export const COMPILATION_SUCCESSFUL_MESSAGE = "Compilation successful.";
|
|
11
|
+
export const MOD_UPLOADER_PATH = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\The Binding of Isaac Rebirth\\tools\\ModUploader\\ModUploader.exe";
|
|
12
|
+
export const PROJECT_NAME = "IsaacScript";
|
|
13
|
+
// `isaacscript`
|
|
14
|
+
export const REPO_ROOT = path.join(__dirname, "..");
|
|
15
|
+
// `isaacscript/custom-stage`
|
|
16
|
+
const CUSTOM_STAGE_PATH = path.join(REPO_ROOT, "custom-stage");
|
|
17
|
+
export const CUSTOM_STAGE_FILES_DIR = path.join(CUSTOM_STAGE_PATH, "isaacscript-custom-stage");
|
|
18
|
+
export const SHADERS_XML_PATH = path.join(CUSTOM_STAGE_PATH, "shaders.xml");
|
|
19
|
+
export const XML_CONVERTER_PATH = path.join(CUSTOM_STAGE_PATH, "xml-converter.exe");
|
|
20
|
+
// `isaacscript/isaacscript-watcher`
|
|
21
|
+
export const DISABLE_IT_FILE = "disable.it";
|
|
22
|
+
export const WATCHER_MOD_NAME = "isaacscript-watcher";
|
|
23
|
+
export const WATCHER_MOD_SOURCE_PATH = path.join(REPO_ROOT, WATCHER_MOD_NAME);
|
|
24
|
+
// `isaacscript/file-templates`
|
|
25
|
+
export const TEMPLATES_DIR = path.join(REPO_ROOT, "file-templates");
|
|
26
|
+
// `isaacscript/file-templates/static`
|
|
27
|
+
export const TEMPLATES_STATIC_DIR = path.join(TEMPLATES_DIR, "static");
|
|
28
|
+
// `isaacscript/file-templates/dynamic`
|
|
29
|
+
export const TEMPLATES_DYNAMIC_DIR = path.join(TEMPLATES_DIR, "dynamic");
|
|
30
|
+
export const CI_YML = "ci.yml";
|
|
31
|
+
export const CI_YML_TEMPLATE_PATH = path.join(TEMPLATES_DYNAMIC_DIR, ".github", "workflows", CI_YML);
|
|
32
|
+
export const ACTION_YML = "action.yml";
|
|
33
|
+
export const ACTION_YML_TEMPLATE_PATH = path.join(TEMPLATES_DYNAMIC_DIR, ".github", "workflows", "setup", ACTION_YML);
|
|
34
|
+
export const YARNRC_YML = ".yarnrc.yml";
|
|
35
|
+
export const YARNRC_TEMPLATE_PATH = path.join(TEMPLATES_DYNAMIC_DIR, YARNRC_YML);
|
|
36
|
+
const GITIGNORE = "_gitignore"; // Not named ".gitignore" to prevent npm from deleting it.
|
|
37
|
+
export const GITIGNORE_TEMPLATE_PATH = path.join(TEMPLATES_DYNAMIC_DIR, GITIGNORE);
|
|
38
|
+
export const MAIN_TS = "main.ts";
|
|
39
|
+
export const MAIN_TS_TEMPLATE_PATH = path.join(TEMPLATES_DYNAMIC_DIR, "src", MAIN_TS);
|
|
40
|
+
export const MAIN_DEV_TS_TEMPLATE_PATH = path.join(TEMPLATES_DYNAMIC_DIR, "src", "main-dev.ts");
|
|
41
|
+
export const METADATA_XML = "metadata.xml";
|
|
42
|
+
export const METADATA_XML_TEMPLATE_PATH = path.join(TEMPLATES_DYNAMIC_DIR, "mod", METADATA_XML);
|
|
43
|
+
export const README_MD = "README.md";
|
|
44
|
+
// `isaacscript/schemas`
|
|
45
|
+
const SCHEMAS_DIR = path.join(REPO_ROOT, "schemas");
|
|
46
|
+
export const ISAACSCRIPT_SCHEMA_PATH = path.join(SCHEMAS_DIR, "tsconfig-isaacscript-section-schema.json");
|
|
47
|
+
// `project`
|
|
48
|
+
export const CONFIG_FILE_NAME = "isaacscript.json";
|
|
49
|
+
export const CONFIG_FILE_PATH = path.join(CWD, CONFIG_FILE_NAME);
|
|
50
|
+
export const TSCONFIG_JSON_PATH = path.join(CWD, TSCONFIG_JSON);
|
|
51
|
+
export const PACKAGE_JSON_PATH = path.join(CWD, PACKAGE_JSON);
|
|
52
|
+
export const CONSTANTS_TS_PATH = path.join(CWD, "src", "constants.ts");
|
|
53
|
+
// `project/mod`
|
|
54
|
+
export const MOD_SOURCE_PATH = path.join(CWD, "mod");
|
|
55
|
+
export const MAIN_LUA = "main.lua";
|
|
56
|
+
export const METADATA_XML_PATH = path.join(MOD_SOURCE_PATH, "metadata.xml");
|
|
57
|
+
export const VERSION_TXT_PATH = path.join(MOD_SOURCE_PATH, "version.txt");
|
|
58
|
+
// `project/scripts`
|
|
59
|
+
const SCRIPTS_PATH = path.join(CWD, "scripts");
|
|
60
|
+
export const PUBLISH_PRE_COPY_PY_PATH = path.join(SCRIPTS_PATH, "publish_pre_copy.py");
|
|
61
|
+
export const PUBLISH_POST_COPY_PY_PATH = path.join(SCRIPTS_PATH, "publish_post_copy.py");
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { copyFileOrDirectory, fatalError, getFileNamesInDirectory, getPackageManagerAddCommand, isDirectory, isFile, makeDirectory, readFile, writeFile, } from "isaacscript-common-node";
|
|
3
|
+
import { parseFloatSafe, parseIntSafe } from "isaacscript-common-ts";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import * as tstl from "typescript-to-lua";
|
|
6
|
+
import xml2js from "xml2js";
|
|
7
|
+
import { getJSONRoomDoorSlotFlags } from "./common.js";
|
|
8
|
+
import { CUSTOM_STAGE_FILES_DIR, CWD, MOD_SOURCE_PATH, SHADERS_XML_PATH, XML_CONVERTER_PATH, } from "./constants.js";
|
|
9
|
+
import { execExe } from "./exec.js";
|
|
10
|
+
import { getCustomStagesFromTSConfig } from "./tsconfig.js";
|
|
11
|
+
const ISAACSCRIPT_COMMON = "isaacscript-common";
|
|
12
|
+
const ISAACSCRIPT_COMMON_PATH = path.join(CWD, "node_modules", ISAACSCRIPT_COMMON, "dist", "src");
|
|
13
|
+
const METADATA_LUA_PATH = path.join(ISAACSCRIPT_COMMON_PATH, "customStageMetadata.lua");
|
|
14
|
+
const ROOM_VARIANT_MULTIPLIER = 10000;
|
|
15
|
+
const VARIANT_REGEX = / variant="(?<variant>\d+)"/;
|
|
16
|
+
const WEIGHT_REGEX = / weight=".+?"/;
|
|
17
|
+
const EMPTY_SHADER_NAME = "IsaacScript-RenderAboveHUD";
|
|
18
|
+
export async function prepareCustomStages(packageManager, verbose) {
|
|
19
|
+
const customStagesTSConfig = getCustomStagesFromTSConfig();
|
|
20
|
+
if (customStagesTSConfig.length === 0) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
validateCustomStagePaths(customStagesTSConfig);
|
|
24
|
+
copyCustomStageFilesToProject();
|
|
25
|
+
await insertEmptyShader();
|
|
26
|
+
await fillCustomStageMetadata(customStagesTSConfig, packageManager);
|
|
27
|
+
combineCustomStageXMLs(customStagesTSConfig, verbose);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Before we proceed with compiling the mod, ensure that all of the file paths that the end-user put
|
|
31
|
+
* in their "tsconfig.json" file map to actual files on the file system.
|
|
32
|
+
*/
|
|
33
|
+
function validateCustomStagePaths(customStagesTSConfig) {
|
|
34
|
+
for (const customStageTSConfig of customStagesTSConfig) {
|
|
35
|
+
if (customStageTSConfig.backdropPNGPaths !== undefined) {
|
|
36
|
+
for (const filePaths of Object.values(customStageTSConfig.backdropPNGPaths)) {
|
|
37
|
+
for (const filePath of filePaths) {
|
|
38
|
+
checkFile(filePath);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const filePath of [
|
|
43
|
+
customStageTSConfig.decorationsPNGPath,
|
|
44
|
+
customStageTSConfig.decorationsANM2Path,
|
|
45
|
+
customStageTSConfig.rocksPNGPath,
|
|
46
|
+
customStageTSConfig.rocksANM2Path,
|
|
47
|
+
customStageTSConfig.pitsPNGPath,
|
|
48
|
+
customStageTSConfig.pitsANM2Path,
|
|
49
|
+
]) {
|
|
50
|
+
checkFile(filePath);
|
|
51
|
+
}
|
|
52
|
+
if (customStageTSConfig.doorPNGPaths !== undefined) {
|
|
53
|
+
for (const filePath of Object.values(customStageTSConfig.doorPNGPaths)) {
|
|
54
|
+
checkFile(filePath);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (customStageTSConfig.shadows !== undefined) {
|
|
58
|
+
for (const stageShadows of Object.values(customStageTSConfig.shadows)) {
|
|
59
|
+
for (const stageShadow of stageShadows) {
|
|
60
|
+
checkFile(stageShadow.pngPath);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (customStageTSConfig.bossPool !== undefined) {
|
|
65
|
+
for (const bossPoolEntry of Object.values(customStageTSConfig.bossPool)) {
|
|
66
|
+
if (bossPoolEntry.versusScreen !== undefined) {
|
|
67
|
+
checkFile(bossPoolEntry.versusScreen.namePNGPath);
|
|
68
|
+
checkFile(bossPoolEntry.versusScreen.portraitPNGPath);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function checkFile(filePath) {
|
|
75
|
+
if (filePath === undefined) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!filePath.includes("gfx/")) {
|
|
79
|
+
fatalError(`Failed to validate the "${filePath}" file: all PNG file paths must be inside of a "gfx" directory. (e.g. "./mod/resources/gfx/backdrop/foo/nfloor.png")`);
|
|
80
|
+
}
|
|
81
|
+
if (!isFile(filePath)) {
|
|
82
|
+
fatalError(`Failed to find the "${filePath}" file. Check your "tsconfig.json" file and then restart IsaacScript.`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** The custom stages feature needs some anm2 files in order to work properly. */
|
|
86
|
+
function copyCustomStageFilesToProject() {
|
|
87
|
+
const dstDirPath = path.join(CWD, "mod", "resources", "gfx", "isaacscript-custom-stage");
|
|
88
|
+
makeDirectory(dstDirPath);
|
|
89
|
+
const fileNames = getFileNamesInDirectory(CUSTOM_STAGE_FILES_DIR);
|
|
90
|
+
for (const fileName of fileNames) {
|
|
91
|
+
const srcPath = path.join(CUSTOM_STAGE_FILES_DIR, fileName);
|
|
92
|
+
const dstPath = path.join(dstDirPath, fileName);
|
|
93
|
+
copyFileOrDirectory(srcPath, dstPath);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The custom stage feature requires an empty shader to be present in order to render sprites on top
|
|
98
|
+
* of the HUD.
|
|
99
|
+
*/
|
|
100
|
+
async function insertEmptyShader() {
|
|
101
|
+
const shadersXMLDstPath = path.join(CWD, "mod", "content", "shaders.xml");
|
|
102
|
+
if (!isFile(shadersXMLDstPath)) {
|
|
103
|
+
copyFileOrDirectory(SHADERS_XML_PATH, shadersXMLDstPath);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// The end-user mod might have their own custom shaders, so we need to merge our empty shader
|
|
107
|
+
// inside the existing "shaders.xml" file.
|
|
108
|
+
const shadersXMLDstContents = readFile(shadersXMLDstPath);
|
|
109
|
+
const shadersXMLDst = (await xml2js.parseStringPromise(shadersXMLDstContents));
|
|
110
|
+
const hasIsaacScriptEmptyShader = shadersXMLDst.shaders.shader.some((shader) => shader.$.name === EMPTY_SHADER_NAME);
|
|
111
|
+
if (hasIsaacScriptEmptyShader) {
|
|
112
|
+
// Our empty shader already exists, so we don't have to do anything.
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const shadersXMLSrcContents = readFile(SHADERS_XML_PATH);
|
|
116
|
+
const shadersXMLSrc = (await xml2js.parseStringPromise(shadersXMLSrcContents));
|
|
117
|
+
const isaacScriptEmptyShader = shadersXMLSrc.shaders.shader.find((shader) => shader.$.name === EMPTY_SHADER_NAME);
|
|
118
|
+
if (isaacScriptEmptyShader === undefined) {
|
|
119
|
+
fatalError(`Failed to find the empty shader named "${EMPTY_SHADER_NAME}" in the following file: ${SHADERS_XML_PATH}`);
|
|
120
|
+
}
|
|
121
|
+
// Add the empty shader to the mod's existing "shaders.xml" file.
|
|
122
|
+
shadersXMLDst.shaders.shader.push(isaacScriptEmptyShader);
|
|
123
|
+
const xmlBuilder = new xml2js.Builder();
|
|
124
|
+
const newXML = xmlBuilder.buildObject(shadersXMLDst);
|
|
125
|
+
writeFile(shadersXMLDstPath, newXML);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* In order for the custom stage feature to work properly, Lua code will need to know the list of
|
|
129
|
+
* possible rooms corresponding to a custom stage. In an end-user mod, this is a Lua file located at
|
|
130
|
+
* `METADATA_LUA_PATH`. By default, the file is blank, and must be filled in by tooling before
|
|
131
|
+
* compiling the mod.
|
|
132
|
+
*/
|
|
133
|
+
async function fillCustomStageMetadata(customStagesTSConfig, packageManager) {
|
|
134
|
+
validateMetadataLuaFileExists(packageManager);
|
|
135
|
+
const customStages = await getCustomStagesWithMetadata(customStagesTSConfig);
|
|
136
|
+
const customStagesLua = convertCustomStagesToLua(customStages);
|
|
137
|
+
writeFile(METADATA_LUA_PATH, customStagesLua);
|
|
138
|
+
console.log(`Wrote custom stage metadata to: ${METADATA_LUA_PATH}`);
|
|
139
|
+
}
|
|
140
|
+
function validateMetadataLuaFileExists(packageManager) {
|
|
141
|
+
if (!isDirectory(ISAACSCRIPT_COMMON_PATH)) {
|
|
142
|
+
const addCommand = getPackageManagerAddCommand(packageManager, ISAACSCRIPT_COMMON);
|
|
143
|
+
fatalError(`${chalk.red(`The custom stages feature requires a dependency of "${ISAACSCRIPT_COMMON}" in the "package.json" file. You can add it with the following command:`)} ${chalk.green(addCommand)}`);
|
|
144
|
+
}
|
|
145
|
+
if (!isFile(METADATA_LUA_PATH)) {
|
|
146
|
+
fatalError(`${chalk.red("Failed to find the custom stage metadata file at:")} ${chalk.red(METADATA_LUA_PATH)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* This parses all of the end-user's XML files and gathers metadata about all of the rooms within.
|
|
151
|
+
* (In other words, this creates the full set of `CustomStageLua` objects.)
|
|
152
|
+
*/
|
|
153
|
+
async function getCustomStagesWithMetadata(customStagesTSConfig) {
|
|
154
|
+
if (!isFile(METADATA_LUA_PATH)) {
|
|
155
|
+
fatalError(`${chalk.red("Failed to find the custom stage metadata file at:")} ${chalk.red(METADATA_LUA_PATH)}`);
|
|
156
|
+
}
|
|
157
|
+
const customStagesLua = [];
|
|
158
|
+
for (const customStageTSConfig of customStagesTSConfig) {
|
|
159
|
+
// Some manual input validation was already performed in the `getCustomStagesFromTSConfig`
|
|
160
|
+
// function.
|
|
161
|
+
const { name } = customStageTSConfig;
|
|
162
|
+
const { xmlPath } = customStageTSConfig;
|
|
163
|
+
const resolvedXMLPath = path.resolve(CWD, xmlPath);
|
|
164
|
+
if (!isFile(resolvedXMLPath)) {
|
|
165
|
+
fatalError(`${chalk.red("Failed to find the custom stage XML file at:")} ${chalk.red(resolvedXMLPath)}`);
|
|
166
|
+
}
|
|
167
|
+
const xmlContents = readFile(resolvedXMLPath);
|
|
168
|
+
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-unsafe-assignment
|
|
169
|
+
const jsonRoomsFileAny = await xml2js.parseStringPromise(xmlContents);
|
|
170
|
+
const jsonRoomsFile = jsonRoomsFileAny;
|
|
171
|
+
const roomVariantSet = new Set();
|
|
172
|
+
const customStageRoomsMetadata = [];
|
|
173
|
+
for (const room of jsonRoomsFile.rooms.room) {
|
|
174
|
+
const typeString = room.$.type;
|
|
175
|
+
const type = parseIntSafe(typeString);
|
|
176
|
+
if (type === undefined) {
|
|
177
|
+
fatalError(`Failed to parse the type of one of the "${name}" custom stage rooms: ${typeString}`);
|
|
178
|
+
}
|
|
179
|
+
const variantString = room.$.variant;
|
|
180
|
+
const baseVariant = parseIntSafe(variantString);
|
|
181
|
+
if (baseVariant === undefined) {
|
|
182
|
+
fatalError(`Failed to parse the variant of one of the "${name}" custom stage rooms: ${variantString}`);
|
|
183
|
+
}
|
|
184
|
+
if (roomVariantSet.has(baseVariant)) {
|
|
185
|
+
fatalError(chalk.red(`There is more than one room with a variant of "${baseVariant}" in the "${resolvedXMLPath}" file. Make sure that each room has a unique variant. (The room variant is also called the "ID" in Basement Renovator.)`));
|
|
186
|
+
}
|
|
187
|
+
roomVariantSet.add(baseVariant);
|
|
188
|
+
const roomVariantPrefix = customStageTSConfig.roomVariantPrefix * ROOM_VARIANT_MULTIPLIER;
|
|
189
|
+
const variant = roomVariantPrefix + baseVariant;
|
|
190
|
+
const subTypeString = room.$.subtype;
|
|
191
|
+
const subType = parseIntSafe(subTypeString);
|
|
192
|
+
if (subType === undefined) {
|
|
193
|
+
fatalError(`Failed to parse the sub-type of one of the "${name}" custom stage rooms: ${subTypeString}`);
|
|
194
|
+
}
|
|
195
|
+
const shapeString = room.$.shape;
|
|
196
|
+
const shape = parseIntSafe(shapeString);
|
|
197
|
+
if (shape === undefined) {
|
|
198
|
+
fatalError(`Failed to parse the shape of one of the "${name}" custom stage rooms: ${shapeString}`);
|
|
199
|
+
}
|
|
200
|
+
const doorSlotFlags = getJSONRoomDoorSlotFlags(room);
|
|
201
|
+
const weightString = room.$.weight;
|
|
202
|
+
const weight = parseFloatSafe(weightString);
|
|
203
|
+
if (weight === undefined) {
|
|
204
|
+
fatalError(`Failed to parse the weight of one of the "${name}" custom stage rooms: ${weightString}`);
|
|
205
|
+
}
|
|
206
|
+
const customStageRoomMetadata = {
|
|
207
|
+
type,
|
|
208
|
+
variant,
|
|
209
|
+
subType,
|
|
210
|
+
shape,
|
|
211
|
+
doorSlotFlags,
|
|
212
|
+
weight,
|
|
213
|
+
};
|
|
214
|
+
customStageRoomsMetadata.push(customStageRoomMetadata);
|
|
215
|
+
}
|
|
216
|
+
const customStageLua = {
|
|
217
|
+
...customStageTSConfig,
|
|
218
|
+
roomsMetadata: customStageRoomsMetadata,
|
|
219
|
+
};
|
|
220
|
+
customStagesLua.push(customStageLua);
|
|
221
|
+
}
|
|
222
|
+
return customStagesLua;
|
|
223
|
+
}
|
|
224
|
+
function convertCustomStagesToLua(customStages) {
|
|
225
|
+
const customStagesString = JSON.stringify(customStages);
|
|
226
|
+
const fakeTypeScriptFile = `return ${customStagesString}`;
|
|
227
|
+
const result = tstl.transpileString(fakeTypeScriptFile, {
|
|
228
|
+
noHeader: true,
|
|
229
|
+
});
|
|
230
|
+
if (result.file === undefined || result.file.lua === undefined) {
|
|
231
|
+
fatalError("Failed to convert the JSON metadata for the custom stages to a Lua file.");
|
|
232
|
+
}
|
|
233
|
+
return result.file.lua;
|
|
234
|
+
}
|
|
235
|
+
/** We combine all of the custom stages together and add them to "00.special rooms.xml". */
|
|
236
|
+
function combineCustomStageXMLs(customStagesTSConfig, verbose) {
|
|
237
|
+
let allRooms = "";
|
|
238
|
+
for (const customStageTSConfig of customStagesTSConfig) {
|
|
239
|
+
const xmlPath = path.resolve(CWD, customStageTSConfig.xmlPath);
|
|
240
|
+
if (!isFile(xmlPath)) {
|
|
241
|
+
fatalError(`${chalk.red("Failed to find the custom stage XML file at:")} ${chalk.red(xmlPath)}`);
|
|
242
|
+
}
|
|
243
|
+
const xmlContents = readFile(xmlPath);
|
|
244
|
+
// It is easier to work with the XML files as text rather than converting it to JSON and then
|
|
245
|
+
// converting it back to XML.
|
|
246
|
+
const lines = xmlContents.trim().split("\n");
|
|
247
|
+
// Remove the first line of "<?xml version="1.0" ?>".
|
|
248
|
+
lines.shift();
|
|
249
|
+
// Remove the second line of "<rooms>".
|
|
250
|
+
lines.shift();
|
|
251
|
+
// Remove the last line of "</rooms>".
|
|
252
|
+
lines.pop();
|
|
253
|
+
// Change the variants
|
|
254
|
+
for (let i = 0; i < lines.length; i++) {
|
|
255
|
+
const line = lines[i];
|
|
256
|
+
if (line === undefined) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (!line.includes("<room")) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const match = line.match(VARIANT_REGEX);
|
|
263
|
+
if (match === null || match.groups === undefined) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const baseVariantString = match.groups["variant"];
|
|
267
|
+
if (baseVariantString === undefined) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const baseVariant = parseIntSafe(baseVariantString);
|
|
271
|
+
if (baseVariant === undefined) {
|
|
272
|
+
fatalError(`Failed to parse the variant of one of the custom stage rooms: ${baseVariantString}`);
|
|
273
|
+
}
|
|
274
|
+
const roomVariantPrefix = customStageTSConfig.roomVariantPrefix * ROOM_VARIANT_MULTIPLIER;
|
|
275
|
+
const variant = roomVariantPrefix + baseVariant;
|
|
276
|
+
const newLine = line
|
|
277
|
+
.replace(VARIANT_REGEX, ` variant="${variant}"`)
|
|
278
|
+
.replace(WEIGHT_REGEX, ' weight="0.0"');
|
|
279
|
+
lines[i] = newLine;
|
|
280
|
+
}
|
|
281
|
+
const modifiedRooms = lines.join("\n");
|
|
282
|
+
allRooms += modifiedRooms;
|
|
283
|
+
}
|
|
284
|
+
const combinedXMLFile = `
|
|
285
|
+
<?xml version="1.0" ?>
|
|
286
|
+
<rooms>
|
|
287
|
+
${allRooms}
|
|
288
|
+
</rooms>
|
|
289
|
+
`.trim();
|
|
290
|
+
const contentRoomsDir = path.join(MOD_SOURCE_PATH, "content", "rooms");
|
|
291
|
+
if (!isFile(contentRoomsDir)) {
|
|
292
|
+
makeDirectory(contentRoomsDir);
|
|
293
|
+
}
|
|
294
|
+
const specialRoomsXMLPath = path.join(contentRoomsDir, "00.special rooms.xml");
|
|
295
|
+
writeFile(specialRoomsXMLPath, combinedXMLFile);
|
|
296
|
+
// Convert the XML file to an STB file, which is the format actually read by the game.
|
|
297
|
+
execExe(XML_CONVERTER_PATH, [specialRoomsXMLPath], verbose, contentRoomsDir);
|
|
298
|
+
}
|