From 9c967559cb9fbbe5b8fb8d4cc4955ddb772b37ff Mon Sep 17 00:00:00 2001 From: Tyler Wilding Date: Mon, 1 Aug 2022 22:31:37 -0400 Subject: [PATCH] decomp: add tooling to semi-automatically add casts --- package-lock.json | 91 ++++ package.json | 44 +- src/config/config.ts | 24 + src/decomp/decomp-tools.ts | 18 +- src/decomp/type-caster.ts | 584 ++++++++++++++++++++++ src/extension.ts | 6 +- src/utils/{FileUtils.ts => file-utils.ts} | 24 + 7 files changed, 771 insertions(+), 20 deletions(-) create mode 100644 src/decomp/type-caster.ts rename src/utils/{FileUtils.ts => file-utils.ts} (65%) diff --git a/package-lock.json b/package-lock.json index cb1ffeb..86bfcf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.2", "license": "ISC", "dependencies": { + "comment-json": "^4.2.2", "follow-redirects": "^1.15.1", "glob": "^8.0.3", "open": "^8.4.0", @@ -397,6 +398,11 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==" + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -475,11 +481,31 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/comment-json": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.2.tgz", + "integrity": "sha512-H8T+kl3nZesZu41zO2oNXIJWojNeK3mHxCLrsBNu6feksBXsgb+PtYz5daP5P86A0F3sz3840KVYehr04enISQ==", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -698,6 +724,18 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", @@ -979,6 +1017,14 @@ "node": ">=8" } }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/ignore": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", @@ -1329,6 +1375,14 @@ "url": "https://github.com/sponsors/mysticatea" } }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -1892,6 +1946,11 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==" + }, "array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -1952,11 +2011,28 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "comment-json": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.2.tgz", + "integrity": "sha512-H8T+kl3nZesZu41zO2oNXIJWojNeK3mHxCLrsBNu6feksBXsgb+PtYz5daP5P86A0F3sz3840KVYehr04enISQ==", + "requires": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2117,6 +2193,11 @@ "eslint-visitor-keys": "^3.3.0" } }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, "esquery": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", @@ -2332,6 +2413,11 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==" + }, "ignore": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", @@ -2578,6 +2664,11 @@ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==" + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", diff --git a/package.json b/package.json index 5a17169..626f903 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,9 @@ "typescript": "^4.6.3" }, "dependencies": { + "comment-json": "^4.2.2", "follow-redirects": "^1.15.1", "glob": "^8.0.3", - "open": "^8.4.0", "vscode-languageclient": "^8.0.1" }, "activationEvents": [ @@ -52,7 +52,11 @@ "onCommand:opengoal.decomp.decompileCurrentFile", "onCommand:opengoal.decomp.toggleAutoDecompilation", "onCommand:opengoal.decomp.updateSourceFile", - "onCommand:opengoal.decomp.updateReferenceTest" + "onCommand:opengoal.decomp.updateReferenceTest", + "opengoal.decomp.casts.repeatLast", + "opengoal.decomp.casts.labelCastSelection", + "opengoal.decomp.casts.stackCastSelection", + "opengoal.decomp.casts.typeCastSelection" ], "contributes": { "commands": [ @@ -88,6 +92,26 @@ "command": "opengoal.decomp.updateReferenceTest", "title": "OpenGOAL - Copy Decompilation to Reference Tests" }, + { + "command": "opengoal.decomp.casts.repeatLast", + "title": "OpenGOAL - Casts - Repeat Last" + }, + { + "command": "opengoal.decomp.casts.castSelection", + "title": "OpenGOAL - Casts - Add Cast to Selection" + }, + { + "command": "opengoal.decomp.casts.labelCastSelection", + "title": "OpenGOAL - Casts - Add Label Cast to Selection" + }, + { + "command": "opengoal.decomp.casts.stackCastSelection", + "title": "OpenGOAL - Casts - Add Stack Cast to Selection" + }, + { + "command": "opengoal.decomp.casts.typeCastSelection", + "title": "OpenGOAL - Casts - Add Type Cast to Selection" + }, { "command": "opengoal.lsp.start", "title": "OpenGOAL - LSP - Start" @@ -150,6 +174,22 @@ ], "default": null, "description": "Config to use for decompiling jak 2 related files" + }, + "opengoal.decompilerJak1ConfigDirectory": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Directory containing cast files to use for decompiling jak 1 related files" + }, + "opengoal.decompilerJak2ConfigDirectory": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Directory containing cast files to use for decompiling jak 2 related files" } } }, diff --git a/src/config/config.ts b/src/config/config.ts index 36f73d0..99a1306 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -12,6 +12,12 @@ export function getConfig() { decompilerPath: configOptions.get("decompilerPath"), jak1DecompConfig: configOptions.get("decompilerJak1Config"), jak2DecompConfig: configOptions.get("decompilerJak2Config"), + decompilerJak1ConfigDirectory: configOptions.get( + "decompilerJak1ConfigDirectory" + ), + decompilerJak2ConfigDirectory: configOptions.get( + "decompilerJak2ConfigDirectory" + ), }; } @@ -59,3 +65,21 @@ export async function updateJak2DecompConfig(config: string) { vscode.ConfigurationTarget.Global ); } + +export async function updateJak1DecompConfigDirectory(dir: string) { + const userConfig = vscode.workspace.getConfiguration(); + await userConfig.update( + "opengoal.decompilerJak1ConfigDirectory", + dir, + vscode.ConfigurationTarget.Global + ); +} + +export async function updateJak2DecompConfigDirectory(dir: string) { + const userConfig = vscode.workspace.getConfiguration(); + await userConfig.update( + "opengoal.decompilerJak2ConfigDirectory", + dir, + vscode.ConfigurationTarget.Global + ); +} diff --git a/src/decomp/decomp-tools.ts b/src/decomp/decomp-tools.ts index 09f1444..570fab7 100644 --- a/src/decomp/decomp-tools.ts +++ b/src/decomp/decomp-tools.ts @@ -1,7 +1,7 @@ import { execFile } from "child_process"; import { existsSync, promises as fs } from "fs"; import * as vscode from "vscode"; -import { openFile } from "../utils/FileUtils"; +import { determineGameFromPath, GameName, openFile } from "../utils/file-utils"; import { open_in_pdf } from "./man-page"; import * as util from "util"; import { @@ -81,22 +81,6 @@ function defaultDecompPath() { } } -enum GameName { - Jak1, - Jak2, -} - -async function determineGameFromPath( - path: vscode.Uri -): Promise { - if (path.fsPath.includes("jak1")) { - return GameName.Jak1; - } else if (path.fsPath.includes("jak2")) { - return GameName.Jak2; - } - return undefined; -} - async function promptUserToSelectConfig( projectRoot: vscode.Uri ): Promise { diff --git a/src/decomp/type-caster.ts b/src/decomp/type-caster.ts new file mode 100644 index 0000000..ccc32da --- /dev/null +++ b/src/decomp/type-caster.ts @@ -0,0 +1,584 @@ +// [inclusive, exclusive] + +import { getExtensionContext } from "../context"; +import * as vscode from "vscode"; +import { basename, join } from "path"; +import { getWorkspaceFolderByName } from "../utils/workspace"; +import { existsSync, readFileSync, writeFileSync } from "fs"; +import { parse, stringify } from "comment-json"; +import { + getConfig, + updateJak1DecompConfigDirectory, + updateJak2DecompConfigDirectory, +} from "../config/config"; +import { + determineGameFromPath, + GameName, + getDirectoriesInDir, +} from "../utils/file-utils"; + +enum CastKind { + Label, + Stack, + TypeCast, +} + +let lastCastKind: CastKind | undefined; +let lastLabelCastType: string | undefined; +let lastLabelCastSize: number | undefined; +let lastStackCastType: string | undefined; +let lastTypeCastRegister: string | undefined; +let lastTypeCastType: string | undefined; + +const opNumRegex = /.*;; \[\s*(\d+)\]/g; +const registerRegex = /[a|s|t|v]\d|gp|fp|r0|ra/g; +const funcNameRegex = /; \.function (.*).*/g; +const stackOffsetRegex = /sp, (\d+)/g; +const labelRefRegex = /(L\d+).*;;/g; + +class CastContext { + startOp: number; + endOp: number | undefined; + constructor(start: number, end?: number) { + this.startOp = start; + this.endOp = end; + } +} + +async function promptUserToSelectConfigDirectory( + projectRoot: vscode.Uri +): Promise { + // Get all `.jsonc` files in ./decompiler/config + const dirs = await getDirectoriesInDir( + vscode.Uri.joinPath(projectRoot, "decompiler/config").fsPath + ); + return await vscode.window.showQuickPick(dirs, { + title: "Config?", + }); +} + +async function getDecompilerConfigDirectory( + activeFile: vscode.Uri +): Promise { + const projectRoot = getWorkspaceFolderByName("jak-project"); + if (projectRoot === undefined) { + vscode.window.showErrorMessage( + "OpenGOAL - Unable to locate 'jak-project' workspace folder" + ); + return undefined; + } + + const config = getConfig(); + const gameName = await determineGameFromPath(activeFile); + if (gameName == GameName.Jak1) { + if ( + config.decompilerJak1ConfigDirectory === undefined || + !existsSync(config.decompilerJak1ConfigDirectory) + ) { + const selection = await promptUserToSelectConfigDirectory(projectRoot); + if (selection === undefined) { + vscode.window.showErrorMessage( + "OpenGOAL - Can't cast without knowing where to store them!" + ); + return undefined; + } + await updateJak1DecompConfigDirectory(selection); + return vscode.Uri.joinPath(projectRoot, "decompiler/config/", selection) + .fsPath; + } else { + return vscode.Uri.joinPath( + projectRoot, + "decompiler/config/", + config.decompilerJak1ConfigDirectory + ).fsPath; + } + } else if (gameName == GameName.Jak2) { + if ( + config.decompilerJak2ConfigDirectory === undefined || + !existsSync(config.decompilerJak2ConfigDirectory) + ) { + const selection = await promptUserToSelectConfigDirectory(projectRoot); + if (selection === undefined) { + vscode.window.showErrorMessage( + "OpenGOAL - Can't cast without knowing where to store them!" + ); + return undefined; + } + await updateJak2DecompConfigDirectory(selection); + return vscode.Uri.joinPath(projectRoot, "decompiler/config/", selection) + .fsPath; + } else { + return vscode.Uri.joinPath( + projectRoot, + "decompiler/config/", + config.decompilerJak2ConfigDirectory + ).fsPath; + } + } +} + +async function getOpNumber(line: string): Promise { + const matches = [...line.matchAll(opNumRegex)]; + if (matches.length == 1) { + return parseInt(matches[0][1].toString()); + } + await vscode.window.showErrorMessage("Couldn't determine operation number"); + return undefined; +} + +async function getFuncName( + document: vscode.TextDocument, + selection: vscode.Selection +): Promise { + for (let i = selection.start.line; i >= 0; i--) { + const line = document.lineAt(i).text; + const matches = [...line.matchAll(funcNameRegex)]; + if (matches.length == 1) { + return matches[0][1].toString(); + } + } + await vscode.window.showErrorMessage( + "Couldn't determine function or method name" + ); + return undefined; +} + +async function getLabelReference(line: string): Promise { + const matches = [...line.matchAll(labelRefRegex)]; + if (matches.length == 1) { + return matches[0][1].toString(); + } + await vscode.window.showErrorMessage("Couldn't determine label reference"); + return undefined; +} + +async function applyLabelCast( + editor: vscode.TextEditor, + objectName: string, + labelRef: string, + castToType: string, + pointerSize?: number +) { + const configDir = await getDecompilerConfigDirectory(editor.document.uri); + if (configDir === undefined) { + return; + } + const filePath = join(configDir, "label_types.jsonc"); + + const json: any = parse(readFileSync(filePath).toString()); + // Add our new entry + if (objectName in json) { + if (pointerSize === undefined) { + json[objectName].push([labelRef, castToType]); + } else { + json[objectName].push([labelRef, castToType, pointerSize]); + } + } else { + if (pointerSize === undefined) { + json[objectName] = [[labelRef, castToType]]; + } else { + json[objectName] = [[labelRef, castToType, pointerSize]]; + } + } + + writeFileSync(filePath, stringify(json, null, 2)); +} + +async function validActiveFile(editor: vscode.TextEditor): Promise { + if (!editor.document === undefined) { + await vscode.window.showErrorMessage( + "No active file open, can't decompile!" + ); + return false; + } + + const fileName = basename(editor.document.fileName); + if (!fileName.match(/.*_ir2\.asm/)) { + await vscode.window.showErrorMessage( + "Current file is not a valid IR2 file." + ); + return false; + } + return true; +} + +async function labelCastSelection() { + const editor = vscode.window.activeTextEditor; + if (editor === undefined || !validActiveFile(editor)) { + return; + } + + const objectName = basename(editor.document.fileName).split("_ir2.asm")[0]; + + // Get the stack index + const labelRef = await getLabelReference( + editor.document.lineAt(editor.selection.start.line).text + ); + if (labelRef === undefined) { + return; + } + + // Get what we should cast to + const castToType = await vscode.window.showInputBox({ + title: "Cast to Type?", + }); + if (castToType === undefined) { + await vscode.window.showErrorMessage("Can't cast if no type is provided"); + return; + } + + // If the label is a pointer, ask for a size + let pointerSize = undefined; + if (castToType.includes("pointer")) { + pointerSize = await vscode.window.showInputBox({ + title: "Pointer Size?", + }); + if (pointerSize === undefined) { + await vscode.window.showErrorMessage("Provide a pointer size!"); + return; + } + pointerSize = parseInt(pointerSize); + } + + // Finally, do the cast! + await applyLabelCast(editor, objectName, labelRef, castToType, pointerSize); + + lastCastKind = CastKind.Label; + lastLabelCastType = castToType; + lastLabelCastSize = pointerSize; +} + +async function getStackOffset(line: string): Promise { + const matches = [...line.matchAll(stackOffsetRegex)]; + if (matches.length == 1) { + return parseInt(matches[0][1].toString()); + } + await vscode.window.showErrorMessage("Couldn't determine stack offset"); + return undefined; +} + +async function applyStackCast( + editor: vscode.TextEditor, + funcName: string, + stackOffset: number, + castToType: string +) { + const configDir = await getDecompilerConfigDirectory(editor.document.uri); + if (configDir === undefined) { + return; + } + const filePath = join(configDir, "stack_structures.jsonc"); + + const json: any = parse(readFileSync(filePath).toString()); + // Add our new entry + if (funcName in json) { + json[funcName].push([stackOffset, castToType]); + } else { + json[funcName] = [[stackOffset, castToType]]; + } + + writeFileSync(filePath, stringify(json, null, 2)); +} + +async function stackCastSelection() { + const editor = vscode.window.activeTextEditor; + if (editor === undefined || !validActiveFile(editor)) { + return; + } + + // Get the relevant function/method name + const funcName = await getFuncName(editor.document, editor.selection); + if (funcName === undefined) { + return; + } + + // Get the stack index + const stackOffset = await getStackOffset( + editor.document.lineAt(editor.selection.start.line).text + ); + if (stackOffset === undefined) { + return; + } + + // Get what we should cast to + const castToType = await vscode.window.showInputBox({ + title: "Cast to Type?", + }); + if (castToType === undefined) { + await vscode.window.showErrorMessage("Can't cast if no type is provided"); + return; + } + + // Finally, do the cast! + await applyStackCast(editor, funcName, stackOffset, castToType); + + lastCastKind = CastKind.Stack; + lastStackCastType = castToType; +} + +function getRegisters( + document: vscode.TextDocument, + selection: vscode.Selection +): string[] { + const regSet = new Set(); + for (let i = selection.start.line; i <= selection.end.line; i++) { + const line = document.lineAt(i).text; + const regs = [...line.matchAll(registerRegex)]; + regs.forEach((regMatch) => regSet.add(regMatch.toString())); + } + return Array.from(regSet).sort(); +} + +async function applyTypeCast( + editor: vscode.TextEditor, + funcName: string, + castContext: CastContext, + registerSelection: string, + castToType: string +) { + const configDir = await getDecompilerConfigDirectory(editor.document.uri); + if (configDir === undefined) { + return; + } + const filePath = join(configDir, "type_casts.jsonc"); + + const json: any = parse(readFileSync(filePath).toString()); + // Add our new entry + if (funcName in json) { + if (castContext.endOp === undefined) { + json[funcName].push([castContext.startOp, registerSelection, castToType]); + } else { + json[funcName].push([ + [castContext.startOp, castContext.endOp], + registerSelection, + castToType, + ]); + } + } else { + if (castContext.endOp === undefined) { + json[funcName] = [[castContext.startOp, registerSelection, castToType]]; + } else { + json[funcName] = [ + [ + [castContext.startOp, castContext.endOp], + registerSelection, + castToType, + ], + ]; + } + } + + writeFileSync(filePath, stringify(json, null, 2)); +} + +async function typeCastSelection() { + const editor = vscode.window.activeTextEditor; + if (editor === undefined || !validActiveFile(editor)) { + return; + } + + // Determine the range of the selection + const startOpNum = await getOpNumber( + editor.document.lineAt(editor.selection.start.line).text + ); + if (startOpNum === undefined) { + return; + } + const castContext = new CastContext(startOpNum); + if (!editor.selection.isSingleLine) { + const endOpNum = await getOpNumber( + editor.document.lineAt(editor.selection.end.line).text + ); + if (endOpNum === undefined) { + return; + } + castContext.endOp = endOpNum; + } + + // Get the relevant function/method name + const funcName = await getFuncName(editor.document, editor.selection); + if (funcName === undefined) { + return; + } + + // Get all possible registers in the given range (in this case, just the line) + const registers = getRegisters(editor.document, editor.selection); + if (registers.length == 0) { + await vscode.window.showErrorMessage( + "Found no registers to cast in that selection" + ); + return; + } + + // Get what register should be casted + const registerSelection = await vscode.window.showQuickPick(registers, { + title: "Register to Cast?", + }); + if (registerSelection === undefined) { + await vscode.window.showErrorMessage( + "Can't cast if no register is provided" + ); + return; + } + + // Get what we should cast to + const castToType = await vscode.window.showInputBox({ + title: "Cast to Type?", + }); + if (castToType === undefined) { + await vscode.window.showErrorMessage("Can't cast if no type is provided"); + return; + } + + // Finally, do the cast! + await applyTypeCast( + editor, + funcName, + castContext, + registerSelection, + castToType + ); + + lastCastKind = CastKind.TypeCast; + lastTypeCastRegister = registerSelection; + lastTypeCastType = castToType; +} + +// Execute the same cast as last time (same type, same register) just on a different selection +async function repeatLastCast() { + if (lastCastKind === undefined) { + return; + } + + const editor = vscode.window.activeTextEditor; + if (editor === undefined || !validActiveFile(editor)) { + return; + } + + if (lastCastKind === CastKind.Label) { + const objectName = basename(editor.document.fileName).split("_ir2.asm")[0]; + const labelRef = await getLabelReference( + editor.document.lineAt(editor.selection.start.line).text + ); + if (labelRef === undefined || lastLabelCastType === undefined) { + return; + } + await applyLabelCast( + editor, + objectName, + labelRef, + lastLabelCastType, + lastLabelCastSize + ); + } else if (lastCastKind === CastKind.Stack) { + const funcName = await getFuncName(editor.document, editor.selection); + if (funcName === undefined) { + return; + } + + // Get the stack index + const stackOffset = await getStackOffset( + editor.document.lineAt(editor.selection.start.line).text + ); + if (stackOffset === undefined || lastStackCastType === undefined) { + return; + } + await applyStackCast(editor, funcName, stackOffset, lastStackCastType); + } else if (lastCastKind === CastKind.TypeCast) { + const funcName = await getFuncName(editor.document, editor.selection); + if (funcName === undefined) { + return; + } + const startOpNum = await getOpNumber( + editor.document.lineAt(editor.selection.start.line).text + ); + if (startOpNum === undefined) { + return; + } + const castContext = new CastContext(startOpNum); + if (!editor.selection.isSingleLine) { + const endOpNum = await getOpNumber( + editor.document.lineAt(editor.selection.end.line).text + ); + if (endOpNum === undefined) { + return; + } + castContext.endOp = endOpNum; + } + + if (lastTypeCastRegister === undefined || lastTypeCastType === undefined) { + return; + } + + await applyTypeCast( + editor, + funcName, + castContext, + lastTypeCastRegister, + lastTypeCastType + ); + } +} + +export async function activateTypeCastTools() { + getExtensionContext().subscriptions.push( + vscode.commands.registerCommand( + "opengoal.decomp.casts.labelCastSelection", + labelCastSelection + ) + ); + getExtensionContext().subscriptions.push( + vscode.commands.registerCommand( + "opengoal.decomp.casts.stackCastSelection", + stackCastSelection + ) + ); + getExtensionContext().subscriptions.push( + vscode.commands.registerCommand( + "opengoal.decomp.casts.typeCastSelection", + typeCastSelection + ) + ); + getExtensionContext().subscriptions.push( + vscode.commands.registerCommand( + "opengoal.decomp.casts.repeatLast", + repeatLastCast + ) + ); +} + +// TODO - better handling around upserting casts +// this requires properly handling the CommentArray type instead of building raw arrays so comments are preserved +// const finalEntries = []; +// if (relevantJson !== undefined) { +// // prepare the entry for the upcoming update +// // remove any identical casts / range casts that effect it +// for (const entry of relevantJson) { +// if (entry[1] === registerSelection) { +// if (entry[0] instanceof Array) { +// const [start, end] = entry[0]; +// if (castContext.endOp === undefined) { +// if (castContext.startOp >= start && castContext.startOp < end) { +// continue; +// } +// } else if ( +// (castContext.startOp >= start && castContext.startOp < end) || +// (castContext.endOp > start && castContext.endOp < end) || +// (castContext.startOp >= start && castContext.endOp < end) +// ) { +// continue; +// } +// } else if (castContext.startOp == entry[0]) { +// continue; +// } +// finalEntries.push(entry); +// } +// } +// // Add our new entry +// // TODO - sort by op number (annoying because of the ranges...) +// if (castContext.endOp === undefined) { +// finalEntries.push([castContext.startOp, registerSelection, castToType]); +// } else { +// finalEntries.push([[castContext.startOp, castContext.endOp], registerSelection, castToType]); +// } +// } diff --git a/src/extension.ts b/src/extension.ts index 62582e0..bd69a00 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,10 +5,11 @@ import { setVSIconAssociations, } from "./config/user-settings"; import { PdfCustomProvider } from "./vendor/vscode-pdfviewer/pdfProvider"; -import { switchFile } from "./utils/FileUtils"; +import { switchFile } from "./utils/file-utils"; import { activateDecompTools } from "./decomp/decomp-tools"; import { initContext } from "./context"; import { IRFoldingRangeProvider } from "./languages/ir2-folder"; +import { activateTypeCastTools } from "./decomp/type-caster"; export async function activate(context: vscode.ExtensionContext) { // Init Context @@ -23,6 +24,7 @@ export async function activate(context: vscode.ExtensionContext) { ); activateDecompTools(); + activateTypeCastTools(); // Customized PDF Viewer const provider = new PdfCustomProvider( @@ -41,6 +43,8 @@ export async function activate(context: vscode.ExtensionContext) { ) ); + // TODO - disposable stuff? + // Language Customizations vscode.languages.registerFoldingRangeProvider( { scheme: "file", language: "opengoal-ir" }, diff --git a/src/utils/FileUtils.ts b/src/utils/file-utils.ts similarity index 65% rename from src/utils/FileUtils.ts rename to src/utils/file-utils.ts index dc1f1e9..4665fa5 100644 --- a/src/utils/FileUtils.ts +++ b/src/utils/file-utils.ts @@ -1,8 +1,14 @@ import * as vscode from "vscode"; import * as path from "path"; +import { promises as fs } from "fs"; // TODO - remove "most recent ir2 file, and wire it up here when in an `all-types.gc` file" +export enum GameName { + Jak1, + Jak2, +} + const fileSwitchingAssoc = { "_ir2.asm": "_disasm.gc", "_disasm.gc": "_ir2.asm", @@ -32,3 +38,21 @@ export function openFile(filePath: string | undefined) { } vscode.window.showTextDocument(vscode.Uri.file(filePath)); } + +export async function determineGameFromPath( + path: vscode.Uri +): Promise { + if (path.fsPath.includes("jak1")) { + return GameName.Jak1; + } else if (path.fsPath.includes("jak2")) { + return GameName.Jak2; + } + return undefined; +} + +export async function getDirectoriesInDir(dir: string) { + const dirs = await fs.readdir(dir, { withFileTypes: true }); + return dirs + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); +}