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",
"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"

View file

@ -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();
}

View file

@ -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));
}

View file

@ -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;
}