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:
Tyler Wilding 2024-01-25 21:27:02 -05:00 committed by GitHub
parent b3ce6e7ef4
commit 4cc9e61c33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 279 additions and 72 deletions

View file

@ -139,6 +139,10 @@
"command": "opengoal.decomp.misc.batchRenameUnnamedVars", "command": "opengoal.decomp.misc.batchRenameUnnamedVars",
"title": "OpenGOAL - Misc - Batch Rename Unnamed Vars" "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", "command": "opengoal.decomp.typeSearcher.open",
"title": "OpenGOAL - Misc - Type Searcher" "title": "OpenGOAL - Misc - Type Searcher"

View file

@ -15,6 +15,11 @@ import {
import { activateDecompTypeSearcher } from "./type-searcher/type-searcher"; import { activateDecompTypeSearcher } from "./type-searcher/type-searcher";
import { updateTypeCastSuggestions } from "./type-caster"; import { updateTypeCastSuggestions } from "./type-caster";
import { glob } from "fast-glob"; import { glob } from "fast-glob";
import {
getFuncBodyFromPosition,
getFuncNameFromPosition,
} from "../languages/ir2/ir2-utils";
import { copyVarCastsFromOneGameToAnother } from "./utils";
const execFileAsync = util.promisify(execFile); const execFileAsync = util.promisify(execFile);
const execAsync = util.promisify(exec); const execAsync = util.promisify(exec);
@ -155,10 +160,17 @@ async function checkDecompilerPath(): Promise<string | undefined> {
} }
async function decompFiles( async function decompFiles(
decompConfig: string,
gameName: GameName, gameName: GameName,
fileNames: string[], 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) { if (fileNames.length == 0) {
return; return;
} }
@ -180,8 +192,16 @@ async function decompFiles(
"--version", "--version",
getDecompilerConfigVersion(gameName), getDecompilerConfigVersion(gameName),
"--config-override", "--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, { const { stdout, stderr } = await execFileAsync(decompilerPath, args, {
encoding: "utf8", encoding: "utf8",
cwd: getProjectRoot()?.fsPath, cwd: getProjectRoot()?.fsPath,
@ -218,10 +238,8 @@ async function getValidObjectNames(gameName: string) {
for (const obj of objs) { for (const obj of objs) {
const is_tpage = obj[0].includes("tpage"); const is_tpage = obj[0].includes("tpage");
const is_art_file = obj[0].endsWith("-ag"); const is_art_file = obj[0].endsWith("-ag");
if (obj[2] == 4 || obj[2] == 5) { if (!is_tpage && !is_art_file) {
if (!is_tpage && !is_art_file) { names.push(obj[0]);
names.push(obj[0]);
}
} }
} }
return names; return names;
@ -268,16 +286,7 @@ async function decompSpecificFile() {
return; return;
} }
// Determine what decomp config to use await decompFiles(gameName, [fileName]);
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]);
} }
async function decompCurrentFile() { async function decompCurrentFile() {
@ -307,15 +316,8 @@ async function decompCurrentFile() {
); );
return; 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() { async function decompAllActiveFiles() {
@ -356,35 +358,13 @@ async function decompAllActiveFiles() {
jak3ObjectNames = [...new Set(jak3ObjectNames)]; jak3ObjectNames = [...new Set(jak3ObjectNames)];
if (jak1ObjectNames.length > 0) { if (jak1ObjectNames.length > 0) {
const jak1Config = getDecompilerConfig(GameName.Jak1); await decompFiles(GameName.Jak1, jak1ObjectNames);
if (jak1Config === undefined) {
await vscode.window.showErrorMessage(
"OpenGOAL - Can't decompile, no Jak 1 config selected",
);
return;
}
await decompFiles(jak1Config, GameName.Jak1, jak1ObjectNames);
} }
if (jak2ObjectNames.length > 0) { if (jak2ObjectNames.length > 0) {
const jak2Config = getDecompilerConfig(GameName.Jak2); await decompFiles(GameName.Jak2, jak2ObjectNames);
if (jak2Config === undefined) {
await vscode.window.showErrorMessage(
"OpenGOAL - Can't decompile, no Jak 2 config selected",
);
return;
}
await decompFiles(jak2Config, GameName.Jak2, jak2ObjectNames);
} }
if (jak3ObjectNames.length > 0) { if (jak3ObjectNames.length > 0) {
const jak3Config = getDecompilerConfig(GameName.Jak3); await decompFiles(GameName.Jak3, jak3ObjectNames);
if (jak3Config === undefined) {
await vscode.window.showErrorMessage(
"OpenGOAL - Can't decompile, no Jak 3 config selected",
);
return;
}
await decompFiles(jak3Config, 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() { export async function activateDecompTools() {
// no color support :( - https://github.com/microsoft/vscode/issues/571 // no color support :( - https://github.com/microsoft/vscode/issues/571
channel = vscode.window.createOutputChannel( channel = vscode.window.createOutputChannel(
@ -580,6 +665,12 @@ export async function activateDecompTools() {
updateReferenceTest, updateReferenceTest,
), ),
); );
getExtensionContext().subscriptions.push(
vscode.commands.registerCommand(
"opengoal.decomp.misc.compareFuncWithJak2",
compareFunctionWithJak2,
),
);
activateDecompTypeSearcher(); activateDecompTypeSearcher();
} }

View file

@ -7,6 +7,30 @@ import { ArgumentMeta } from "../languages/opengoal/opengoal-tools";
import { determineGameFromPath, GameName } from "../utils/file-utils"; import { determineGameFromPath, GameName } from "../utils/file-utils";
import { getWorkspaceFolderByName } from "../utils/workspace"; 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( export function getCastFileData(
projectRoot: vscode.Uri, projectRoot: vscode.Uri,
document: vscode.TextDocument, document: vscode.TextDocument,
@ -16,24 +40,20 @@ export function getCastFileData(
if (gameName === undefined) { if (gameName === undefined) {
return undefined; return undefined;
} }
const config = getConfig(); const castFilePath = getCastFilePathForGame(projectRoot, gameName, fileName);
let castFilePath = ""; if (!existsSync(castFilePath)) {
if (gameName == GameName.Jak1) { return undefined;
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;
} }
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)) { if (!existsSync(castFilePath)) {
return undefined; return undefined;
} }
@ -268,7 +288,7 @@ export async function bulkUpdateVarCasts(
} }
// Write out cast file change // Write out cast file change
const configDir = await getDecompilerConfigDirectory(document.uri); const configDir = getDecompilerConfigDirectory(document.uri);
if (configDir === undefined) { if (configDir === undefined) {
return; return;
} }
@ -276,3 +296,48 @@ export async function bulkUpdateVarCasts(
writeFileSync(filePath, stringify(varNameData, null, 2)); 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));
}

View file

@ -21,10 +21,10 @@ export function insideGoalCodeInIR(
return false; return false;
} }
export async function getFuncNameFromPosition( export function getFuncNameFromPosition(
document: vscode.TextDocument, document: vscode.TextDocument,
position: vscode.Position, position: vscode.Position,
): Promise<string | undefined> { ): string | undefined {
const funcNameRegex = /; \.function (.*).*/g; const funcNameRegex = /; \.function (.*).*/g;
for (let i = position.line; i >= 0; i--) { for (let i = position.line; i >= 0; i--) {
const line = document.lineAt(i).text; const line = document.lineAt(i).text;
@ -33,9 +33,7 @@ export async function getFuncNameFromPosition(
return matches[0][1].toString(); return matches[0][1].toString();
} }
} }
await vscode.window.showErrorMessage( vscode.window.showErrorMessage("Couldn't determine function or method name");
"Couldn't determine function or method name",
);
return undefined; return undefined;
} }
@ -45,3 +43,52 @@ export async function getFuncNameFromSelection(
): Promise<string | undefined> { ): Promise<string | undefined> {
return await getFuncNameFromPosition(document, selection.start); 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;
}