mirror of
https://github.com/open-goal/opengoal-vscode.git
synced 2024-10-19 20:47:37 -04:00
decomp: add a feature that compares two function bodies and if they are the same, copies the name changes (#333)
This commit is contained in:
parent
b3ce6e7ef4
commit
4cc9e61c33
|
@ -139,6 +139,10 @@
|
|||
"command": "opengoal.decomp.misc.batchRenameUnnamedVars",
|
||||
"title": "OpenGOAL - Misc - Batch Rename Unnamed Vars"
|
||||
},
|
||||
{
|
||||
"command": "opengoal.decomp.misc.compareFuncWithJak2",
|
||||
"title": "OpenGOAL - Misc - Compare Func with Jak 2"
|
||||
},
|
||||
{
|
||||
"command": "opengoal.decomp.typeSearcher.open",
|
||||
"title": "OpenGOAL - Misc - Type Searcher"
|
||||
|
|
|
@ -15,6 +15,11 @@ import {
|
|||
import { activateDecompTypeSearcher } from "./type-searcher/type-searcher";
|
||||
import { updateTypeCastSuggestions } from "./type-caster";
|
||||
import { glob } from "fast-glob";
|
||||
import {
|
||||
getFuncBodyFromPosition,
|
||||
getFuncNameFromPosition,
|
||||
} from "../languages/ir2/ir2-utils";
|
||||
import { copyVarCastsFromOneGameToAnother } from "./utils";
|
||||
|
||||
const execFileAsync = util.promisify(execFile);
|
||||
const execAsync = util.promisify(exec);
|
||||
|
@ -155,10 +160,17 @@ async function checkDecompilerPath(): Promise<string | undefined> {
|
|||
}
|
||||
|
||||
async function decompFiles(
|
||||
decompConfig: string,
|
||||
gameName: GameName,
|
||||
fileNames: string[],
|
||||
omitVariableCasts: boolean = false,
|
||||
) {
|
||||
const decompConfig = getDecompilerConfig(gameName);
|
||||
if (decompConfig === undefined) {
|
||||
await vscode.window.showErrorMessage(
|
||||
`OpenGOAL - Can't decompile no ${gameName.toString} config selected`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (fileNames.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -180,8 +192,16 @@ async function decompFiles(
|
|||
"--version",
|
||||
getDecompilerConfigVersion(gameName),
|
||||
"--config-override",
|
||||
`{"decompile_code": true, "print_cfgs": true, "levels_extract": false, "allowed_objects": [${allowed_objects}]}`,
|
||||
];
|
||||
if (omitVariableCasts) {
|
||||
args.push(
|
||||
`{"decompile_code": true, "print_cfgs": true, "levels_extract": false, "ignore_var_name_casts": true,"allowed_objects": [${allowed_objects}]}`,
|
||||
);
|
||||
} else {
|
||||
args.push(
|
||||
`{"decompile_code": true, "print_cfgs": true, "levels_extract": false, "allowed_objects": [${allowed_objects}]}`,
|
||||
);
|
||||
}
|
||||
const { stdout, stderr } = await execFileAsync(decompilerPath, args, {
|
||||
encoding: "utf8",
|
||||
cwd: getProjectRoot()?.fsPath,
|
||||
|
@ -218,12 +238,10 @@ async function getValidObjectNames(gameName: string) {
|
|||
for (const obj of objs) {
|
||||
const is_tpage = obj[0].includes("tpage");
|
||||
const is_art_file = obj[0].endsWith("-ag");
|
||||
if (obj[2] == 4 || obj[2] == 5) {
|
||||
if (!is_tpage && !is_art_file) {
|
||||
names.push(obj[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
|
@ -268,16 +286,7 @@ async function decompSpecificFile() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Determine what decomp config to use
|
||||
const decompConfig = getDecompilerConfig(gameName);
|
||||
if (decompConfig === undefined) {
|
||||
await vscode.window.showErrorMessage(
|
||||
`OpenGOAL - Can't decompile no ${gameName.toString} config selected`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await decompFiles(decompConfig, gameName, [fileName]);
|
||||
await decompFiles(gameName, [fileName]);
|
||||
}
|
||||
|
||||
async function decompCurrentFile() {
|
||||
|
@ -307,15 +316,8 @@ async function decompCurrentFile() {
|
|||
);
|
||||
return;
|
||||
}
|
||||
const decompConfig = getDecompilerConfig(gameName);
|
||||
if (decompConfig === undefined) {
|
||||
await vscode.window.showErrorMessage(
|
||||
`OpenGOAL - Can't decompile no ${gameName.toString} config selected`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await decompFiles(decompConfig, gameName, [fileName]);
|
||||
await decompFiles(gameName, [fileName]);
|
||||
}
|
||||
|
||||
async function decompAllActiveFiles() {
|
||||
|
@ -356,35 +358,13 @@ async function decompAllActiveFiles() {
|
|||
jak3ObjectNames = [...new Set(jak3ObjectNames)];
|
||||
|
||||
if (jak1ObjectNames.length > 0) {
|
||||
const jak1Config = getDecompilerConfig(GameName.Jak1);
|
||||
if (jak1Config === undefined) {
|
||||
await vscode.window.showErrorMessage(
|
||||
"OpenGOAL - Can't decompile, no Jak 1 config selected",
|
||||
);
|
||||
return;
|
||||
await decompFiles(GameName.Jak1, jak1ObjectNames);
|
||||
}
|
||||
await decompFiles(jak1Config, GameName.Jak1, jak1ObjectNames);
|
||||
}
|
||||
|
||||
if (jak2ObjectNames.length > 0) {
|
||||
const jak2Config = getDecompilerConfig(GameName.Jak2);
|
||||
if (jak2Config === undefined) {
|
||||
await vscode.window.showErrorMessage(
|
||||
"OpenGOAL - Can't decompile, no Jak 2 config selected",
|
||||
);
|
||||
return;
|
||||
}
|
||||
await decompFiles(jak2Config, GameName.Jak2, jak2ObjectNames);
|
||||
await decompFiles(GameName.Jak2, jak2ObjectNames);
|
||||
}
|
||||
if (jak3ObjectNames.length > 0) {
|
||||
const jak3Config = getDecompilerConfig(GameName.Jak3);
|
||||
if (jak3Config === undefined) {
|
||||
await vscode.window.showErrorMessage(
|
||||
"OpenGOAL - Can't decompile, no Jak 3 config selected",
|
||||
);
|
||||
return;
|
||||
}
|
||||
await decompFiles(jak3Config, GameName.Jak3, jak3ObjectNames);
|
||||
await decompFiles(GameName.Jak3, jak3ObjectNames);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -534,6 +514,111 @@ async function updateReferenceTest() {
|
|||
});
|
||||
}
|
||||
|
||||
async function compareFunctionWithJak2() {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor || !editor.document === undefined) {
|
||||
await vscode.window.showErrorMessage(
|
||||
"No active file open, can't compare decompiler output!",
|
||||
);
|
||||
return;
|
||||
}
|
||||
let fileName = path.basename(editor.document.fileName);
|
||||
if (!fileName.match(/.*_ir2\.asm/)) {
|
||||
await vscode.window.showErrorMessage(
|
||||
"Current file is not a valid IR2 file, can't compare!",
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
fileName = fileName.split("_ir2.asm")[0];
|
||||
}
|
||||
|
||||
// 0. Determine the current function we are interested in comparing
|
||||
const funcName = getFuncNameFromPosition(
|
||||
editor.document,
|
||||
editor.selection.start,
|
||||
);
|
||||
if (funcName === undefined) {
|
||||
await vscode.window.showErrorMessage(
|
||||
"Couldn't determine function name to compare with jak 2!",
|
||||
);
|
||||
return;
|
||||
}
|
||||
// 1. Run the decompiler on the same file in jak 2 without variable names
|
||||
await decompFiles(GameName.Jak2, [fileName], true);
|
||||
// 2. Go grab that file's contents, find the same function and grab it, cut out the docstring if it's there (and save it)
|
||||
const decompiledOutput = (
|
||||
await fs.readFile(
|
||||
path.join(
|
||||
getProjectRoot()?.fsPath,
|
||||
"decompiler_out",
|
||||
"jak2",
|
||||
`${fileName}_ir2.asm`,
|
||||
),
|
||||
)
|
||||
)
|
||||
.toString()
|
||||
.split("\n");
|
||||
let foundFunc = false;
|
||||
let foundFuncBody = false;
|
||||
const funcBody = [];
|
||||
const docstring = [];
|
||||
for (const line of decompiledOutput) {
|
||||
if (line.includes(`; .function ${funcName}`)) {
|
||||
foundFunc = true;
|
||||
continue;
|
||||
}
|
||||
if (foundFunc && line.includes(";;-*-OpenGOAL-Start-*-")) {
|
||||
foundFuncBody = true;
|
||||
continue;
|
||||
}
|
||||
if (foundFuncBody) {
|
||||
if (line.includes(";;-*-OpenGOAL-End-*-")) {
|
||||
break;
|
||||
}
|
||||
if (line.trim() === ``) {
|
||||
continue;
|
||||
}
|
||||
// NOTE - this will fail with functions with multi-line signatures
|
||||
if (
|
||||
funcBody.length === 1 &&
|
||||
(line.trim().startsWith('"') || !line.trim().startsWith("("))
|
||||
) {
|
||||
docstring.push(line.trimEnd());
|
||||
continue;
|
||||
}
|
||||
funcBody.push(line.trimEnd());
|
||||
}
|
||||
}
|
||||
// 3. Compare the two, if they match, then copy over any var-name changes and put the docstring in the clipboard
|
||||
const jak3FuncBody = getFuncBodyFromPosition(
|
||||
editor.document,
|
||||
editor.selection.start,
|
||||
);
|
||||
if (jak3FuncBody === undefined) {
|
||||
await vscode.window.showErrorMessage(
|
||||
"Couldn't determine function body in jak 3!",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (funcBody.join("\n") === jak3FuncBody.join("\n")) {
|
||||
// Update var casts
|
||||
await copyVarCastsFromOneGameToAnother(
|
||||
editor.document,
|
||||
GameName.Jak2,
|
||||
GameName.Jak3,
|
||||
funcName,
|
||||
);
|
||||
await vscode.window.showInformationMessage(
|
||||
"Function bodies match! Docstring copied to clipboard if it was found.",
|
||||
);
|
||||
if (docstring.length > 0) {
|
||||
await vscode.env.clipboard.writeText(docstring.join("\n"));
|
||||
}
|
||||
} else {
|
||||
await vscode.window.showWarningMessage("Function bodies don't match!");
|
||||
}
|
||||
}
|
||||
|
||||
export async function activateDecompTools() {
|
||||
// no color support :( - https://github.com/microsoft/vscode/issues/571
|
||||
channel = vscode.window.createOutputChannel(
|
||||
|
@ -580,6 +665,12 @@ export async function activateDecompTools() {
|
|||
updateReferenceTest,
|
||||
),
|
||||
);
|
||||
getExtensionContext().subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
"opengoal.decomp.misc.compareFuncWithJak2",
|
||||
compareFunctionWithJak2,
|
||||
),
|
||||
);
|
||||
|
||||
activateDecompTypeSearcher();
|
||||
}
|
||||
|
|
|
@ -7,6 +7,30 @@ import { ArgumentMeta } from "../languages/opengoal/opengoal-tools";
|
|||
import { determineGameFromPath, GameName } from "../utils/file-utils";
|
||||
import { getWorkspaceFolderByName } from "../utils/workspace";
|
||||
|
||||
export function getCastFilePathForGame(
|
||||
projectRoot: vscode.Uri,
|
||||
gameName: GameName,
|
||||
fileName: string,
|
||||
): string {
|
||||
const config = getConfig();
|
||||
if (gameName == GameName.Jak1) {
|
||||
return vscode.Uri.joinPath(
|
||||
projectRoot,
|
||||
`decompiler/config/jak1/${config.jak1DecompConfigVersion}/${fileName}`,
|
||||
).fsPath;
|
||||
} else if (gameName == GameName.Jak2) {
|
||||
return vscode.Uri.joinPath(
|
||||
projectRoot,
|
||||
`decompiler/config/jak2/${config.jak2DecompConfigVersion}/${fileName}`,
|
||||
).fsPath;
|
||||
} else {
|
||||
return vscode.Uri.joinPath(
|
||||
projectRoot,
|
||||
`decompiler/config/jak3/${config.jak3DecompConfigVersion}/${fileName}`,
|
||||
).fsPath;
|
||||
}
|
||||
}
|
||||
|
||||
export function getCastFileData(
|
||||
projectRoot: vscode.Uri,
|
||||
document: vscode.TextDocument,
|
||||
|
@ -16,24 +40,20 @@ export function getCastFileData(
|
|||
if (gameName === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const config = getConfig();
|
||||
let castFilePath = "";
|
||||
if (gameName == GameName.Jak1) {
|
||||
castFilePath = vscode.Uri.joinPath(
|
||||
projectRoot,
|
||||
`decompiler/config/jak1/${config.jak1DecompConfigVersion}/${fileName}`,
|
||||
).fsPath;
|
||||
} else if (gameName == GameName.Jak2) {
|
||||
castFilePath = vscode.Uri.joinPath(
|
||||
projectRoot,
|
||||
`decompiler/config/jak2/${config.jak2DecompConfigVersion}/${fileName}`,
|
||||
).fsPath;
|
||||
} else if (gameName == GameName.Jak3) {
|
||||
castFilePath = vscode.Uri.joinPath(
|
||||
projectRoot,
|
||||
`decompiler/config/jak3/${config.jak3DecompConfigVersion}/${fileName}`,
|
||||
).fsPath;
|
||||
const castFilePath = getCastFilePathForGame(projectRoot, gameName, fileName);
|
||||
if (!existsSync(castFilePath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return parse(readFileSync(castFilePath).toString(), undefined, true);
|
||||
}
|
||||
|
||||
export function getCastFileDataForGame(
|
||||
projectRoot: vscode.Uri,
|
||||
gameName: GameName,
|
||||
fileName: string,
|
||||
): any | undefined {
|
||||
const castFilePath = getCastFilePathForGame(projectRoot, gameName, fileName);
|
||||
if (!existsSync(castFilePath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -268,7 +288,7 @@ export async function bulkUpdateVarCasts(
|
|||
}
|
||||
|
||||
// Write out cast file change
|
||||
const configDir = await getDecompilerConfigDirectory(document.uri);
|
||||
const configDir = getDecompilerConfigDirectory(document.uri);
|
||||
if (configDir === undefined) {
|
||||
return;
|
||||
}
|
||||
|
@ -276,3 +296,48 @@ export async function bulkUpdateVarCasts(
|
|||
|
||||
writeFileSync(filePath, stringify(varNameData, null, 2));
|
||||
}
|
||||
|
||||
export async function copyVarCastsFromOneGameToAnother(
|
||||
document: vscode.TextDocument,
|
||||
oldGame: GameName,
|
||||
newGame: GameName,
|
||||
funcName: string,
|
||||
) {
|
||||
const projectRoot = getWorkspaceFolderByName("jak-project");
|
||||
if (projectRoot === undefined) {
|
||||
vscode.window.showErrorMessage(
|
||||
"OpenGOAL - Unable to locate 'jak-project' workspace folder",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const oldVarNameData = getCastFileDataForGame(
|
||||
projectRoot,
|
||||
oldGame,
|
||||
"var_names.jsonc",
|
||||
);
|
||||
if (oldVarNameData === undefined) {
|
||||
return;
|
||||
}
|
||||
const newVarNameData = getCastFileDataForGame(
|
||||
projectRoot,
|
||||
newGame,
|
||||
"var_names.jsonc",
|
||||
);
|
||||
if (newVarNameData === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(funcName in oldVarNameData)) {
|
||||
return;
|
||||
}
|
||||
newVarNameData[funcName] = oldVarNameData[funcName];
|
||||
// Write out cast file change
|
||||
const configDir = getDecompilerConfigDirectory(document.uri);
|
||||
if (configDir === undefined) {
|
||||
return;
|
||||
}
|
||||
const filePath = join(configDir, "var_names.jsonc");
|
||||
|
||||
writeFileSync(filePath, stringify(newVarNameData, null, 2));
|
||||
}
|
||||
|
|
|
@ -21,10 +21,10 @@ export function insideGoalCodeInIR(
|
|||
return false;
|
||||
}
|
||||
|
||||
export async function getFuncNameFromPosition(
|
||||
export function getFuncNameFromPosition(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
): Promise<string | undefined> {
|
||||
): string | undefined {
|
||||
const funcNameRegex = /; \.function (.*).*/g;
|
||||
for (let i = position.line; i >= 0; i--) {
|
||||
const line = document.lineAt(i).text;
|
||||
|
@ -33,9 +33,7 @@ export async function getFuncNameFromPosition(
|
|||
return matches[0][1].toString();
|
||||
}
|
||||
}
|
||||
await vscode.window.showErrorMessage(
|
||||
"Couldn't determine function or method name",
|
||||
);
|
||||
vscode.window.showErrorMessage("Couldn't determine function or method name");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
@ -45,3 +43,52 @@ export async function getFuncNameFromSelection(
|
|||
): Promise<string | undefined> {
|
||||
return await getFuncNameFromPosition(document, selection.start);
|
||||
}
|
||||
|
||||
export function getFuncBodyFromPosition(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
): string[] | undefined {
|
||||
let funcName = undefined;
|
||||
let funcNamePosition = 0;
|
||||
const funcNameRegex = /; \.function (.*).*/g;
|
||||
for (let i = position.line; i >= 0; i--) {
|
||||
const line = document.lineAt(i).text;
|
||||
const matches = [...line.matchAll(funcNameRegex)];
|
||||
if (matches.length == 1) {
|
||||
funcName = matches[0][1].toString();
|
||||
funcNamePosition = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (funcName === undefined) {
|
||||
vscode.window.showErrorMessage(
|
||||
"Couldn't determine function or method name",
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
// Find the function body
|
||||
let foundFunc = false;
|
||||
let foundFuncBody = false;
|
||||
const funcBody = [];
|
||||
for (let i = funcNamePosition; i <= document.lineCount; i++) {
|
||||
const line = document.lineAt(i).text;
|
||||
if (line.includes(`; .function ${funcName}`)) {
|
||||
foundFunc = true;
|
||||
continue;
|
||||
}
|
||||
if (foundFunc && line.includes(";;-*-OpenGOAL-Start-*-")) {
|
||||
foundFuncBody = true;
|
||||
continue;
|
||||
}
|
||||
if (foundFuncBody) {
|
||||
if (line.includes(";;-*-OpenGOAL-End-*-")) {
|
||||
break;
|
||||
}
|
||||
if (line.trim() === ``) {
|
||||
continue;
|
||||
}
|
||||
funcBody.push(line.trimEnd());
|
||||
}
|
||||
}
|
||||
return funcBody;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue