From da1b6970796d8b7d85ef383e7e2381546771bb08 Mon Sep 17 00:00:00 2001 From: VladiStep Date: Tue, 2 Jan 2024 23:07:16 +0300 Subject: [PATCH] Some fixes and improvements: 1) The "Find all references" feature also searches game objects in code, but only in single-argument functions (e.g. `instance_exists(id)`). The same for fonts. 2) Font and game objects references search in code also works for GM 2023.8. 3) Refactored the code. --- UndertaleModLib/Decompiler/Decompiler.cs | 81 +++-- UndertaleModTool/MainWindow.xaml.cs | 2 +- .../UndertaleResourceReferenceMap.cs | 1 + .../UndertaleResourceReferenceMethodsMap.cs | 282 +++++++++++++----- 4 files changed, 247 insertions(+), 119 deletions(-) diff --git a/UndertaleModLib/Decompiler/Decompiler.cs b/UndertaleModLib/Decompiler/Decompiler.cs index d6eca4b5f..1f88e7630 100644 --- a/UndertaleModLib/Decompiler/Decompiler.cs +++ b/UndertaleModLib/Decompiler/Decompiler.cs @@ -616,9 +616,7 @@ public ExpressionAssetRef(int encodedResourceIndex) { Type = UndertaleInstruction.DataType.Variable; - // Break down index - first 24 bits are the ID, the rest is the ref type - AssetIndex = encodedResourceIndex & 0xffffff; - AssetRefType = (RefType)(encodedResourceIndex >> 24); + (AssetIndex, AssetRefType) = DecodeResourceIndexAndType(encodedResourceIndex); } public ExpressionAssetRef(int resourceIndex, RefType resourceType) @@ -641,46 +639,7 @@ public override string ToString(DecompileContext context) { if (context.GlobalContext.Data != null) { - IList assetList = null; - switch (AssetRefType) - { - case RefType.Sprite: - assetList = (IList)context.GlobalContext.Data.Sprites; - break; - case RefType.Background: - assetList = (IList)context.GlobalContext.Data.Backgrounds; - break; - case RefType.Sound: - assetList = (IList)context.GlobalContext.Data.Sounds; - break; - case RefType.Font: - assetList = (IList)context.GlobalContext.Data.Fonts; - break; - case RefType.Path: - assetList = (IList)context.GlobalContext.Data.Paths; - break; - case RefType.Timeline: - assetList = (IList)context.GlobalContext.Data.Timelines; - break; - case RefType.Room: - assetList = (IList)context.GlobalContext.Data.Rooms; - break; - case RefType.Object: - assetList = (IList)context.GlobalContext.Data.GameObjects; - break; - case RefType.Shader: - assetList = (IList)context.GlobalContext.Data.Shaders; - break; - case RefType.AnimCurve: - assetList = (IList)context.GlobalContext.Data.AnimationCurves; - break; - case RefType.Sequence: - assetList = (IList)context.GlobalContext.Data.Sequences; - break; - case RefType.ParticleSystem: - assetList = (IList)context.GlobalContext.Data.ParticleSystems; - break; - } + IList assetList = GetAssetListFromType(context.GlobalContext.Data, AssetRefType); if (assetList != null && AssetIndex >= 0 && AssetIndex < assetList.Count) return ((UndertaleNamedResource)assetList[AssetIndex]).Name.Content; @@ -707,6 +666,42 @@ internal override AssetIDType DoTypePropagation(DecompileContext context, AssetI _ => throw new NotImplementedException($"Missing ref type {AssetRefType}") }; } + + public static (int resourceIndex, RefType resourceType) DecodeResourceIndexAndType(int encodedResourceIndex) + { + // Break down index - first 24 bits are the ID, the rest is the ref type + return (encodedResourceIndex & 0xffffff, (RefType)(encodedResourceIndex >> 24)); + } + public static int DecodeResourceIndex(int encodedResourceIndex) + { + return encodedResourceIndex & 0xffffff; + } + public static RefType DecodeResourceType(int encodedResourceIndex) + { + return (RefType)(encodedResourceIndex >> 24); + } + + public static IList GetAssetListFromType(UndertaleData data, RefType type) + { + object list = type switch + { + RefType.Sprite => data.Sprites, + RefType.Background => data.Backgrounds, + RefType.Sound => data.Sounds, + RefType.Font => data.Fonts, + RefType.Path => data.Paths, + RefType.Timeline => data.Timelines, + RefType.Room => data.Rooms, + RefType.Object => data.GameObjects, + RefType.Shader => data.Shaders, + RefType.AnimCurve => data.AnimationCurves, + RefType.Sequence => data.Sequences, + RefType.ParticleSystem => data.ParticleSystems, + _ => null + }; + + return list as IList; + } } // Represents an expression converted to one of another data type - makes no difference on high-level code. diff --git a/UndertaleModTool/MainWindow.xaml.cs b/UndertaleModTool/MainWindow.xaml.cs index 112b41f5d..90f6d95d2 100644 --- a/UndertaleModTool/MainWindow.xaml.cs +++ b/UndertaleModTool/MainWindow.xaml.cs @@ -925,7 +925,7 @@ private void DisposeGameData() UpdateLayout(); Dispatcher.Invoke(() => { }, DispatcherPriority.ApplicationIdle); - UndertaleResourceReferenceMethodsMap.ClearFontFunctionList(); + UndertaleResourceReferenceMethodsMap.ClearFunctionLists(); Data.Dispose(); Data = null; diff --git a/UndertaleModTool/Windows/FindReferencesTypesDialog/UndertaleResourceReferenceMap.cs b/UndertaleModTool/Windows/FindReferencesTypesDialog/UndertaleResourceReferenceMap.cs index 8448a61f4..3cae33ed1 100644 --- a/UndertaleModTool/Windows/FindReferencesTypesDialog/UndertaleResourceReferenceMap.cs +++ b/UndertaleModTool/Windows/FindReferencesTypesDialog/UndertaleResourceReferenceMap.cs @@ -280,6 +280,7 @@ public static class UndertaleResourceReferenceMap Version = (1, 0, 0), Types = new[] { + (typeof(UndertaleCode), "Code"), (typeof(UndertaleRoom.GameObject), "Room object instance") } }, diff --git a/UndertaleModTool/Windows/FindReferencesTypesDialog/UndertaleResourceReferenceMethodsMap.cs b/UndertaleModTool/Windows/FindReferencesTypesDialog/UndertaleResourceReferenceMethodsMap.cs index 4694e5ec5..afa6eb87f 100644 --- a/UndertaleModTool/Windows/FindReferencesTypesDialog/UndertaleResourceReferenceMethodsMap.cs +++ b/UndertaleModTool/Windows/FindReferencesTypesDialog/UndertaleResourceReferenceMethodsMap.cs @@ -44,8 +44,53 @@ public static class UndertaleResourceReferenceMethodsMap private static Dictionary> funcReferences; private static Dictionary> variReferences; - private static Dictionary fontFunctions; + private static HashSet fontFunctions; + private static HashSet gameObjFunctions; private static Dictionary> fontReferences; + private static Dictionary> gameObjReferences; + + private static readonly Func getAssetIndex = (int i, UndertaleCode code, int assetListLen) => + { + if (i - 2 < 0) + return -1; + + int assetInstrIndex = i - 1; + var assetInstr = code.Instructions[assetInstrIndex]; + if (assetInstr.Kind != UndertaleInstruction.Opcode.Conv) + return -1; + + assetInstrIndex--; + assetInstr = code.Instructions[assetInstrIndex]; + if (assetInstr.Kind != UndertaleInstruction.Opcode.PushI + || assetInstr.Type1 != UndertaleInstruction.DataType.Int16 + || assetInstr.Value is not short val + || val < 0 || val > assetListLen - 1) + return -1; + + return val; + }; + private static readonly Func getAssetIndexGM2023_8 = (int i, UndertaleCode code, int assetListLen) => + { + int assetInstrIndex = i - 1; + if (assetInstrIndex < 0) + return -1; + + var assetInstr = code.Instructions[assetInstrIndex]; + + // If not `pushref` + if (assetInstr.Kind != UndertaleInstruction.Opcode.Break + || assetInstr.Value is not short val + || val != -11) + return -1; + + int assetIndex = Decompiler.ExpressionAssetRef.DecodeResourceIndex(assetInstr.IntArgument); + if (assetIndex < 0 || assetIndex > assetListLen - 1) + return -1; + + return assetIndex; + }; + private static Func getAssetIndexCurr; + private static readonly Dictionary typeMap = new() { @@ -371,23 +416,13 @@ IEnumerable GetCodeEntries() string funcName = instr.Function?.Target?.Name?.Content; if (funcName is null) continue; - if (!fontFunctions.TryGetValue(funcName, out var argTypes)) + if (instr.ArgumentsCount != 1) continue; - - int fontArgIndex; - if (argTypes.Length < 2) - fontArgIndex = 0; - else - fontArgIndex = Array.IndexOf(argTypes, AssetIDType.Font); // This shouldn't return -1 - int fontInstrIndex = i - ((fontArgIndex + 1) * 2); - if (fontInstrIndex < 0) + if (!fontFunctions.Contains(funcName)) continue; - var fontInstr = code.Instructions[fontInstrIndex]; - if (fontInstr.Kind != UndertaleInstruction.Opcode.PushI - || fontInstr.Type1 != UndertaleInstruction.DataType.Int16 - || fontInstr.Value is not short fontIndex - || fontIndex < 0 || fontIndex > data.Fonts.Count - 1) + int fontIndex = getAssetIndexCurr(i, code, data.Fonts.Count); + if (fontIndex == -1) continue; if (data.Fonts[fontIndex] == obj) @@ -1117,45 +1152,99 @@ IEnumerable GetPartSysInstances() Version = (1, 0, 0), Predicate = (objSrc, types, checkOne) => { - if (!types.Contains(typeof(UndertaleRoom.GameObject))) - return null; - if (objSrc is not UndertaleGameObject obj) return null; - IEnumerable GetObjInstances() + Dictionary outDict = new(); + + if (types.Contains(typeof(UndertaleCode))) { - if (data.IsGameMaker2()) + IEnumerable gameObjRefs; + if (fontReferences is not null) { - foreach (var room in data.Rooms) + gameObjRefs = gameObjReferences.Where(x => x.Value.Contains(obj)) + .Select(x => x.Key); + } + else + { + IEnumerable GetCodeEntries() { - foreach (var layer in room.Layers) + foreach (var code in data.Code) { - if (layer.InstancesData is not null) + UndertaleCode gameObjReference = null; + + for (int i = 0; i < code.Instructions.Count; i++) { - foreach (var inst in layer.InstancesData.Instances) - if (inst.ObjectDefinition == obj) - yield return new object[] { inst, layer, room }; + var instr = code.Instructions[i]; + + string funcName = instr.Function?.Target?.Name?.Content; + if (funcName is null) + continue; + if (instr.ArgumentsCount != 1) + continue; + if (!gameObjFunctions.Contains(funcName)) + continue; + + int gameObjIndex = getAssetIndexCurr(i, code, data.GameObjects.Count); + if (gameObjIndex == -1) + continue; + + if (data.GameObjects[gameObjIndex] == obj) + { + gameObjReference = code; + break; + } } + + if (gameObjReference is not null) + yield return gameObjReference; } } + + gameObjRefs = GetCodeEntries(); + if (gameObjRefs.Any()) + outDict["Code"] = checkOne ? gameObjRefs.ToEmptyArray() : gameObjRefs.ToArray(); } - else + } + + if (types.Contains(typeof(UndertaleRoom.GameObject))) + { + IEnumerable GetObjInstances() { - foreach (var room in data.Rooms) + if (data.IsGameMaker2()) + { + foreach (var room in data.Rooms) + { + foreach (var layer in room.Layers) + { + if (layer.InstancesData is not null) + { + foreach (var inst in layer.InstancesData.Instances) + if (inst.ObjectDefinition == obj) + yield return new object[] { inst, layer, room }; + } + } + } + } + else { - foreach (var inst in room.GameObjects) - if (inst.ObjectDefinition == obj) - yield return new object[] { inst, room }; + foreach (var room in data.Rooms) + { + foreach (var inst in room.GameObjects) + if (inst.ObjectDefinition == obj) + yield return new object[] { inst, room }; + } } } + + var objInstances = GetObjInstances(); + if (objInstances.Any()) + outDict["Room object instance"] = checkOne ? objInstances.ToEmptyArray() : objInstances.ToArray(); } - var objInstances = GetObjInstances(); - if (objInstances.Any()) - return new() { { "Room object instance", checkOne ? objInstances.ToEmptyArray() : objInstances.ToArray() } }; - else + if (outDict.Count == 0) return null; + return outDict; } }, new PredicateForVersion() @@ -1507,10 +1596,19 @@ public static Dictionary> GetReferencesOfObject(object obj, if (!typeMap.TryGetValue(obj.GetType(), out PredicateForVersion[] predicatesForVer)) return null; - if (!checkOne && fontFunctions is null) + if (!checkOne) { - var kvpList = AssetTypeResolver.builtin_funcs.Where(x => x.Value.Contains(AssetIDType.Font)); - fontFunctions = new(kvpList); + // Select only functions with single argument + fontFunctions ??= AssetTypeResolver.builtin_funcs.Where(x => x.Value.Length == 1 + && x.Value[0] == AssetIDType.Font) + .Select(x => x.Key) + .ToHashSet(); + gameObjFunctions ??= AssetTypeResolver.builtin_funcs.Where(x => x.Value.Length == 1 + && x.Value[0] == AssetIDType.GameObject) + .Select(x => x.Key) + .ToHashSet(); + + getAssetIndexCurr = data.IsVersionAtLeast(2023, 8) ? getAssetIndexGM2023_8 : getAssetIndex; } UndertaleResourceReferenceMethodsMap.data = data; @@ -1546,11 +1644,16 @@ public static async Task>> GetUnreferencedObject { UndertaleResourceReferenceMethodsMap.data = data; - if (fontFunctions is null) - { - var kvpList = AssetTypeResolver.builtin_funcs.Where(x => x.Value.Contains(AssetIDType.Font)); - fontFunctions = new(kvpList); - } + fontFunctions ??= AssetTypeResolver.builtin_funcs.Where(x => x.Value.Length == 1 + && x.Value[0] == AssetIDType.Font) + .Select(x => x.Key) + .ToHashSet(); + gameObjFunctions ??= AssetTypeResolver.builtin_funcs.Where(x => x.Value.Length == 1 + && x.Value[0] == AssetIDType.GameObject) + .Select(x => x.Key) + .ToHashSet(); + + getAssetIndexCurr = data.IsVersionAtLeast(2023, 8) ? getAssetIndexGM2023_8 : getAssetIndex; Dictionary> outDict = new(); @@ -1567,58 +1670,77 @@ public static async Task>> GetUnreferencedObject assets.AddRange(list.Item1.Cast() .Select(x => (x, list.Item2))); + #region Instruction processing + void ProcessInstruction(int i, UndertaleCode code, UndertaleInstruction instr, + ref HashSet fonts, ref HashSet gameObjects) + { + string funcName = instr.Function.Target.Name?.Content; + if (funcName is null) + return; + if (instr.ArgumentsCount != 1) + return; + + ProcessInstructionForFont(i, code, funcName, in fonts); + ProcessInstructionForGameObj(i, code, funcName, in gameObjects); + } + void ProcessInstructionForFont(int i, UndertaleCode code, string funcName, + in HashSet fonts) + { + if (!fontFunctions.Contains(funcName)) + return; + + int fontIndex = getAssetIndexCurr(i, code, data.Fonts.Count); + if (fontIndex == -1) + return; + + fonts.Add(data.Fonts[fontIndex]); + } + void ProcessInstructionForGameObj(int i, UndertaleCode code, string funcName, + in HashSet gameObjects) + { + if (!gameObjFunctions.Contains(funcName)) + return; + + int gameObjIndex = getAssetIndexCurr(i, code, data.GameObjects.Count); + if (gameObjIndex == -1) + return; + + gameObjects.Add(data.GameObjects[gameObjIndex]); + } + #endregion + stringReferences = new(); funcReferences = new(); variReferences = new(); fontReferences = new(); + gameObjReferences = new(); foreach (var code in data.Code) { var strings = new HashSet(); var functions = new HashSet(); var variables = new HashSet(); var fonts = new HashSet(); + var gameObjects = new HashSet(); for (int i = 0; i < code.Instructions.Count; i++) { - var inst = code.Instructions[i]; + var instr = code.Instructions[i]; - if (inst.Value is UndertaleResourceById strPtr) + if (instr.Value is UndertaleResourceById strPtr) strings.Add(strPtr.Resource); - if (inst.Destination?.Target is not null) - variables.Add(inst.Destination.Target); - if (inst.Value is UndertaleInstruction.Reference varRef && varRef.Target is not null) + if (instr.Destination?.Target is not null) + variables.Add(instr.Destination.Target); + if (instr.Value is UndertaleInstruction.Reference varRef && varRef.Target is not null) variables.Add(varRef.Target); - if (inst.Function?.Target is not null) + if (instr.Function?.Target is not null) { - functions.Add(inst.Function.Target); + functions.Add(instr.Function.Target); - string funcName = inst.Function.Target.Name?.Content; - if (funcName is not null - && fontFunctions.TryGetValue(funcName, out var argTypes)) - { - int fontArgIndex; - if (argTypes.Length < 2) - fontArgIndex = 0; - else - fontArgIndex = Array.IndexOf(argTypes, AssetIDType.Font); // This shouldn't return -1 - int fontInstrIndex = i - ((fontArgIndex + 1) * 2); - if (fontInstrIndex >= 0) - { - var fontInstr = code.Instructions[fontInstrIndex]; - if (fontInstr.Kind == UndertaleInstruction.Opcode.PushI - && fontInstr.Type1 == UndertaleInstruction.DataType.Int16 - && fontInstr.Value is short fontIndex - && fontIndex >= 0 && fontIndex <= data.Fonts.Count - 1) - { - fonts.Add(data.Fonts[fontIndex]); - break; - } - } - } + ProcessInstruction(i, code, instr, ref fonts, ref gameObjects); } - if (inst.Value is UndertaleInstruction.Reference funcRef && funcRef.Target is not null) + if (instr.Value is UndertaleInstruction.Reference funcRef && funcRef.Target is not null) functions.Add(funcRef.Target); } @@ -1630,6 +1752,8 @@ public static async Task>> GetUnreferencedObject variReferences[code] = variables; if (fonts.Count != 0) fontReferences[code] = fonts; + if (gameObjects.Count != 0) + gameObjReferences[code] = gameObjects; } mainWindow.IsEnabled = false; @@ -1714,6 +1838,8 @@ await Task.Run(() => stringReferences = null; funcReferences = null; variReferences = null; + fontReferences = null; + gameObjReferences = null; throw; } @@ -1721,13 +1847,19 @@ await Task.Run(() => stringReferences = null; funcReferences = null; variReferences = null; + fontReferences = null; + gameObjReferences = null; if (outDict.Count == 0) return null; return outDict; } - public static void ClearFontFunctionList() => fontFunctions = null; + public static void ClearFunctionLists() + { + fontFunctions = null; + gameObjFunctions = null; + } private static T[] ToEmptyArray(this IEnumerable _) => Array.Empty(); }