From 829cdb494925fb4673d628e3d0992de4c8d0da93 Mon Sep 17 00:00:00 2001 From: +TEK Date: Fri, 22 Apr 2022 21:24:43 +0200 Subject: [PATCH 1/8] added UndertaleObject duplicating with deep copies thanks to expression trees --- .../Util/DeepCopyByExpressionTrees.cs | 788 ++++++++++++++++++ .../Editors/UndertaleRoomEditor.xaml.cs | 7 +- UndertaleModTool/MainWindow.xaml | 1 + UndertaleModTool/MainWindow.xaml.cs | 20 +- 4 files changed, 812 insertions(+), 4 deletions(-) create mode 100644 UndertaleModLib/Util/DeepCopyByExpressionTrees.cs diff --git a/UndertaleModLib/Util/DeepCopyByExpressionTrees.cs b/UndertaleModLib/Util/DeepCopyByExpressionTrees.cs new file mode 100644 index 000000000..9a09cfecb --- /dev/null +++ b/UndertaleModLib/Util/DeepCopyByExpressionTrees.cs @@ -0,0 +1,788 @@ +// Made by Frantisek Konopecky, Prague, 2014 - 2016 +// +// Code comes under MIT licence - Can be used without +// limitations for both personal and commercial purposes. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace UndertaleModLib.Util +{ + /// + /// Superfast deep copier class, which uses Expression trees. + /// + public static class DeepCopyByExpressionTrees + { + private static readonly object IsStructTypeToDeepCopyDictionaryLocker = new object(); + private static Dictionary IsStructTypeToDeepCopyDictionary = new Dictionary(); + + private static readonly object CompiledCopyFunctionsDictionaryLocker = new object(); + private static Dictionary, object>> CompiledCopyFunctionsDictionary = + new Dictionary, object>>(); + + private static readonly Type ObjectType = typeof(Object); + private static readonly Type ObjectDictionaryType = typeof(Dictionary); + + /// + /// Creates a deep copy of an object. + /// + /// Object type. + /// Object to copy. + /// Dictionary of already copied objects (Keys: original objects, Values: their copies). + /// + public static T DeepCopyByExpressionTree(this T original, Dictionary copiedReferencesDict = null) + { + return (T)DeepCopyByExpressionTreeObj(original, false, copiedReferencesDict ?? new Dictionary(new ReferenceEqualityComparer())); + } + + private static object DeepCopyByExpressionTreeObj(object original, bool forceDeepCopy, Dictionary copiedReferencesDict) + { + if (original == null) + { + return null; + } + + var type = original.GetType(); + + if (IsDelegate(type)) + { + return null; + } + + if (!forceDeepCopy && !IsTypeToDeepCopy(type)) + { + return original; + } + + object alreadyCopiedObject; + + if (copiedReferencesDict.TryGetValue(original, out alreadyCopiedObject)) + { + return alreadyCopiedObject; + } + + if (type == ObjectType) + { + return new object(); + } + + var compiledCopyFunction = GetOrCreateCompiledLambdaCopyFunction(type); + + object copy = compiledCopyFunction(original, copiedReferencesDict); + + return copy; + } + + private static Func, object> GetOrCreateCompiledLambdaCopyFunction(Type type) + { + // The following structure ensures that multiple threads can use the dictionary + // even while dictionary is locked and being updated by other thread. + // That is why we do not modify the old dictionary instance but + // we replace it with a new instance everytime. + + Func, object> compiledCopyFunction; + + if (!CompiledCopyFunctionsDictionary.TryGetValue(type, out compiledCopyFunction)) + { + lock (CompiledCopyFunctionsDictionaryLocker) + { + if (!CompiledCopyFunctionsDictionary.TryGetValue(type, out compiledCopyFunction)) + { + var uncompiledCopyFunction = CreateCompiledLambdaCopyFunctionForType(type); + + compiledCopyFunction = uncompiledCopyFunction.Compile(); + + var dictionaryCopy = CompiledCopyFunctionsDictionary.ToDictionary(pair => pair.Key, pair => pair.Value); + + dictionaryCopy.Add(type, compiledCopyFunction); + + CompiledCopyFunctionsDictionary = dictionaryCopy; + } + } + } + + return compiledCopyFunction; + } + + private static Expression, object>> CreateCompiledLambdaCopyFunctionForType(Type type) + { + ParameterExpression inputParameter; + ParameterExpression inputDictionary; + ParameterExpression outputVariable; + ParameterExpression boxingVariable; + LabelTarget endLabel; + List variables; + List expressions; + + ///// INITIALIZATION OF EXPRESSIONS AND VARIABLES + + InitializeExpressions(type, + out inputParameter, + out inputDictionary, + out outputVariable, + out boxingVariable, + out endLabel, + out variables, + out expressions); + + ///// RETURN NULL IF ORIGINAL IS NULL + + IfNullThenReturnNullExpression(inputParameter, endLabel, expressions); + + ///// MEMBERWISE CLONE ORIGINAL OBJECT + + MemberwiseCloneInputToOutputExpression(type, inputParameter, outputVariable, expressions); + + ///// STORE COPIED OBJECT TO REFERENCES DICTIONARY + + if (IsClassOtherThanString(type)) + { + StoreReferencesIntoDictionaryExpression(inputParameter, inputDictionary, outputVariable, expressions); + } + + ///// COPY ALL NONVALUE OR NONPRIMITIVE FIELDS + + FieldsCopyExpressions(type, + inputParameter, + inputDictionary, + outputVariable, + boxingVariable, + expressions); + + ///// COPY ELEMENTS OF ARRAY + + if (IsArray(type) && IsTypeToDeepCopy(type.GetElementType())) + { + CreateArrayCopyLoopExpression(type, + inputParameter, + inputDictionary, + outputVariable, + variables, + expressions); + } + + ///// COMBINE ALL EXPRESSIONS INTO LAMBDA FUNCTION + + var lambda = CombineAllIntoLambdaFunctionExpression(inputParameter, inputDictionary, outputVariable, endLabel, variables, expressions); + + return lambda; + } + + private static void InitializeExpressions(Type type, + out ParameterExpression inputParameter, + out ParameterExpression inputDictionary, + out ParameterExpression outputVariable, + out ParameterExpression boxingVariable, + out LabelTarget endLabel, + out List variables, + out List expressions) + { + + inputParameter = Expression.Parameter(ObjectType); + + inputDictionary = Expression.Parameter(ObjectDictionaryType); + + outputVariable = Expression.Variable(type); + + boxingVariable = Expression.Variable(ObjectType); + + endLabel = Expression.Label(); + + variables = new List(); + + expressions = new List(); + + variables.Add(outputVariable); + variables.Add(boxingVariable); + } + + private static void IfNullThenReturnNullExpression(ParameterExpression inputParameter, LabelTarget endLabel, List expressions) + { + ///// Intended code: + ///// + ///// if (input == null) + ///// { + ///// return null; + ///// } + + var ifNullThenReturnNullExpression = + Expression.IfThen( + Expression.Equal( + inputParameter, + Expression.Constant(null, ObjectType)), + Expression.Return(endLabel)); + + expressions.Add(ifNullThenReturnNullExpression); + } + + private static void MemberwiseCloneInputToOutputExpression( + Type type, + ParameterExpression inputParameter, + ParameterExpression outputVariable, + List expressions) + { + ///// Intended code: + ///// + ///// var output = ()input.MemberwiseClone(); + + var memberwiseCloneMethod = ObjectType.GetMethod("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.Instance); + + var memberwiseCloneInputExpression = + Expression.Assign( + outputVariable, + Expression.Convert( + Expression.Call( + inputParameter, + memberwiseCloneMethod), + type)); + + expressions.Add(memberwiseCloneInputExpression); + } + + private static void StoreReferencesIntoDictionaryExpression(ParameterExpression inputParameter, + ParameterExpression inputDictionary, + ParameterExpression outputVariable, + List expressions) + { + ///// Intended code: + ///// + ///// inputDictionary[(Object)input] = (Object)output; + + var storeReferencesExpression = + Expression.Assign( + Expression.Property( + inputDictionary, + ObjectDictionaryType.GetProperty("Item"), + inputParameter), + Expression.Convert(outputVariable, ObjectType)); + + expressions.Add(storeReferencesExpression); + } + + private static Expression, object>> CombineAllIntoLambdaFunctionExpression( + ParameterExpression inputParameter, + ParameterExpression inputDictionary, + ParameterExpression outputVariable, + LabelTarget endLabel, + List variables, + List expressions) + { + expressions.Add(Expression.Label(endLabel)); + + expressions.Add(Expression.Convert(outputVariable, ObjectType)); + + var finalBody = Expression.Block(variables, expressions); + + var lambda = Expression.Lambda, object>>(finalBody, inputParameter, inputDictionary); + + return lambda; + } + + private static void CreateArrayCopyLoopExpression(Type type, + ParameterExpression inputParameter, + ParameterExpression inputDictionary, + ParameterExpression outputVariable, + List variables, + List expressions) + { + ///// Intended code: + ///// + ///// int i1, i2, ..., in; + ///// + ///// int length1 = inputarray.GetLength(0); + ///// i1 = 0; + ///// while (true) + ///// { + ///// if (i1 >= length1) + ///// { + ///// goto ENDLABELFORLOOP1; + ///// } + ///// int length2 = inputarray.GetLength(1); + ///// i2 = 0; + ///// while (true) + ///// { + ///// if (i2 >= length2) + ///// { + ///// goto ENDLABELFORLOOP2; + ///// } + ///// ... + ///// ... + ///// ... + ///// int lengthn = inputarray.GetLength(n); + ///// in = 0; + ///// while (true) + ///// { + ///// if (in >= lengthn) + ///// { + ///// goto ENDLABELFORLOOPn; + ///// } + ///// outputarray[i1, i2, ..., in] + ///// = ()DeepCopyByExpressionTreeObj( + ///// (Object)inputarray[i1, i2, ..., in]) + ///// in++; + ///// } + ///// ENDLABELFORLOOPn: + ///// ... + ///// ... + ///// ... + ///// i2++; + ///// } + ///// ENDLABELFORLOOP2: + ///// i1++; + ///// } + ///// ENDLABELFORLOOP1: + + var rank = type.GetArrayRank(); + + var indices = GenerateIndices(rank); + + variables.AddRange(indices); + + var elementType = type.GetElementType(); + + var assignExpression = ArrayFieldToArrayFieldAssignExpression(inputParameter, inputDictionary, outputVariable, elementType, type, indices); + + Expression forExpression = assignExpression; + + for (int dimension = 0; dimension < rank; dimension++) + { + var indexVariable = indices[dimension]; + + forExpression = LoopIntoLoopExpression(inputParameter, indexVariable, forExpression, dimension); + } + + expressions.Add(forExpression); + } + + private static List GenerateIndices(int arrayRank) + { + ///// Intended code: + ///// + ///// int i1, i2, ..., in; + + var indices = new List(); + + for (int i = 0; i < arrayRank; i++) + { + var indexVariable = Expression.Variable(typeof(Int32)); + + indices.Add(indexVariable); + } + + return indices; + } + + private static BinaryExpression ArrayFieldToArrayFieldAssignExpression( + ParameterExpression inputParameter, + ParameterExpression inputDictionary, + ParameterExpression outputVariable, + Type elementType, + Type arrayType, + List indices) + { + ///// Intended code: + ///// + ///// outputarray[i1, i2, ..., in] + ///// = ()DeepCopyByExpressionTreeObj( + ///// (Object)inputarray[i1, i2, ..., in]); + + var indexTo = Expression.ArrayAccess(outputVariable, indices); + + var indexFrom = Expression.ArrayIndex(Expression.Convert(inputParameter, arrayType), indices); + + var forceDeepCopy = elementType != ObjectType; + + var rightSide = + Expression.Convert( + Expression.Call( + DeepCopyByExpressionTreeObjMethod, + Expression.Convert(indexFrom, ObjectType), + Expression.Constant(forceDeepCopy, typeof(Boolean)), + inputDictionary), + elementType); + + var assignExpression = Expression.Assign(indexTo, rightSide); + + return assignExpression; + } + + private static BlockExpression LoopIntoLoopExpression( + ParameterExpression inputParameter, + ParameterExpression indexVariable, + Expression loopToEncapsulate, + int dimension) + { + ///// Intended code: + ///// + ///// int length = inputarray.GetLength(dimension); + ///// i = 0; + ///// while (true) + ///// { + ///// if (i >= length) + ///// { + ///// goto ENDLABELFORLOOP; + ///// } + ///// loopToEncapsulate; + ///// i++; + ///// } + ///// ENDLABELFORLOOP: + + var lengthVariable = Expression.Variable(typeof(Int32)); + + var endLabelForThisLoop = Expression.Label(); + + var newLoop = + Expression.Loop( + Expression.Block( + new ParameterExpression[0], + Expression.IfThen( + Expression.GreaterThanOrEqual(indexVariable, lengthVariable), + Expression.Break(endLabelForThisLoop)), + loopToEncapsulate, + Expression.PostIncrementAssign(indexVariable)), + endLabelForThisLoop); + + var lengthAssignment = GetLengthForDimensionExpression(lengthVariable, inputParameter, dimension); + + var indexAssignment = Expression.Assign(indexVariable, Expression.Constant(0)); + + return Expression.Block( + new[] { lengthVariable }, + lengthAssignment, + indexAssignment, + newLoop); + } + + private static BinaryExpression GetLengthForDimensionExpression( + ParameterExpression lengthVariable, + ParameterExpression inputParameter, + int i) + { + ///// Intended code: + ///// + ///// length = ((Array)input).GetLength(i); + + var getLengthMethod = typeof(Array).GetMethod("GetLength", BindingFlags.Public | BindingFlags.Instance); + + var dimensionConstant = Expression.Constant(i); + + return Expression.Assign( + lengthVariable, + Expression.Call( + Expression.Convert(inputParameter, typeof(Array)), + getLengthMethod, + new[] { dimensionConstant })); + } + + private static void FieldsCopyExpressions(Type type, + ParameterExpression inputParameter, + ParameterExpression inputDictionary, + ParameterExpression outputVariable, + ParameterExpression boxingVariable, + List expressions) + { + var fields = GetAllRelevantFields(type); + + var readonlyFields = fields.Where(f => f.IsInitOnly).ToList(); + var writableFields = fields.Where(f => !f.IsInitOnly).ToList(); + + ///// READONLY FIELDS COPY (with boxing) + + bool shouldUseBoxing = readonlyFields.Any(); + + if (shouldUseBoxing) + { + var boxingExpression = Expression.Assign(boxingVariable, Expression.Convert(outputVariable, ObjectType)); + + expressions.Add(boxingExpression); + } + + foreach (var field in readonlyFields) + { + if (IsDelegate(field.FieldType)) + { + ReadonlyFieldToNullExpression(field, boxingVariable, expressions); + } + else + { + ReadonlyFieldCopyExpression(type, + field, + inputParameter, + inputDictionary, + boxingVariable, + expressions); + } + } + + if (shouldUseBoxing) + { + var unboxingExpression = Expression.Assign(outputVariable, Expression.Convert(boxingVariable, type)); + + expressions.Add(unboxingExpression); + } + + ///// NOT-READONLY FIELDS COPY + + foreach (var field in writableFields) + { + if (IsDelegate(field.FieldType)) + { + WritableFieldToNullExpression(field, outputVariable, expressions); + } + else + { + WritableFieldCopyExpression(type, + field, + inputParameter, + inputDictionary, + outputVariable, + expressions); + } + } + } + + private static FieldInfo[] GetAllRelevantFields(Type type, bool forceAllFields = false) + { + var fieldsList = new List(); + + var typeCache = type; + + while (typeCache != null) + { + fieldsList.AddRange( + typeCache + .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy) + .Where(field => forceAllFields || IsTypeToDeepCopy(field.FieldType))); + + typeCache = typeCache.BaseType; + } + + return fieldsList.ToArray(); + } + + private static FieldInfo[] GetAllFields(Type type) + { + return GetAllRelevantFields(type, forceAllFields: true); + } + + private static readonly Type FieldInfoType = typeof(FieldInfo); + private static readonly MethodInfo SetValueMethod = FieldInfoType.GetMethod("SetValue", new[] { ObjectType, ObjectType }); + + private static void ReadonlyFieldToNullExpression(FieldInfo field, ParameterExpression boxingVariable, List expressions) + { + // This option must be implemented by Reflection because of the following: + // https://visualstudio.uservoice.com/forums/121579-visual-studio-2015/suggestions/2727812-allow-expression-assign-to-set-readonly-struct-f + + ///// Intended code: + ///// + ///// fieldInfo.SetValue(boxing, null); + + var fieldToNullExpression = + Expression.Call( + Expression.Constant(field), + SetValueMethod, + boxingVariable, + Expression.Constant(null, field.FieldType)); + + expressions.Add(fieldToNullExpression); + } + + private static readonly Type ThisType = typeof(DeepCopyByExpressionTrees); + private static readonly MethodInfo DeepCopyByExpressionTreeObjMethod = ThisType.GetMethod("DeepCopyByExpressionTreeObj", BindingFlags.NonPublic | BindingFlags.Static); + + private static void ReadonlyFieldCopyExpression(Type type, + FieldInfo field, + ParameterExpression inputParameter, + ParameterExpression inputDictionary, + ParameterExpression boxingVariable, + List expressions) + { + // This option must be implemented by Reflection (SetValueMethod) because of the following: + // https://visualstudio.uservoice.com/forums/121579-visual-studio-2015/suggestions/2727812-allow-expression-assign-to-set-readonly-struct-f + + ///// Intended code: + ///// + ///// fieldInfo.SetValue(boxing, DeepCopyByExpressionTreeObj((Object)(()input).)) + + var fieldFrom = Expression.Field(Expression.Convert(inputParameter, type), field); + + var forceDeepCopy = field.FieldType != ObjectType; + + var fieldDeepCopyExpression = + Expression.Call( + Expression.Constant(field, FieldInfoType), + SetValueMethod, + boxingVariable, + Expression.Call( + DeepCopyByExpressionTreeObjMethod, + Expression.Convert(fieldFrom, ObjectType), + Expression.Constant(forceDeepCopy, typeof(Boolean)), + inputDictionary)); + + expressions.Add(fieldDeepCopyExpression); + } + + private static void WritableFieldToNullExpression(FieldInfo field, ParameterExpression outputVariable, List expressions) + { + ///// Intended code: + ///// + ///// output. = ()null; + + var fieldTo = Expression.Field(outputVariable, field); + + var fieldToNullExpression = + Expression.Assign( + fieldTo, + Expression.Constant(null, field.FieldType)); + + expressions.Add(fieldToNullExpression); + } + + private static void WritableFieldCopyExpression(Type type, + FieldInfo field, + ParameterExpression inputParameter, + ParameterExpression inputDictionary, + ParameterExpression outputVariable, + List expressions) + { + ///// Intended code: + ///// + ///// output. = ()DeepCopyByExpressionTreeObj((Object)(()input).); + + var fieldFrom = Expression.Field(Expression.Convert(inputParameter, type), field); + + var fieldType = field.FieldType; + + var fieldTo = Expression.Field(outputVariable, field); + + var forceDeepCopy = field.FieldType != ObjectType; + + var fieldDeepCopyExpression = + Expression.Assign( + fieldTo, + Expression.Convert( + Expression.Call( + DeepCopyByExpressionTreeObjMethod, + Expression.Convert(fieldFrom, ObjectType), + Expression.Constant(forceDeepCopy, typeof(Boolean)), + inputDictionary), + fieldType)); + + expressions.Add(fieldDeepCopyExpression); + } + + private static bool IsArray(Type type) + { + return type.IsArray; + } + + private static bool IsDelegate(Type type) + { + return typeof(Delegate).IsAssignableFrom(type); + } + + private static bool IsTypeToDeepCopy(Type type) + { + return IsClassOtherThanString(type) + || IsStructWhichNeedsDeepCopy(type); + } + + private static bool IsClassOtherThanString(Type type) + { + return !type.IsValueType && type != typeof(String); + } + + private static bool IsStructWhichNeedsDeepCopy(Type type) + { + // The following structure ensures that multiple threads can use the dictionary + // even while dictionary is locked and being updated by other thread. + // That is why we do not modify the old dictionary instance but + // we replace it with a new instance everytime. + + bool isStructTypeToDeepCopy; + + if (!IsStructTypeToDeepCopyDictionary.TryGetValue(type, out isStructTypeToDeepCopy)) + { + lock (IsStructTypeToDeepCopyDictionaryLocker) + { + if (!IsStructTypeToDeepCopyDictionary.TryGetValue(type, out isStructTypeToDeepCopy)) + { + isStructTypeToDeepCopy = IsStructWhichNeedsDeepCopy_NoDictionaryUsed(type); + + var newDictionary = IsStructTypeToDeepCopyDictionary.ToDictionary(pair => pair.Key, pair => pair.Value); + + newDictionary[type] = isStructTypeToDeepCopy; + + IsStructTypeToDeepCopyDictionary = newDictionary; + } + } + } + + return isStructTypeToDeepCopy; + } + + private static bool IsStructWhichNeedsDeepCopy_NoDictionaryUsed(Type type) + { + return IsStructOtherThanBasicValueTypes(type) + && HasInItsHierarchyFieldsWithClasses(type); + } + + private static bool IsStructOtherThanBasicValueTypes(Type type) + { + return type.IsValueType + && !type.IsPrimitive + && !type.IsEnum + && type != typeof(Decimal); + } + + private static bool HasInItsHierarchyFieldsWithClasses(Type type, HashSet alreadyCheckedTypes = null) + { + alreadyCheckedTypes = alreadyCheckedTypes ?? new HashSet(); + + alreadyCheckedTypes.Add(type); + + var allFields = GetAllFields(type); + + var allFieldTypes = allFields.Select(f => f.FieldType).Distinct().ToList(); + + var hasFieldsWithClasses = allFieldTypes.Any(IsClassOtherThanString); + + if (hasFieldsWithClasses) + { + return true; + } + + var notBasicStructsTypes = allFieldTypes.Where(IsStructOtherThanBasicValueTypes).ToList(); + + var typesToCheck = notBasicStructsTypes.Where(t => !alreadyCheckedTypes.Contains(t)).ToList(); + + foreach (var typeToCheck in typesToCheck) + { + if (HasInItsHierarchyFieldsWithClasses(typeToCheck, alreadyCheckedTypes)) + { + return true; + } + } + + return false; + } + + public class ReferenceEqualityComparer : EqualityComparer + { + public override bool Equals(object x, object y) + { + return ReferenceEquals(x, y); + } + + public override int GetHashCode(object obj) + { + if (obj == null) return 0; + + return obj.GetHashCode(); + } + } + } +} diff --git a/UndertaleModTool/Editors/UndertaleRoomEditor.xaml.cs b/UndertaleModTool/Editors/UndertaleRoomEditor.xaml.cs index 5f432e2c6..1bceb6329 100644 --- a/UndertaleModTool/Editors/UndertaleRoomEditor.xaml.cs +++ b/UndertaleModTool/Editors/UndertaleRoomEditor.xaml.cs @@ -1502,11 +1502,12 @@ public static void GenerateSpriteCache(UndertaleRoom room) else tiles.AddRange(layer.AssetsData.LegacyTiles); - allObjects.AddRange(layer.AssetsData.Sprites); + allObjects.AddRange(layer.AssetsData.Sprites.Where(s => s.Sprite?.Textures.Count > 0)); break; case LayerType.Background: - allObjects.Add(layer.BackgroundData); + if (layer.BackgroundData.Sprite?.Textures.Count > 0) + allObjects.Add(layer.BackgroundData); break; case LayerType.Instances: @@ -1520,7 +1521,7 @@ public static void GenerateSpriteCache(UndertaleRoom room) tiles = room.Tiles.ToList(); allObjects.AddRange(room.Backgrounds); - allObjects.AddRange(room.GameObjects); + allObjects.AddRange(room.GameObjects.Where(g => g.ObjectDefinition?.Sprite?.Textures.Count > 0)); } tileTextures = tiles?.AsParallel() diff --git a/UndertaleModTool/MainWindow.xaml b/UndertaleModTool/MainWindow.xaml index 7ef0db4ca..3b2dca38f 100644 --- a/UndertaleModTool/MainWindow.xaml +++ b/UndertaleModTool/MainWindow.xaml @@ -187,6 +187,7 @@ + diff --git a/UndertaleModTool/MainWindow.xaml.cs b/UndertaleModTool/MainWindow.xaml.cs index 9f0d5778f..75cb9e525 100644 --- a/UndertaleModTool/MainWindow.xaml.cs +++ b/UndertaleModTool/MainWindow.xaml.cs @@ -46,6 +46,7 @@ using System.Net; using System.Globalization; using System.Windows.Controls.Primitives; +using UndertaleModLib.Util; namespace UndertaleModTool { @@ -1599,7 +1600,19 @@ private TreeViewItem GetTreeViewItemFor(UndertaleObject obj) } return null; } - + private void DuplicateItem(UndertaleObject obj) + { + TreeViewItem container = GetNearestParent(GetTreeViewItemFor(obj)); + object source = container.ItemsSource; + IList list = ((source as ICollectionView)?.SourceCollection as IList) ?? (source as IList); + bool isLast = list.IndexOf(obj) == list.Count - 1; + if (MessageBox.Show("Duplicate " + obj.ToString() + "?", "Confirmation", MessageBoxButton.YesNo, isLast ? MessageBoxImage.Question : MessageBoxImage.Warning) == MessageBoxResult.Yes) + { + var newObject = obj.DeepCopyByExpressionTree(); + list.Insert(list.IndexOf(obj) + 1, newObject); + UpdateTree(); + } + } private void DeleteItem(UndertaleObject obj) { TreeViewItem container = GetNearestParent(GetTreeViewItemFor(obj)); @@ -1727,6 +1740,11 @@ private void MenuItem_CopyName_Click(object sender, RoutedEventArgs e) if (Highlighted is UndertaleNamedResource namedRes) CopyItemName(namedRes); } + private void MenuItem_Duplicate_Click(object sender, RoutedEventArgs e) + { + if (Highlighted is UndertaleObject obj) + DuplicateItem(obj); + } private void MenuItem_Delete_Click(object sender, RoutedEventArgs e) { if (Highlighted is UndertaleObject obj) From c2c8390cd94428cc9ddd554a6785bce16efad38f Mon Sep 17 00:00:00 2001 From: +TEK Date: Fri, 22 Apr 2022 22:30:11 +0200 Subject: [PATCH 2/8] changed default behavior or double clicking anything to open in a new Tab focus on object list in rooms when clicking on something --- UndertaleModTool/Editors/UndertaleRoomEditor.xaml.cs | 1 + UndertaleModTool/MainWindow.xaml.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/UndertaleModTool/Editors/UndertaleRoomEditor.xaml.cs b/UndertaleModTool/Editors/UndertaleRoomEditor.xaml.cs index 1bceb6329..538f11fb7 100644 --- a/UndertaleModTool/Editors/UndertaleRoomEditor.xaml.cs +++ b/UndertaleModTool/Editors/UndertaleRoomEditor.xaml.cs @@ -835,6 +835,7 @@ private void SelectObject(UndertaleObject obj) resListView.IsExpanded = true; resListView.BringIntoView(); + resListView.Focus(); resListView.UpdateLayout(); StackPanel resPanel = MainWindow.FindVisualChild(resListView); diff --git a/UndertaleModTool/MainWindow.xaml.cs b/UndertaleModTool/MainWindow.xaml.cs index 75cb9e525..98f484434 100644 --- a/UndertaleModTool/MainWindow.xaml.cs +++ b/UndertaleModTool/MainWindow.xaml.cs @@ -1465,7 +1465,7 @@ private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEv private void MainTree_MouseDoubleClick(object sender, MouseButtonEventArgs e) { - OpenInTab(Highlighted); + OpenInTab(Highlighted, true); } private void MainTree_KeyUp(object sender, KeyEventArgs e) From cede4d8956f326ee8b5862e19b960c79d45d4016 Mon Sep 17 00:00:00 2001 From: +TEK Date: Wed, 25 May 2022 03:11:39 +0200 Subject: [PATCH 3/8] Add global grid width and height settings which, if enabled, override the automatic grid calculation (from most used tile in a room). Moved the grid from background to foreground (thanks to OpacityMask). Adjusted the settings menu layout a bit. Room thickness is 0.5 by default now. --- UndertaleModLib/Models/UndertaleRoom.cs | 17 +- .../Controls/UndertaleRoomRenderer.xaml.cs | 2 +- .../Editors/UndertaleCodeEditor.xaml.cs | 2596 ++++++++--------- .../Editors/UndertaleRoomEditor.xaml | 39 +- .../Editors/UndertaleRoomEditor.xaml.cs | 53 +- UndertaleModTool/Settings.cs | 4 + UndertaleModTool/Windows/SettingsWindow.xaml | 76 +- .../Windows/SettingsWindow.xaml.cs | 41 + 8 files changed, 1488 insertions(+), 1340 deletions(-) diff --git a/UndertaleModLib/Models/UndertaleRoom.cs b/UndertaleModLib/Models/UndertaleRoom.cs index 171e17372..7bffda745 100644 --- a/UndertaleModLib/Models/UndertaleRoom.cs +++ b/UndertaleModLib/Models/UndertaleRoom.cs @@ -129,7 +129,7 @@ public enum RoomEntryFlags : uint /// /// The thickness of the room grid in pixels. /// - public double GridThicknessPx { get; set; } = 1d; + public double GridThicknessPx { get; set; } = 0.5d; private UndertalePointerList _layers = new(); /// @@ -352,7 +352,7 @@ public void Unserialize(UndertaleReader reader) } } - public void SetupRoom(bool calculateGrid = true) + public void SetupRoom(bool calculateGridWidth = true, bool calculateGridHeight = true) { foreach (Layer layer in Layers) { @@ -362,7 +362,7 @@ public void SetupRoom(bool calculateGrid = true) foreach (UndertaleRoom.Background bgnd in Backgrounds) bgnd.ParentRoom = this; - if (calculateGrid) + if (calculateGridWidth || calculateGridHeight) { // Automagically set the grid size to whatever most tiles are sized @@ -403,11 +403,18 @@ public void SetupRoom(bool calculateGrid = true) } // If tiles exist at all, grab the most used tile size and use that as our grid size + // If a default starting grid is set in the settings, use that instead if (tileSizes.Count > 0) { var largestKey = tileSizes.Aggregate((x, y) => x.Value > y.Value ? x : y).Key; - GridWidth = largestKey.X; - GridHeight = largestKey.Y; + if (calculateGridWidth) + { + GridWidth = largestKey.X; + } + if (calculateGridHeight) + { + GridHeight = largestKey.Y; + } } } } diff --git a/UndertaleModTool/Controls/UndertaleRoomRenderer.xaml.cs b/UndertaleModTool/Controls/UndertaleRoomRenderer.xaml.cs index 69156e671..ee5655c83 100644 --- a/UndertaleModTool/Controls/UndertaleRoomRenderer.xaml.cs +++ b/UndertaleModTool/Controls/UndertaleRoomRenderer.xaml.cs @@ -53,7 +53,7 @@ public UndertaleRoomRenderer() private void RoomRenderer_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) { - (DataContext as UndertaleRoom)?.SetupRoom(!bgGridDisabled); + (DataContext as UndertaleRoom)?.SetupRoom(!bgGridDisabled, !bgGridDisabled); UndertaleRoomEditor.GenerateSpriteCache(DataContext as UndertaleRoom); } diff --git a/UndertaleModTool/Editors/UndertaleCodeEditor.xaml.cs b/UndertaleModTool/Editors/UndertaleCodeEditor.xaml.cs index 52c8a18e9..6325304cc 100644 --- a/UndertaleModTool/Editors/UndertaleCodeEditor.xaml.cs +++ b/UndertaleModTool/Editors/UndertaleCodeEditor.xaml.cs @@ -1,1299 +1,1299 @@ -using GraphVizWrapper; -using GraphVizWrapper.Commands; -using GraphVizWrapper.Queries; -using ICSharpCode.AvalonEdit; -using ICSharpCode.AvalonEdit.Document; -using ICSharpCode.AvalonEdit.Editing; -using ICSharpCode.AvalonEdit.Folding; -using ICSharpCode.AvalonEdit.Highlighting; -using ICSharpCode.AvalonEdit.Highlighting.Xshd; -using ICSharpCode.AvalonEdit.Rendering; -using ICSharpCode.AvalonEdit.Search; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.Versioning; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Media.TextFormatting; -using System.Windows.Navigation; -using System.Xml; -using UndertaleModLib; -using UndertaleModLib.Compiler; -using UndertaleModLib.Decompiler; -using UndertaleModLib.Models; -using static UndertaleModTool.MainWindow.CodeEditorMode; - -namespace UndertaleModTool +using GraphVizWrapper; +using GraphVizWrapper.Commands; +using GraphVizWrapper.Queries; +using ICSharpCode.AvalonEdit; +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Editing; +using ICSharpCode.AvalonEdit.Folding; +using ICSharpCode.AvalonEdit.Highlighting; +using ICSharpCode.AvalonEdit.Highlighting.Xshd; +using ICSharpCode.AvalonEdit.Rendering; +using ICSharpCode.AvalonEdit.Search; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Versioning; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Media.TextFormatting; +using System.Windows.Navigation; +using System.Xml; +using UndertaleModLib; +using UndertaleModLib.Compiler; +using UndertaleModLib.Decompiler; +using UndertaleModLib.Models; +using static UndertaleModTool.MainWindow.CodeEditorMode; + +namespace UndertaleModTool { - /// - /// Logika interakcji dla klasy UndertaleCodeEditor.xaml - /// - [SupportedOSPlatform("windows7.0")] - public partial class UndertaleCodeEditor : DataUserControl - { - private static MainWindow mainWindow = Application.Current.MainWindow as MainWindow; - - public UndertaleCode CurrentDisassembled = null; - public UndertaleCode CurrentDecompiled = null; - public List CurrentLocals = null; - public UndertaleCode CurrentGraphed = null; - public string ProfileHash = mainWindow.ProfileHash; - public string MainPath = Path.Combine(Settings.ProfilesFolder, mainWindow.ProfileHash, "Main"); - public string TempPath = Path.Combine(Settings.ProfilesFolder, mainWindow.ProfileHash, "Temp"); - - public bool DecompiledFocused = false; - public bool DecompiledChanged = false; - public bool DecompiledYet = false; - public bool DecompiledSkipped = false; - public SearchPanel DecompiledSearchPanel; - - public bool DisassemblyFocused = false; - public bool DisassemblyChanged = false; - public bool DisassembledYet = false; - public bool DisassemblySkipped = false; - public SearchPanel DisassemblySearchPanel; - - public static RoutedUICommand Compile = new RoutedUICommand("Compile code", "Compile", typeof(UndertaleCodeEditor)); - - public UndertaleCodeEditor() - { - InitializeComponent(); - - // Decompiled editor styling and functionality - DecompiledSearchPanel = SearchPanel.Install(DecompiledEditor.TextArea); - DecompiledSearchPanel.MarkerBrush = new SolidColorBrush(Color.FromRgb(90, 90, 90)); - - using (Stream stream = this.GetType().Assembly.GetManifestResourceStream("UndertaleModTool.Resources.GML.xshd")) - { - using (XmlTextReader reader = new XmlTextReader(stream)) - { - DecompiledEditor.SyntaxHighlighting = HighlightingLoader.Load(reader, HighlightingManager.Instance); - var def = DecompiledEditor.SyntaxHighlighting; - if (mainWindow.Data.GeneralInfo.Major < 2) - { - foreach (var span in def.MainRuleSet.Spans) - { - string expr = span.StartExpression.ToString(); - if (expr == "\"" || expr == "'") - { - span.RuleSet.Spans.Clear(); - } - } - } - // This was an attempt to only highlight - // GMS 2.3+ keywords if the game is - // made in such a version. - // However despite what StackOverflow - // says, this isn't working so it's just - // hardcoded in the XML for now - /* - if(mainWindow.Data.GMS2_3) - { - HighlightingColor color = null; - foreach (var rule in def.MainRuleSet.Rules) - { - if (rule.Regex.IsMatch("if")) - { - color = rule.Color; - break; - } - } - if (color != null) - { - string[] keywords = - { - "new", - "function", - "keywords" - }; - var rule = new HighlightingRule(); - var regex = String.Format(@"\b(?>{0})\b", String.Join("|", keywords)); - - rule.Regex = new Regex(regex); - rule.Color = color; - - def.MainRuleSet.Rules.Add(rule); - } - }*/ - } - } - - DecompiledEditor.Options.ConvertTabsToSpaces = true; - - DecompiledEditor.TextArea.TextView.ElementGenerators.Add(new NumberGenerator()); - DecompiledEditor.TextArea.TextView.ElementGenerators.Add(new NameGenerator()); - - DecompiledEditor.TextArea.TextView.Options.HighlightCurrentLine = true; - DecompiledEditor.TextArea.TextView.CurrentLineBackground = new SolidColorBrush(Color.FromRgb(60, 60, 60)); - DecompiledEditor.TextArea.TextView.CurrentLineBorder = new Pen() { Thickness = 0 }; - - DecompiledEditor.Document.TextChanged += (s, e) => - { - DecompiledFocused = true; - DecompiledChanged = true; - }; - - DecompiledEditor.TextArea.SelectionBrush = new SolidColorBrush(Color.FromRgb(100, 100, 100)); - DecompiledEditor.TextArea.SelectionForeground = null; - DecompiledEditor.TextArea.SelectionBorder = null; - DecompiledEditor.TextArea.SelectionCornerRadius = 0; - - // Disassembly editor styling and functionality - DisassemblySearchPanel = SearchPanel.Install(DisassemblyEditor.TextArea); - DisassemblySearchPanel.MarkerBrush = new SolidColorBrush(Color.FromRgb(90, 90, 90)); - - using (Stream stream = this.GetType().Assembly.GetManifestResourceStream("UndertaleModTool.Resources.VMASM.xshd")) - { - using (XmlTextReader reader = new XmlTextReader(stream)) - { - DisassemblyEditor.SyntaxHighlighting = HighlightingLoader.Load(reader, HighlightingManager.Instance); - } - } - - DisassemblyEditor.TextArea.TextView.ElementGenerators.Add(new NameGenerator()); - - DisassemblyEditor.TextArea.TextView.Options.HighlightCurrentLine = true; - DisassemblyEditor.TextArea.TextView.CurrentLineBackground = new SolidColorBrush(Color.FromRgb(60, 60, 60)); - DisassemblyEditor.TextArea.TextView.CurrentLineBorder = new Pen() { Thickness = 0 }; - - DisassemblyEditor.Document.TextChanged += (s, e) => DisassemblyChanged = true; - - DisassemblyEditor.TextArea.SelectionBrush = new SolidColorBrush(Color.FromRgb(100, 100, 100)); - DisassemblyEditor.TextArea.SelectionForeground = null; - DisassemblyEditor.TextArea.SelectionBorder = null; - DisassemblyEditor.TextArea.SelectionCornerRadius = 0; - } - - private async void TabControl_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - UndertaleCode code = this.DataContext as UndertaleCode; - Directory.CreateDirectory(MainPath); - Directory.CreateDirectory(TempPath); - if (code == null) - return; - DecompiledSearchPanel.Close(); - DisassemblySearchPanel.Close(); - await DecompiledLostFocusBody(sender, null); - DisassemblyEditor_LostFocus(sender, null); - if (DisassemblyTab.IsSelected && code != CurrentDisassembled) - { - DisassembleCode(code, !DisassembledYet); - DisassembledYet = true; - } - if (DecompiledTab.IsSelected && code != CurrentDecompiled) - { - _ = DecompileCode(code, !DecompiledYet); - DecompiledYet = true; - } - if (GraphTab.IsSelected && code != CurrentGraphed) - { - GraphCode(code); - } - } - - private async void UserControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) - { - UndertaleCode code = this.DataContext as UndertaleCode; - if (code == null) - return; - - // compile/disassemble previously edited code (save changes) - if (DecompiledTab.IsSelected && DecompiledFocused && DecompiledChanged && - CurrentDecompiled is not null && CurrentDecompiled != code) - { - DecompiledSkipped = true; - DecompiledEditor_LostFocus(sender, null); - - } - else if (DisassemblyTab.IsSelected && DisassemblyFocused && DisassemblyChanged && - CurrentDisassembled is not null && CurrentDisassembled != code) - { - DisassemblySkipped = true; - DisassemblyEditor_LostFocus(sender, null); - } - - DecompiledEditor_LostFocus(sender, null); - DisassemblyEditor_LostFocus(sender, null); - - if (MainWindow.CodeEditorDecompile != Unstated) //if opened from the code search results "link" - { - if (MainWindow.CodeEditorDecompile == DontDecompile && code != CurrentDisassembled) - { - if (CodeModeTabs.SelectedItem != DisassemblyTab) - CodeModeTabs.SelectedItem = DisassemblyTab; - else - DisassembleCode(code, true); - } - - if (MainWindow.CodeEditorDecompile == Decompile && code != CurrentDecompiled) - { - if (CodeModeTabs.SelectedItem != DecompiledTab) - CodeModeTabs.SelectedItem = DecompiledTab; - else - _ = DecompileCode(code, true); - } - - MainWindow.CodeEditorDecompile = Unstated; - } - else - { - if (DisassemblyTab.IsSelected && code != CurrentDisassembled) - { - DisassembleCode(code, true); - } - if (DecompiledTab.IsSelected && code != CurrentDecompiled) - { - _ = DecompileCode(code, true); - } - if (GraphTab.IsSelected && code != CurrentGraphed) - { - GraphCode(code); - } - } - } - - public static readonly RoutedEvent CtrlKEvent = EventManager.RegisterRoutedEvent( - "CtrlK", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(UndertaleCodeEditor)); - - private async Task CompileCommandBody(object sender, EventArgs e) - { - if (DecompiledFocused) - { - await DecompiledLostFocusBody(sender, new RoutedEventArgs(CtrlKEvent)); - } - else if (DisassemblyFocused) - { - DisassemblyEditor_LostFocus(sender, new RoutedEventArgs(CtrlKEvent)); - DisassemblyEditor_GotFocus(sender, null); - } - - await Task.Delay(1); //dummy await - } - private void Command_Compile(object sender, EventArgs e) - { - _ = CompileCommandBody(sender, e); - } - public async Task SaveChanges() - { - await CompileCommandBody(null, null); - } - - private void DisassembleCode(UndertaleCode code, bool first) - { - code.UpdateAddresses(); - - string text; - - DisassemblyEditor.TextArea.ClearSelection(); - if (code.ParentEntry != null) - { - DisassemblyEditor.IsReadOnly = true; - text = "; This code entry is a reference to an anonymous function within " + code.ParentEntry.Name.Content + ", view it there"; - } - else - { - DisassemblyEditor.IsReadOnly = false; - - var data = mainWindow.Data; - text = code.Disassemble(data.Variables, data.CodeLocals.For(code)); - - CurrentLocals = new List(); - } - - DisassemblyEditor.Document.BeginUpdate(); - DisassemblyEditor.Document.Text = text; - DisassemblyEditor.Document.EndUpdate(); - - if (first) - DisassemblyEditor.Document.UndoStack.ClearAll(); - - CurrentDisassembled = code; - DisassemblyChanged = false; - } - - public static Dictionary gettext = null; - private void UpdateGettext(UndertaleCode gettextCode) - { - gettext = new Dictionary(); - string[] decompilationOutput; - if (!SettingsWindow.ProfileModeEnabled) - decompilationOutput = Decompiler.Decompile(gettextCode, new GlobalDecompileContext(null, false)).Replace("\r\n", "\n").Split('\n'); - else - { - try - { - string path = Path.Combine(TempPath, gettextCode.Name.Content + ".gml"); - if (File.Exists(path)) - decompilationOutput = File.ReadAllText(path).Replace("\r\n", "\n").Split('\n'); - else - decompilationOutput = Decompiler.Decompile(gettextCode, new GlobalDecompileContext(null, false)).Replace("\r\n", "\n").Split('\n'); - } - catch - { - decompilationOutput = Decompiler.Decompile(gettextCode, new GlobalDecompileContext(null, false)).Replace("\r\n", "\n").Split('\n'); - } - } - Regex textdataRegex = new Regex("^ds_map_add\\(global\\.text_data_en, \\\"(.*)\\\", \\\"(.*)\\\"\\)"); - foreach (var line in decompilationOutput) - { - Match m = textdataRegex.Match(line); - if (m.Success) - { - try - { - gettext.Add(m.Groups[1].Value, m.Groups[2].Value); - } - catch (ArgumentException) - { - MessageBox.Show("There is a duplicate key in textdata_en, being " + m.Groups[1].Value + ". This may cause errors in the comment display of text.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); - } - catch - { - MessageBox.Show("Unknown error in textdata_en. This may cause errors in the comment display of text.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); - } - } - } - } - - public static Dictionary gettextJSON = null; - private string UpdateGettextJSON(string json) - { - try - { - gettextJSON = JsonConvert.DeserializeObject>(json); - } - catch (Exception e) - { - gettextJSON = new Dictionary(); - return "Failed to parse language file: " + e.Message; - } - return null; - } - - private async Task DecompileCode(UndertaleCode code, bool first, LoaderDialog existingDialog = null) - { - DecompiledEditor.IsReadOnly = true; - DecompiledEditor.TextArea.ClearSelection(); - if (code.ParentEntry != null) - { - DecompiledEditor.Text = "// This code entry is a reference to an anonymous function within " + code.ParentEntry.Name.Content + ", view it there"; - CurrentDecompiled = code; - existingDialog?.TryClose(); - } - else - { - LoaderDialog dialog; - if (existingDialog != null) - { - dialog = existingDialog; - dialog.Message = "Decompiling, please wait..."; - } - else - { - dialog = new LoaderDialog("Decompiling", "Decompiling, please wait... This can take a while on complex scripts."); - dialog.Owner = Window.GetWindow(this); - try - { - _ = Dispatcher.BeginInvoke(new Action(() => { if (!dialog.IsClosed) dialog.TryShowDialog(); })); - } - catch - { - // This is still a problem in rare cases for some unknown reason - } - } - - bool openSaveDialog = false; - - UndertaleCode gettextCode = null; - if (gettext == null) - gettextCode = mainWindow.Data.Code.ByName("gml_Script_textdata_en"); - - string dataPath = Path.GetDirectoryName(mainWindow.FilePath); - string gettextJsonPath = null; - if (dataPath is not null) - { - gettextJsonPath = Path.Combine(dataPath, "lang", "lang_en.json"); - if (!File.Exists(gettextJsonPath)) - gettextJsonPath = Path.Combine(dataPath, "lang", "lang_en_ch1.json"); - } - - var dataa = mainWindow.Data; - Task t = Task.Run(() => - { - GlobalDecompileContext context = new GlobalDecompileContext(dataa, false); - string decompiled = null; - Exception e = null; - try - { - string path = Path.Combine(TempPath, code.Name.Content + ".gml"); - if (!SettingsWindow.ProfileModeEnabled || !File.Exists(path)) - { - decompiled = Decompiler.Decompile(code, context); - } - else - decompiled = File.ReadAllText(path); - } - catch (Exception ex) - { - e = ex; - } - - if (gettextCode != null) - UpdateGettext(gettextCode); - - try - { - if (gettextJSON == null && gettextJsonPath != null && File.Exists(gettextJsonPath)) - { - string err = UpdateGettextJSON(File.ReadAllText(gettextJsonPath)); - if (err != null) - e = new Exception(err); - } - } - catch (Exception exc) - { - MessageBox.Show(exc.ToString()); - } - - if (decompiled != null) - { - string[] decompiledLines; - if (gettext != null && decompiled.Contains("scr_gettext")) - { - decompiledLines = decompiled.Split('\n'); - for (int i = 0; i < decompiledLines.Length; i++) - { - var matches = Regex.Matches(decompiledLines[i], "scr_gettext\\(\\\"(\\w*)\\\"\\)"); - foreach (Match match in matches) - { - if (match.Success) - { - if (gettext.TryGetValue(match.Groups[1].Value, out string text)) - decompiledLines[i] += $" // {text}"; - } - } - } - decompiled = string.Join('\n', decompiledLines); - } - else if (gettextJSON != null && decompiled.Contains("scr_84_get_lang_string")) - { - decompiledLines = decompiled.Split('\n'); - for (int i = 0; i < decompiledLines.Length; i++) - { - var matches = Regex.Matches(decompiledLines[i], "scr_84_get_lang_string(\\w*)\\(\\\"(\\w*)\\\"\\)"); - foreach (Match match in matches) - { - if (match.Success) - { - if (gettextJSON.TryGetValue(match.Groups[^1].Value, out string text)) - decompiledLines[i] += $" // {text}"; - } - } - } - decompiled = string.Join('\n', decompiledLines); - } - } - - Dispatcher.Invoke(() => - { - if (DataContext != code) - return; // Switched to another code entry or otherwise - - DecompiledEditor.Document.BeginUpdate(); - if (e != null) - DecompiledEditor.Document.Text = "/* EXCEPTION!\n " + e.ToString() + "\n*/"; - else if (decompiled != null) - { - DecompiledEditor.Document.Text = decompiled; - CurrentLocals = new List(); - - var locals = dataa.CodeLocals.ByName(code.Name.Content); - if (locals != null) - { - foreach (var local in locals.Locals) - CurrentLocals.Add(local.Name.Content); - } - - if (existingDialog is not null) //if code was edited (and compiles after it) - { - dataa.GMLCacheChanged.Add(code.Name.Content); - dataa.GMLCacheFailed?.Remove(code.Name.Content); //remove that code name, since that code compiles now - - openSaveDialog = mainWindow.IsSaving; - } - } - DecompiledEditor.Document.EndUpdate(); - DecompiledEditor.IsReadOnly = false; - if (first) - DecompiledEditor.Document.UndoStack.ClearAll(); - DecompiledChanged = false; - - CurrentDecompiled = code; - dialog.Hide(); - }); - }); - await t; - dialog.Close(); - - mainWindow.IsSaving = false; - - if (openSaveDialog) - await mainWindow.DoSaveDialog(); - } - } - - private async void GraphCode(UndertaleCode code) - { - if (code.ParentEntry != null) - { - GraphView.Source = null; - CurrentGraphed = code; - return; - } - - LoaderDialog dialog = new LoaderDialog("Generating graph", "Generating graph, please wait..."); - dialog.Owner = Window.GetWindow(this); - Task t = Task.Run(() => - { - ImageSource image = null; - try - { - code.UpdateAddresses(); - List entryPoints = new List(); - entryPoints.Add(0); - foreach (UndertaleCode duplicate in code.ChildEntries) - entryPoints.Add(duplicate.Offset / 4); - var blocks = Decompiler.DecompileFlowGraph(code, entryPoints); - string dot = Decompiler.ExportFlowGraph(blocks); - - try - { - var getStartProcessQuery = new GetStartProcessQuery(); - var getProcessStartInfoQuery = new GetProcessStartInfoQuery(); - var registerLayoutPluginCommand = new RegisterLayoutPluginCommand(getProcessStartInfoQuery, getStartProcessQuery); - var wrapper = new GraphGeneration(getStartProcessQuery, getProcessStartInfoQuery, registerLayoutPluginCommand); - wrapper.GraphvizPath = Settings.Instance.GraphVizPath; - - byte[] output = wrapper.GenerateGraph(dot, Enums.GraphReturnType.Png); // TODO: Use SVG instead - - image = new ImageSourceConverter().ConvertFrom(output) as ImageSource; - } - catch (Exception e) - { - Debug.WriteLine(e.ToString()); - if (MessageBox.Show("Unable to execute GraphViz: " + e.Message + "\nMake sure you have downloaded it and set the path in settings.\nDo you want to open the download page now?", "Graph generation failed", MessageBoxButton.YesNo, MessageBoxImage.Error) == MessageBoxResult.Yes) - MainWindow.OpenBrowser("https://graphviz.gitlab.io/_pages/Download/Download_windows.html"); - } - } - catch (Exception e) - { - Debug.WriteLine(e.ToString()); - MessageBox.Show(e.Message, "Graph generation failed", MessageBoxButton.OK, MessageBoxImage.Error); - } - - Dispatcher.Invoke(() => - { - GraphView.Source = image; - CurrentGraphed = code; - dialog.Hide(); - }); - }); - dialog.ShowDialog(); - await t; - } - - private void DecompiledEditor_GotFocus(object sender, RoutedEventArgs e) - { - if (DecompiledEditor.IsReadOnly) - return; - DecompiledFocused = true; - } - - private static string Truncate(string value, int maxChars) - { - return value.Length <= maxChars ? value : value.Substring(0, maxChars) + "..."; - } - - private async Task DecompiledLostFocusBody(object sender, RoutedEventArgs e) - { - if (!DecompiledFocused) - return; - if (DecompiledEditor.IsReadOnly) - return; - DecompiledFocused = false; - - if (!DecompiledChanged) - return; - - UndertaleCode code; - if (DecompiledSkipped) - { - code = CurrentDecompiled; - DecompiledSkipped = false; - } - else - code = this.DataContext as UndertaleCode; - - if (code == null) - { - if (IsLoaded) - code = CurrentDecompiled; // switched to the tab with different object type - else - return; // probably loaded another data.win or something. - } - - if (code.ParentEntry != null) - return; - - // Check to make sure this isn't an element inside of the textbox, or another tab - IInputElement elem = Keyboard.FocusedElement; - if (elem is UIElement) - { - if (e != null && e.RoutedEvent?.Name != "CtrlK" && (elem as UIElement).IsDescendantOf(DecompiledEditor)) - return; - } - - UndertaleData data = mainWindow.Data; - - LoaderDialog dialog = new LoaderDialog("Compiling", "Compiling, please wait..."); - dialog.Owner = Window.GetWindow(this); - try - { - _ = Dispatcher.BeginInvoke(new Action(() => { if (!dialog.IsClosed) dialog.TryShowDialog(); })); - } - catch - { - // This is still a problem in rare cases for some unknown reason - } - - CompileContext compileContext = null; - string text = DecompiledEditor.Text; - var dispatcher = Dispatcher; - Task t = Task.Run(() => - { - compileContext = Compiler.CompileGMLText(text, data, code, (f) => { dispatcher.Invoke(() => f()); }); - }); - await t; - - if (compileContext == null) - { - dialog.TryClose(); - MessageBox.Show("Compile context was null for some reason...", "This shouldn't happen", MessageBoxButton.OK, MessageBoxImage.Error); - return; - } - - if (compileContext.HasError) - { - dialog.TryClose(); - MessageBox.Show(Truncate(compileContext.ResultError, 512), "Compiler error", MessageBoxButton.OK, MessageBoxImage.Error); - return; - } - - if (!compileContext.SuccessfulCompile) - { - dialog.TryClose(); - MessageBox.Show("(unknown error message)", "Compile failed", MessageBoxButton.OK, MessageBoxImage.Error); - return; - } - - code.Replace(compileContext.ResultAssembly); - - if (!mainWindow.Data.GMS2_3) - { - try - { - string path = Path.Combine(TempPath, code.Name.Content + ".gml"); - if (SettingsWindow.ProfileModeEnabled) - { - // Write text, only if in the profile mode. - File.WriteAllText(path, DecompiledEditor.Text); - } - else - { - // Destroy file with comments if it's been edited outside the profile mode. - // We're dealing with the decompiled code only, it has to happen. - // Otherwise it will cause a desync, which is more important to prevent. - if (File.Exists(path)) - File.Delete(path); - } - } - catch (Exception exc) - { - MessageBox.Show("Error during writing of GML code to profile:\n" + exc.ToString()); - } - } - - // Invalidate gettext if necessary - if (code.Name.Content == "gml_Script_textdata_en") - gettext = null; - - // Show new code, decompiled. - CurrentDisassembled = null; - CurrentDecompiled = null; - CurrentGraphed = null; - - // Tab switch - if (e == null) - { - dialog.TryClose(); - return; - } - - // Decompile new code - await DecompileCode(code, false, dialog); - - //GMLCacheChanged.Add() is inside DecompileCode() - } - private void DecompiledEditor_LostFocus(object sender, RoutedEventArgs e) - { - _ = DecompiledLostFocusBody(sender, e); - } - - private void DisassemblyEditor_GotFocus(object sender, RoutedEventArgs e) - { - if (DisassemblyEditor.IsReadOnly) - return; - DisassemblyFocused = true; - } - - private void DisassemblyEditor_LostFocus(object sender, RoutedEventArgs e) - { - if (!DisassemblyFocused) - return; - if (DisassemblyEditor.IsReadOnly) - return; - DisassemblyFocused = false; - - if (!DisassemblyChanged) - return; - - UndertaleCode code; - if (DisassemblySkipped) - { - code = CurrentDisassembled; - DisassemblySkipped = false; - } - else - code = this.DataContext as UndertaleCode; - - if (code == null) - { - if (IsLoaded) - code = CurrentDisassembled; // switched to the tab with different object type - else - return; // probably loaded another data.win or something. - } - - // Check to make sure this isn't an element inside of the textbox, or another tab - IInputElement elem = Keyboard.FocusedElement; - if (elem is UIElement) - { - if (e != null && e.RoutedEvent?.Name != "CtrlK" && (elem as UIElement).IsDescendantOf(DisassemblyEditor)) - return; - } - - UndertaleData data = mainWindow.Data; - try - { - var instructions = Assembler.Assemble(DisassemblyEditor.Text, data); - code.Replace(instructions); - mainWindow.NukeProfileGML(code.Name.Content); - } - catch (Exception ex) - { - MessageBox.Show(ex.ToString(), "Assembler error", MessageBoxButton.OK, MessageBoxImage.Error); - return; - } - - // Get rid of old code - CurrentDisassembled = null; - CurrentDecompiled = null; - CurrentGraphed = null; - - // Tab switch - if (e == null) - return; - - // Disassemble new code - DisassembleCode(code, false); - - if (!DisassemblyEditor.IsReadOnly) - { - data.GMLCacheChanged.Add(code.Name.Content); - - if (mainWindow.IsSaving) - { - mainWindow.IsSaving = false; - - _ = mainWindow.DoSaveDialog(); - } - } - } - - // Based on https://stackoverflow.com/questions/28379206/custom-hyperlinks-using-avalonedit - public class NumberGenerator : VisualLineElementGenerator - { - readonly static Regex regex = new Regex(@"-?\d+\.?"); - - public NumberGenerator() - { - } - - Match FindMatch(int startOffset, Regex r) - { - // fetch the end offset of the VisualLine being generated - int endOffset = CurrentContext.VisualLine.LastDocumentLine.EndOffset; - TextDocument document = CurrentContext.Document; - string relevantText = document.GetText(startOffset, endOffset - startOffset); - return r.Match(relevantText); - } - - /// Gets the first offset >= startOffset where the generator wants to construct - /// an element. - /// Return -1 to signal no interest. - public override int GetFirstInterestedOffset(int startOffset) - { - Match m = FindMatch(startOffset, regex); - - var textArea = CurrentContext.TextView.GetService(typeof(TextArea)) as TextArea; - var highlighter = textArea.GetService(typeof(IHighlighter)) as IHighlighter; - int line = CurrentContext.Document.GetLocation(startOffset).Line; - HighlightedLine highlighted = null; - try - { - highlighted = highlighter.HighlightLine(line); - } - catch - { - } - - while (m.Success) - { - int res = startOffset + m.Index; - int currLine = CurrentContext.Document.GetLocation(res).Line; - if (currLine != line) - { - line = currLine; - highlighted = highlighter.HighlightLine(line); - } - - foreach (var section in highlighted.Sections) - { - if (section.Color.Name == "Number" && - section.Offset == res) - return res; - } - - startOffset += m.Length; - m = FindMatch(startOffset, regex); - } - - return -1; - } - - /// Constructs an element at the specified offset. - /// May return null if no element should be constructed. - public override VisualLineElement ConstructElement(int offset) - { - Match m = FindMatch(offset, regex); - - if (m.Success && m.Index == 0) - { - var line = new ClickVisualLineText(m.Value, CurrentContext.VisualLine, m.Length); - var doc = CurrentContext.Document; - var textArea = CurrentContext.TextView.GetService(typeof(TextArea)) as TextArea; - var editor = textArea.GetService(typeof(TextEditor)) as TextEditor; - var parent = VisualTreeHelper.GetParent(editor); - do - { - if ((parent as FrameworkElement) is UserControl) - break; - parent = VisualTreeHelper.GetParent(parent); - } while (parent != null); - line.Clicked += (text) => - { - if (text.EndsWith(".")) - return; - if (int.TryParse(text, out int id)) - { - (parent as UndertaleCodeEditor).DecompiledFocused = true; - UndertaleData data = mainWindow.Data; - - List possibleObjects = new List(); - if (id >= 0) - { - if (id < data.Sprites.Count) - possibleObjects.Add(data.Sprites[id]); - if (id < data.Rooms.Count) - possibleObjects.Add(data.Rooms[id]); - if (id < data.GameObjects.Count) - possibleObjects.Add(data.GameObjects[id]); - if (id < data.Backgrounds.Count) - possibleObjects.Add(data.Backgrounds[id]); - if (id < data.Scripts.Count) - possibleObjects.Add(data.Scripts[id]); - if (id < data.Paths.Count) - possibleObjects.Add(data.Paths[id]); - if (id < data.Fonts.Count) - possibleObjects.Add(data.Fonts[id]); - if (id < data.Sounds.Count) - possibleObjects.Add(data.Sounds[id]); - if (id < data.Shaders.Count) - possibleObjects.Add(data.Shaders[id]); - if (id < data.Timelines.Count) - possibleObjects.Add(data.Timelines[id]); - } - - ContextMenu contextMenu = new ContextMenu(); - foreach (UndertaleObject obj in possibleObjects) - { - MenuItem item = new MenuItem(); - item.Header = obj.ToString().Replace("_", "__"); - item.Click += (sender2, ev2) => - { - if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) - mainWindow.ChangeSelection(obj); - else - { - doc.Replace(line.ParentVisualLine.StartOffset + line.RelativeTextOffset, - text.Length, (obj as UndertaleNamedResource).Name.Content, null); - (parent as UndertaleCodeEditor).DecompiledChanged = true; - } - }; - contextMenu.Items.Add(item); - } - if (id > 0x00050000) - { - MenuItem item = new MenuItem(); - item.Header = "0x" + id.ToString("X6") + " (color)"; - item.Click += (sender2, ev2) => - { - if (!((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)) - { - doc.Replace(line.ParentVisualLine.StartOffset + line.RelativeTextOffset, - text.Length, "0x" + id.ToString("X6"), null); - (parent as UndertaleCodeEditor).DecompiledChanged = true; - } - }; - contextMenu.Items.Add(item); - } - BuiltinList list = mainWindow.Data.BuiltinList; - var myKey = list.Constants.FirstOrDefault(x => x.Value == (double)id).Key; - if (myKey != null) - { - MenuItem item = new MenuItem(); - item.Header = myKey.Replace("_", "__") + " (constant)"; - item.Click += (sender2, ev2) => - { - if (!((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)) - { - doc.Replace(line.ParentVisualLine.StartOffset + line.RelativeTextOffset, - text.Length, myKey, null); - (parent as UndertaleCodeEditor).DecompiledChanged = true; - } - }; - contextMenu.Items.Add(item); - } - contextMenu.Items.Add(new MenuItem() { Header = id + " (number)", IsEnabled = false }); - - contextMenu.IsOpen = true; - } - }; - return line; - } - - return null; - } - } - - public class NameGenerator : VisualLineElementGenerator - { - readonly static Regex regex = new Regex(@"[_a-zA-Z][_a-zA-Z0-9]*"); - - public NameGenerator() - { - } - - Match FindMatch(int startOffset, Regex r) - { - // fetch the end offset of the VisualLine being generated - int endOffset = CurrentContext.VisualLine.LastDocumentLine.EndOffset; - TextDocument document = CurrentContext.Document; - string relevantText = document.GetText(startOffset, endOffset - startOffset); - return r.Match(relevantText); - } - - /// Gets the first offset >= startOffset where the generator wants to construct - /// an element. - /// Return -1 to signal no interest. - public override int GetFirstInterestedOffset(int startOffset) - { - Match m = FindMatch(startOffset, regex); - - var textArea = CurrentContext.TextView.GetService(typeof(TextArea)) as TextArea; - var highlighter = textArea.GetService(typeof(IHighlighter)) as IHighlighter; - int line = CurrentContext.Document.GetLocation(startOffset).Line; - HighlightedLine highlighted = null; - try - { - highlighted = highlighter.HighlightLine(line); - } - catch - { - } - - while (m.Success) - { - int res = startOffset + m.Index; - int currLine = CurrentContext.Document.GetLocation(res).Line; - if (currLine != line) - { - line = currLine; - highlighted = highlighter.HighlightLine(line); - } - - foreach (var section in highlighted.Sections) - { - if (section.Color.Name == "Identifier" || section.Color.Name == "Function") - { - if (section.Offset == res) - return res; - } - } - - startOffset += m.Length; - m = FindMatch(startOffset, regex); - } - return -1; - } - - /// Constructs an element at the specified offset. - /// May return null if no element should be constructed. - public override VisualLineElement ConstructElement(int offset) - { - Match m = FindMatch(offset, regex); - - if (m.Success && m.Index == 0) - { - UndertaleData data = mainWindow.Data; - bool func = (offset + m.Length + 1 < CurrentContext.VisualLine.LastDocumentLine.EndOffset) && - (CurrentContext.Document.GetCharAt(offset + m.Length) == '('); - UndertaleNamedResource val = null; - - var doc = CurrentContext.Document; - var textArea = CurrentContext.TextView.GetService(typeof(TextArea)) as TextArea; - var editor = textArea.GetService(typeof(TextEditor)) as TextEditor; - var parent = VisualTreeHelper.GetParent(editor); - do - { - if ((parent as FrameworkElement) is UserControl) - break; - parent = VisualTreeHelper.GetParent(parent); - } while (parent != null); - - // Process the content of this identifier/function - if (func) - { - val = null; - if (!data.GMS2_3) // in GMS2.3 every custom "function" is in fact a member variable and scripts are never referenced directly - val = data.Scripts.ByName(m.Value); - if (val == null) - { - val = data.Functions.ByName(m.Value); - if (data.GMS2_3) - { - if (val != null) - { - if (data.Code.ByName(val.Name.Content) != null) - val = null; // in GMS2.3 every custom "function" is in fact a member variable, and the names in functions make no sense (they have the gml_Script_ prefix) - } - else - { - // Resolve 2.3 sub-functions for their parent entry - UndertaleFunction f = null; - if (data.KnownSubFunctions?.TryGetValue(m.Value, out f) == true) - val = data.Scripts.ByName(f.Name.Content).Code?.ParentEntry; - } - } - } - if (val == null) - { - if (data.BuiltinList.Functions.ContainsKey(m.Value)) - { - var res = new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, - new SolidColorBrush(Color.FromRgb(0xFF, 0xB8, 0x71))); - res.Bold = true; - return res; - } - } - } - else - { - val = data.ByName(m.Value); - if (data.GMS2_3 & val is UndertaleScript) - val = null; // in GMS2.3 scripts are never referenced directly - } - if (val == null) - { - if (offset >= 7) - { - if (CurrentContext.Document.GetText(offset - 7, 7) == "global.") - { - return new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, - new SolidColorBrush(Color.FromRgb(0xF9, 0x7B, 0xF9))); - } - } - if (data.BuiltinList.Constants.ContainsKey(m.Value)) - return new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, - new SolidColorBrush(Color.FromRgb(0xFF, 0x80, 0x80))); - if (data.BuiltinList.GlobalNotArray.ContainsKey(m.Value) || - data.BuiltinList.Instance.ContainsKey(m.Value) || - data.BuiltinList.GlobalArray.ContainsKey(m.Value)) - return new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, - new SolidColorBrush(Color.FromRgb(0x58, 0xE3, 0x5A))); - if ((parent as UndertaleCodeEditor).CurrentLocals.Contains(m.Value)) - return new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, - new SolidColorBrush(Color.FromRgb(0xFF, 0xF8, 0x99))); - return null; - } - - var line = new ClickVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, - func ? new SolidColorBrush(Color.FromRgb(0xFF, 0xB8, 0x71)) : - new SolidColorBrush(Color.FromRgb(0xFF, 0x80, 0x80))); - if (func) - line.Bold = true; - line.Clicked += (text) => - { - mainWindow.ChangeSelection(val); - }; - - return line; - } - - return null; - } - } - - public class ColorVisualLineText : VisualLineText - { - private string Text { get; set; } - private Brush ForegroundBrush { get; set; } - - public bool Bold { get; set; } = false; - - /// - /// Creates a visual line text element with the specified length. - /// It uses the and its - /// to find the actual text string. - /// - public ColorVisualLineText(string text, VisualLine parentVisualLine, int length, Brush foregroundBrush) - : base(parentVisualLine, length) - { - Text = text; - ForegroundBrush = foregroundBrush; - } - - public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) - { - if (ForegroundBrush != null) - TextRunProperties.SetForegroundBrush(ForegroundBrush); - if (Bold) - TextRunProperties.SetTypeface(new Typeface(TextRunProperties.Typeface.FontFamily, FontStyles.Normal, FontWeights.Bold, FontStretches.Normal)); - return base.CreateTextRun(startVisualColumn, context); - } - - protected override VisualLineText CreateInstance(int length) - { - return new ColorVisualLineText(Text, ParentVisualLine, length, null); - } - } - - public class ClickVisualLineText : VisualLineText - { - - public delegate void ClickHandler(string text); - - public event ClickHandler Clicked; - - private string Text { get; set; } - private Brush ForegroundBrush { get; set; } - - public bool Bold { get; set; } = false; - - /// - /// Creates a visual line text element with the specified length. - /// It uses the and its - /// to find the actual text string. - /// - public ClickVisualLineText(string text, VisualLine parentVisualLine, int length, Brush foregroundBrush = null) - : base(parentVisualLine, length) - { - Text = text; - ForegroundBrush = foregroundBrush; - } - - - public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) - { - if (ForegroundBrush != null) - TextRunProperties.SetForegroundBrush(ForegroundBrush); - if (Bold) - TextRunProperties.SetTypeface(new Typeface(TextRunProperties.Typeface.FontFamily, FontStyles.Normal, FontWeights.Bold, FontStretches.Normal)); - return base.CreateTextRun(startVisualColumn, context); - } - - bool LinkIsClickable() - { - if (string.IsNullOrEmpty(Text)) - return false; - return (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control; - } - - - protected override void OnQueryCursor(QueryCursorEventArgs e) - { - if (LinkIsClickable()) - { - e.Handled = true; - e.Cursor = Cursors.Hand; - } - } - - protected override void OnMouseDown(MouseButtonEventArgs e) - { - if (e.Handled) - return; - if ((e.ChangedButton == System.Windows.Input.MouseButton.Left && LinkIsClickable()) || - e.ChangedButton == System.Windows.Input.MouseButton.Middle) - { - if (Clicked != null) - { - Clicked(Text); - e.Handled = true; - } - } - } - - protected override VisualLineText CreateInstance(int length) - { - var res = new ClickVisualLineText(Text, ParentVisualLine, length); - res.Clicked += Clicked; - return res; - } - } - } -} + /// + /// Logika interakcji dla klasy UndertaleCodeEditor.xaml + /// + [SupportedOSPlatform("windows7.0")] + public partial class UndertaleCodeEditor : DataUserControl + { + private static MainWindow mainWindow = Application.Current.MainWindow as MainWindow; + + public UndertaleCode CurrentDisassembled = null; + public UndertaleCode CurrentDecompiled = null; + public List CurrentLocals = null; + public UndertaleCode CurrentGraphed = null; + public string ProfileHash = mainWindow.ProfileHash; + public string MainPath = Path.Combine(Settings.ProfilesFolder, mainWindow.ProfileHash, "Main"); + public string TempPath = Path.Combine(Settings.ProfilesFolder, mainWindow.ProfileHash, "Temp"); + + public bool DecompiledFocused = false; + public bool DecompiledChanged = false; + public bool DecompiledYet = false; + public bool DecompiledSkipped = false; + public SearchPanel DecompiledSearchPanel; + + public bool DisassemblyFocused = false; + public bool DisassemblyChanged = false; + public bool DisassembledYet = false; + public bool DisassemblySkipped = false; + public SearchPanel DisassemblySearchPanel; + + public static RoutedUICommand Compile = new RoutedUICommand("Compile code", "Compile", typeof(UndertaleCodeEditor)); + + public UndertaleCodeEditor() + { + InitializeComponent(); + + // Decompiled editor styling and functionality + DecompiledSearchPanel = SearchPanel.Install(DecompiledEditor.TextArea); + DecompiledSearchPanel.MarkerBrush = new SolidColorBrush(Color.FromRgb(90, 90, 90)); + + using (Stream stream = this.GetType().Assembly.GetManifestResourceStream("UndertaleModTool.Resources.GML.xshd")) + { + using (XmlTextReader reader = new XmlTextReader(stream)) + { + DecompiledEditor.SyntaxHighlighting = HighlightingLoader.Load(reader, HighlightingManager.Instance); + var def = DecompiledEditor.SyntaxHighlighting; + if (mainWindow.Data.GeneralInfo.Major < 2) + { + foreach (var span in def.MainRuleSet.Spans) + { + string expr = span.StartExpression.ToString(); + if (expr == "\"" || expr == "'") + { + span.RuleSet.Spans.Clear(); + } + } + } + // This was an attempt to only highlight + // GMS 2.3+ keywords if the game is + // made in such a version. + // However despite what StackOverflow + // says, this isn't working so it's just + // hardcoded in the XML for now + /* + if(mainWindow.Data.GMS2_3) + { + HighlightingColor color = null; + foreach (var rule in def.MainRuleSet.Rules) + { + if (rule.Regex.IsMatch("if")) + { + color = rule.Color; + break; + } + } + if (color != null) + { + string[] keywords = + { + "new", + "function", + "keywords" + }; + var rule = new HighlightingRule(); + var regex = String.Format(@"\b(?>{0})\b", String.Join("|", keywords)); + + rule.Regex = new Regex(regex); + rule.Color = color; + + def.MainRuleSet.Rules.Add(rule); + } + }*/ + } + } + + DecompiledEditor.Options.ConvertTabsToSpaces = true; + + DecompiledEditor.TextArea.TextView.ElementGenerators.Add(new NumberGenerator()); + DecompiledEditor.TextArea.TextView.ElementGenerators.Add(new NameGenerator()); + + DecompiledEditor.TextArea.TextView.Options.HighlightCurrentLine = true; + DecompiledEditor.TextArea.TextView.CurrentLineBackground = new SolidColorBrush(Color.FromRgb(60, 60, 60)); + DecompiledEditor.TextArea.TextView.CurrentLineBorder = new Pen() { Thickness = 0 }; + + DecompiledEditor.Document.TextChanged += (s, e) => + { + DecompiledFocused = true; + DecompiledChanged = true; + }; + + DecompiledEditor.TextArea.SelectionBrush = new SolidColorBrush(Color.FromRgb(100, 100, 100)); + DecompiledEditor.TextArea.SelectionForeground = null; + DecompiledEditor.TextArea.SelectionBorder = null; + DecompiledEditor.TextArea.SelectionCornerRadius = 0; + + // Disassembly editor styling and functionality + DisassemblySearchPanel = SearchPanel.Install(DisassemblyEditor.TextArea); + DisassemblySearchPanel.MarkerBrush = new SolidColorBrush(Color.FromRgb(90, 90, 90)); + + using (Stream stream = this.GetType().Assembly.GetManifestResourceStream("UndertaleModTool.Resources.VMASM.xshd")) + { + using (XmlTextReader reader = new XmlTextReader(stream)) + { + DisassemblyEditor.SyntaxHighlighting = HighlightingLoader.Load(reader, HighlightingManager.Instance); + } + } + + DisassemblyEditor.TextArea.TextView.ElementGenerators.Add(new NameGenerator()); + + DisassemblyEditor.TextArea.TextView.Options.HighlightCurrentLine = true; + DisassemblyEditor.TextArea.TextView.CurrentLineBackground = new SolidColorBrush(Color.FromRgb(60, 60, 60)); + DisassemblyEditor.TextArea.TextView.CurrentLineBorder = new Pen() { Thickness = 0 }; + + DisassemblyEditor.Document.TextChanged += (s, e) => DisassemblyChanged = true; + + DisassemblyEditor.TextArea.SelectionBrush = new SolidColorBrush(Color.FromRgb(100, 100, 100)); + DisassemblyEditor.TextArea.SelectionForeground = null; + DisassemblyEditor.TextArea.SelectionBorder = null; + DisassemblyEditor.TextArea.SelectionCornerRadius = 0; + } + + private async void TabControl_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + UndertaleCode code = this.DataContext as UndertaleCode; + Directory.CreateDirectory(MainPath); + Directory.CreateDirectory(TempPath); + if (code == null) + return; + DecompiledSearchPanel.Close(); + DisassemblySearchPanel.Close(); + await DecompiledLostFocusBody(sender, null); + DisassemblyEditor_LostFocus(sender, null); + if (DisassemblyTab.IsSelected && code != CurrentDisassembled) + { + DisassembleCode(code, !DisassembledYet); + DisassembledYet = true; + } + if (DecompiledTab.IsSelected && code != CurrentDecompiled) + { + _ = DecompileCode(code, !DecompiledYet); + DecompiledYet = true; + } + if (GraphTab.IsSelected && code != CurrentGraphed) + { + GraphCode(code); + } + } + + private async void UserControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) + { + UndertaleCode code = this.DataContext as UndertaleCode; + if (code == null) + return; + + // compile/disassemble previously edited code (save changes) + if (DecompiledTab.IsSelected && DecompiledFocused && DecompiledChanged && + CurrentDecompiled is not null && CurrentDecompiled != code) + { + DecompiledSkipped = true; + DecompiledEditor_LostFocus(sender, null); + + } + else if (DisassemblyTab.IsSelected && DisassemblyFocused && DisassemblyChanged && + CurrentDisassembled is not null && CurrentDisassembled != code) + { + DisassemblySkipped = true; + DisassemblyEditor_LostFocus(sender, null); + } + + DecompiledEditor_LostFocus(sender, null); + DisassemblyEditor_LostFocus(sender, null); + + if (MainWindow.CodeEditorDecompile != Unstated) //if opened from the code search results "link" + { + if (MainWindow.CodeEditorDecompile == DontDecompile && code != CurrentDisassembled) + { + if (CodeModeTabs.SelectedItem != DisassemblyTab) + CodeModeTabs.SelectedItem = DisassemblyTab; + else + DisassembleCode(code, true); + } + + if (MainWindow.CodeEditorDecompile == Decompile && code != CurrentDecompiled) + { + if (CodeModeTabs.SelectedItem != DecompiledTab) + CodeModeTabs.SelectedItem = DecompiledTab; + else + _ = DecompileCode(code, true); + } + + MainWindow.CodeEditorDecompile = Unstated; + } + else + { + if (DisassemblyTab.IsSelected && code != CurrentDisassembled) + { + DisassembleCode(code, true); + } + if (DecompiledTab.IsSelected && code != CurrentDecompiled) + { + _ = DecompileCode(code, true); + } + if (GraphTab.IsSelected && code != CurrentGraphed) + { + GraphCode(code); + } + } + } + + public static readonly RoutedEvent CtrlKEvent = EventManager.RegisterRoutedEvent( + "CtrlK", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(UndertaleCodeEditor)); + + private async Task CompileCommandBody(object sender, EventArgs e) + { + if (DecompiledFocused) + { + await DecompiledLostFocusBody(sender, new RoutedEventArgs(CtrlKEvent)); + } + else if (DisassemblyFocused) + { + DisassemblyEditor_LostFocus(sender, new RoutedEventArgs(CtrlKEvent)); + DisassemblyEditor_GotFocus(sender, null); + } + + await Task.Delay(1); //dummy await + } + private void Command_Compile(object sender, EventArgs e) + { + _ = CompileCommandBody(sender, e); + } + public async Task SaveChanges() + { + await CompileCommandBody(null, null); + } + + private void DisassembleCode(UndertaleCode code, bool first) + { + code.UpdateAddresses(); + + string text; + + DisassemblyEditor.TextArea.ClearSelection(); + if (code.ParentEntry != null) + { + DisassemblyEditor.IsReadOnly = true; + text = "; This code entry is a reference to an anonymous function within " + code.ParentEntry.Name.Content + ", view it there"; + } + else + { + DisassemblyEditor.IsReadOnly = false; + + var data = mainWindow.Data; + text = code.Disassemble(data.Variables, data.CodeLocals.For(code)); + + CurrentLocals = new List(); + } + + DisassemblyEditor.Document.BeginUpdate(); + DisassemblyEditor.Document.Text = text; + DisassemblyEditor.Document.EndUpdate(); + + if (first) + DisassemblyEditor.Document.UndoStack.ClearAll(); + + CurrentDisassembled = code; + DisassemblyChanged = false; + } + + public static Dictionary gettext = null; + private void UpdateGettext(UndertaleCode gettextCode) + { + gettext = new Dictionary(); + string[] decompilationOutput; + if (!SettingsWindow.ProfileModeEnabled) + decompilationOutput = Decompiler.Decompile(gettextCode, new GlobalDecompileContext(null, false)).Replace("\r\n", "\n").Split('\n'); + else + { + try + { + string path = Path.Combine(TempPath, gettextCode.Name.Content + ".gml"); + if (File.Exists(path)) + decompilationOutput = File.ReadAllText(path).Replace("\r\n", "\n").Split('\n'); + else + decompilationOutput = Decompiler.Decompile(gettextCode, new GlobalDecompileContext(null, false)).Replace("\r\n", "\n").Split('\n'); + } + catch + { + decompilationOutput = Decompiler.Decompile(gettextCode, new GlobalDecompileContext(null, false)).Replace("\r\n", "\n").Split('\n'); + } + } + Regex textdataRegex = new Regex("^ds_map_add\\(global\\.text_data_en, \\\"(.*)\\\", \\\"(.*)\\\"\\)"); + foreach (var line in decompilationOutput) + { + Match m = textdataRegex.Match(line); + if (m.Success) + { + try + { + gettext.Add(m.Groups[1].Value, m.Groups[2].Value); + } + catch (ArgumentException) + { + MessageBox.Show("There is a duplicate key in textdata_en, being " + m.Groups[1].Value + ". This may cause errors in the comment display of text.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + } + catch + { + MessageBox.Show("Unknown error in textdata_en. This may cause errors in the comment display of text.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + } + } + + public static Dictionary gettextJSON = null; + private string UpdateGettextJSON(string json) + { + try + { + gettextJSON = JsonConvert.DeserializeObject>(json); + } + catch (Exception e) + { + gettextJSON = new Dictionary(); + return "Failed to parse language file: " + e.Message; + } + return null; + } + + private async Task DecompileCode(UndertaleCode code, bool first, LoaderDialog existingDialog = null) + { + DecompiledEditor.IsReadOnly = true; + DecompiledEditor.TextArea.ClearSelection(); + if (code.ParentEntry != null) + { + DecompiledEditor.Text = "// This code entry is a reference to an anonymous function within " + code.ParentEntry.Name.Content + ", view it there"; + CurrentDecompiled = code; + existingDialog?.TryClose(); + } + else + { + LoaderDialog dialog; + if (existingDialog != null) + { + dialog = existingDialog; + dialog.Message = "Decompiling, please wait..."; + } + else + { + dialog = new LoaderDialog("Decompiling", "Decompiling, please wait... This can take a while on complex scripts."); + dialog.Owner = Window.GetWindow(this); + try + { + _ = Dispatcher.BeginInvoke(new Action(() => { if (!dialog.IsClosed) dialog.TryShowDialog(); })); + } + catch + { + // This is still a problem in rare cases for some unknown reason + } + } + + bool openSaveDialog = false; + + UndertaleCode gettextCode = null; + if (gettext == null) + gettextCode = mainWindow.Data.Code.ByName("gml_Script_textdata_en"); + + string dataPath = Path.GetDirectoryName(mainWindow.FilePath); + string gettextJsonPath = null; + if (dataPath is not null) + { + gettextJsonPath = Path.Combine(dataPath, "lang", "lang_en.json"); + if (!File.Exists(gettextJsonPath)) + gettextJsonPath = Path.Combine(dataPath, "lang", "lang_en_ch1.json"); + } + + var dataa = mainWindow.Data; + Task t = Task.Run(() => + { + GlobalDecompileContext context = new GlobalDecompileContext(dataa, false); + string decompiled = null; + Exception e = null; + try + { + string path = Path.Combine(TempPath, code.Name.Content + ".gml"); + if (!SettingsWindow.ProfileModeEnabled || !File.Exists(path)) + { + decompiled = Decompiler.Decompile(code, context); + } + else + decompiled = File.ReadAllText(path); + } + catch (Exception ex) + { + e = ex; + } + + if (gettextCode != null) + UpdateGettext(gettextCode); + + try + { + if (gettextJSON == null && gettextJsonPath != null && File.Exists(gettextJsonPath)) + { + string err = UpdateGettextJSON(File.ReadAllText(gettextJsonPath)); + if (err != null) + e = new Exception(err); + } + } + catch (Exception exc) + { + MessageBox.Show(exc.ToString()); + } + + if (decompiled != null) + { + string[] decompiledLines; + if (gettext != null && decompiled.Contains("scr_gettext")) + { + decompiledLines = decompiled.Split('\n'); + for (int i = 0; i < decompiledLines.Length; i++) + { + var matches = Regex.Matches(decompiledLines[i], "scr_gettext\\(\\\"(\\w*)\\\"\\)"); + foreach (Match match in matches) + { + if (match.Success) + { + if (gettext.TryGetValue(match.Groups[1].Value, out string text)) + decompiledLines[i] += $" // {text}"; + } + } + } + decompiled = string.Join('\n', decompiledLines); + } + else if (gettextJSON != null && decompiled.Contains("scr_84_get_lang_string")) + { + decompiledLines = decompiled.Split('\n'); + for (int i = 0; i < decompiledLines.Length; i++) + { + var matches = Regex.Matches(decompiledLines[i], "scr_84_get_lang_string(\\w*)\\(\\\"(\\w*)\\\"\\)"); + foreach (Match match in matches) + { + if (match.Success) + { + if (gettextJSON.TryGetValue(match.Groups[^1].Value, out string text)) + decompiledLines[i] += $" // {text}"; + } + } + } + decompiled = string.Join('\n', decompiledLines); + } + } + + Dispatcher.Invoke(() => + { + if (DataContext != code) + return; // Switched to another code entry or otherwise + + DecompiledEditor.Document.BeginUpdate(); + if (e != null) + DecompiledEditor.Document.Text = "/* EXCEPTION!\n " + e.ToString() + "\n*/"; + else if (decompiled != null) + { + DecompiledEditor.Document.Text = decompiled; + CurrentLocals = new List(); + + var locals = dataa.CodeLocals.ByName(code.Name.Content); + if (locals != null) + { + foreach (var local in locals.Locals) + CurrentLocals.Add(local.Name.Content); + } + + if (existingDialog is not null) //if code was edited (and compiles after it) + { + dataa.GMLCacheChanged.Add(code.Name.Content); + dataa.GMLCacheFailed?.Remove(code.Name.Content); //remove that code name, since that code compiles now + + openSaveDialog = mainWindow.IsSaving; + } + } + DecompiledEditor.Document.EndUpdate(); + DecompiledEditor.IsReadOnly = false; + if (first) + DecompiledEditor.Document.UndoStack.ClearAll(); + DecompiledChanged = false; + + CurrentDecompiled = code; + dialog.Hide(); + }); + }); + await t; + dialog.Close(); + + mainWindow.IsSaving = false; + + if (openSaveDialog) + await mainWindow.DoSaveDialog(); + } + } + + private async void GraphCode(UndertaleCode code) + { + if (code.ParentEntry != null) + { + GraphView.Source = null; + CurrentGraphed = code; + return; + } + + LoaderDialog dialog = new LoaderDialog("Generating graph", "Generating graph, please wait..."); + dialog.Owner = Window.GetWindow(this); + Task t = Task.Run(() => + { + ImageSource image = null; + try + { + code.UpdateAddresses(); + List entryPoints = new List(); + entryPoints.Add(0); + foreach (UndertaleCode duplicate in code.ChildEntries) + entryPoints.Add(duplicate.Offset / 4); + var blocks = Decompiler.DecompileFlowGraph(code, entryPoints); + string dot = Decompiler.ExportFlowGraph(blocks); + + try + { + var getStartProcessQuery = new GetStartProcessQuery(); + var getProcessStartInfoQuery = new GetProcessStartInfoQuery(); + var registerLayoutPluginCommand = new RegisterLayoutPluginCommand(getProcessStartInfoQuery, getStartProcessQuery); + var wrapper = new GraphGeneration(getStartProcessQuery, getProcessStartInfoQuery, registerLayoutPluginCommand); + wrapper.GraphvizPath = Settings.Instance.GraphVizPath; + + byte[] output = wrapper.GenerateGraph(dot, Enums.GraphReturnType.Png); // TODO: Use SVG instead + + image = new ImageSourceConverter().ConvertFrom(output) as ImageSource; + } + catch (Exception e) + { + Debug.WriteLine(e.ToString()); + if (MessageBox.Show("Unable to execute GraphViz: " + e.Message + "\nMake sure you have downloaded it and set the path in settings.\nDo you want to open the download page now?", "Graph generation failed", MessageBoxButton.YesNo, MessageBoxImage.Error) == MessageBoxResult.Yes) + MainWindow.OpenBrowser("https://graphviz.gitlab.io/_pages/Download/Download_windows.html"); + } + } + catch (Exception e) + { + Debug.WriteLine(e.ToString()); + MessageBox.Show(e.Message, "Graph generation failed", MessageBoxButton.OK, MessageBoxImage.Error); + } + + Dispatcher.Invoke(() => + { + GraphView.Source = image; + CurrentGraphed = code; + dialog.Hide(); + }); + }); + dialog.ShowDialog(); + await t; + } + + private void DecompiledEditor_GotFocus(object sender, RoutedEventArgs e) + { + if (DecompiledEditor.IsReadOnly) + return; + DecompiledFocused = true; + } + + private static string Truncate(string value, int maxChars) + { + return value.Length <= maxChars ? value : value.Substring(0, maxChars) + "..."; + } + + private async Task DecompiledLostFocusBody(object sender, RoutedEventArgs e) + { + if (!DecompiledFocused) + return; + if (DecompiledEditor.IsReadOnly) + return; + DecompiledFocused = false; + + if (!DecompiledChanged) + return; + + UndertaleCode code; + if (DecompiledSkipped) + { + code = CurrentDecompiled; + DecompiledSkipped = false; + } + else + code = this.DataContext as UndertaleCode; + + if (code == null) + { + if (IsLoaded) + code = CurrentDecompiled; // switched to the tab with different object type + else + return; // probably loaded another data.win or something. + } + + if (code.ParentEntry != null) + return; + + // Check to make sure this isn't an element inside of the textbox, or another tab + IInputElement elem = Keyboard.FocusedElement; + if (elem is UIElement) + { + if (e != null && e.RoutedEvent?.Name != "CtrlK" && (elem as UIElement).IsDescendantOf(DecompiledEditor)) + return; + } + + UndertaleData data = mainWindow.Data; + + LoaderDialog dialog = new LoaderDialog("Compiling", "Compiling, please wait..."); + dialog.Owner = Window.GetWindow(this); + try + { + _ = Dispatcher.BeginInvoke(new Action(() => { if (!dialog.IsClosed) dialog.TryShowDialog(); })); + } + catch + { + // This is still a problem in rare cases for some unknown reason + } + + CompileContext compileContext = null; + string text = DecompiledEditor.Text; + var dispatcher = Dispatcher; + Task t = Task.Run(() => + { + compileContext = Compiler.CompileGMLText(text, data, code, (f) => { dispatcher.Invoke(() => f()); }); + }); + await t; + + if (compileContext == null) + { + dialog.TryClose(); + MessageBox.Show("Compile context was null for some reason...", "This shouldn't happen", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + + if (compileContext.HasError) + { + dialog.TryClose(); + MessageBox.Show(Truncate(compileContext.ResultError, 512), "Compiler error", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + + if (!compileContext.SuccessfulCompile) + { + dialog.TryClose(); + MessageBox.Show("(unknown error message)", "Compile failed", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + + code.Replace(compileContext.ResultAssembly); + + if (!mainWindow.Data.GMS2_3) + { + try + { + string path = Path.Combine(TempPath, code.Name.Content + ".gml"); + if (SettingsWindow.ProfileModeEnabled) + { + // Write text, only if in the profile mode. + File.WriteAllText(path, DecompiledEditor.Text); + } + else + { + // Destroy file with comments if it's been edited outside the profile mode. + // We're dealing with the decompiled code only, it has to happen. + // Otherwise it will cause a desync, which is more important to prevent. + if (File.Exists(path)) + File.Delete(path); + } + } + catch (Exception exc) + { + MessageBox.Show("Error during writing of GML code to profile:\n" + exc.ToString()); + } + } + + // Invalidate gettext if necessary + if (code.Name.Content == "gml_Script_textdata_en") + gettext = null; + + // Show new code, decompiled. + CurrentDisassembled = null; + CurrentDecompiled = null; + CurrentGraphed = null; + + // Tab switch + if (e == null) + { + dialog.TryClose(); + return; + } + + // Decompile new code + await DecompileCode(code, false, dialog); + + //GMLCacheChanged.Add() is inside DecompileCode() + } + private void DecompiledEditor_LostFocus(object sender, RoutedEventArgs e) + { + _ = DecompiledLostFocusBody(sender, e); + } + + private void DisassemblyEditor_GotFocus(object sender, RoutedEventArgs e) + { + if (DisassemblyEditor.IsReadOnly) + return; + DisassemblyFocused = true; + } + + private void DisassemblyEditor_LostFocus(object sender, RoutedEventArgs e) + { + if (!DisassemblyFocused) + return; + if (DisassemblyEditor.IsReadOnly) + return; + DisassemblyFocused = false; + + if (!DisassemblyChanged) + return; + + UndertaleCode code; + if (DisassemblySkipped) + { + code = CurrentDisassembled; + DisassemblySkipped = false; + } + else + code = this.DataContext as UndertaleCode; + + if (code == null) + { + if (IsLoaded) + code = CurrentDisassembled; // switched to the tab with different object type + else + return; // probably loaded another data.win or something. + } + + // Check to make sure this isn't an element inside of the textbox, or another tab + IInputElement elem = Keyboard.FocusedElement; + if (elem is UIElement) + { + if (e != null && e.RoutedEvent?.Name != "CtrlK" && (elem as UIElement).IsDescendantOf(DisassemblyEditor)) + return; + } + + UndertaleData data = mainWindow.Data; + try + { + var instructions = Assembler.Assemble(DisassemblyEditor.Text, data); + code.Replace(instructions); + mainWindow.NukeProfileGML(code.Name.Content); + } + catch (Exception ex) + { + MessageBox.Show(ex.ToString(), "Assembler error", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + + // Get rid of old code + CurrentDisassembled = null; + CurrentDecompiled = null; + CurrentGraphed = null; + + // Tab switch + if (e == null) + return; + + // Disassemble new code + DisassembleCode(code, false); + + if (!DisassemblyEditor.IsReadOnly) + { + data.GMLCacheChanged.Add(code.Name.Content); + + if (mainWindow.IsSaving) + { + mainWindow.IsSaving = false; + + _ = mainWindow.DoSaveDialog(); + } + } + } + + // Based on https://stackoverflow.com/questions/28379206/custom-hyperlinks-using-avalonedit + public class NumberGenerator : VisualLineElementGenerator + { + readonly static Regex regex = new Regex(@"-?\d+\.?"); + + public NumberGenerator() + { + } + + Match FindMatch(int startOffset, Regex r) + { + // fetch the end offset of the VisualLine being generated + int endOffset = CurrentContext.VisualLine.LastDocumentLine.EndOffset; + TextDocument document = CurrentContext.Document; + string relevantText = document.GetText(startOffset, endOffset - startOffset); + return r.Match(relevantText); + } + + /// Gets the first offset >= startOffset where the generator wants to construct + /// an element. + /// Return -1 to signal no interest. + public override int GetFirstInterestedOffset(int startOffset) + { + Match m = FindMatch(startOffset, regex); + + var textArea = CurrentContext.TextView.GetService(typeof(TextArea)) as TextArea; + var highlighter = textArea.GetService(typeof(IHighlighter)) as IHighlighter; + int line = CurrentContext.Document.GetLocation(startOffset).Line; + HighlightedLine highlighted = null; + try + { + highlighted = highlighter.HighlightLine(line); + } + catch + { + } + + while (m.Success) + { + int res = startOffset + m.Index; + int currLine = CurrentContext.Document.GetLocation(res).Line; + if (currLine != line) + { + line = currLine; + highlighted = highlighter.HighlightLine(line); + } + + foreach (var section in highlighted.Sections) + { + if (section.Color.Name == "Number" && + section.Offset == res) + return res; + } + + startOffset += m.Length; + m = FindMatch(startOffset, regex); + } + + return -1; + } + + /// Constructs an element at the specified offset. + /// May return null if no element should be constructed. + public override VisualLineElement ConstructElement(int offset) + { + Match m = FindMatch(offset, regex); + + if (m.Success && m.Index == 0) + { + var line = new ClickVisualLineText(m.Value, CurrentContext.VisualLine, m.Length); + var doc = CurrentContext.Document; + var textArea = CurrentContext.TextView.GetService(typeof(TextArea)) as TextArea; + var editor = textArea.GetService(typeof(TextEditor)) as TextEditor; + var parent = VisualTreeHelper.GetParent(editor); + do + { + if ((parent as FrameworkElement) is UserControl) + break; + parent = VisualTreeHelper.GetParent(parent); + } while (parent != null); + line.Clicked += (text) => + { + if (text.EndsWith(".")) + return; + if (int.TryParse(text, out int id)) + { + (parent as UndertaleCodeEditor).DecompiledFocused = true; + UndertaleData data = mainWindow.Data; + + List possibleObjects = new List(); + if (id >= 0) + { + if (id < data.Sprites.Count) + possibleObjects.Add(data.Sprites[id]); + if (id < data.Rooms.Count) + possibleObjects.Add(data.Rooms[id]); + if (id < data.GameObjects.Count) + possibleObjects.Add(data.GameObjects[id]); + if (id < data.Backgrounds.Count) + possibleObjects.Add(data.Backgrounds[id]); + if (id < data.Scripts.Count) + possibleObjects.Add(data.Scripts[id]); + if (id < data.Paths.Count) + possibleObjects.Add(data.Paths[id]); + if (id < data.Fonts.Count) + possibleObjects.Add(data.Fonts[id]); + if (id < data.Sounds.Count) + possibleObjects.Add(data.Sounds[id]); + if (id < data.Shaders.Count) + possibleObjects.Add(data.Shaders[id]); + if (id < data.Timelines.Count) + possibleObjects.Add(data.Timelines[id]); + } + + ContextMenu contextMenu = new ContextMenu(); + foreach (UndertaleObject obj in possibleObjects) + { + MenuItem item = new MenuItem(); + item.Header = obj.ToString().Replace("_", "__"); + item.Click += (sender2, ev2) => + { + if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) + mainWindow.ChangeSelection(obj); + else + { + doc.Replace(line.ParentVisualLine.StartOffset + line.RelativeTextOffset, + text.Length, (obj as UndertaleNamedResource).Name.Content, null); + (parent as UndertaleCodeEditor).DecompiledChanged = true; + } + }; + contextMenu.Items.Add(item); + } + if (id > 0x00050000) + { + MenuItem item = new MenuItem(); + item.Header = "0x" + id.ToString("X6") + " (color)"; + item.Click += (sender2, ev2) => + { + if (!((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)) + { + doc.Replace(line.ParentVisualLine.StartOffset + line.RelativeTextOffset, + text.Length, "0x" + id.ToString("X6"), null); + (parent as UndertaleCodeEditor).DecompiledChanged = true; + } + }; + contextMenu.Items.Add(item); + } + BuiltinList list = mainWindow.Data.BuiltinList; + var myKey = list.Constants.FirstOrDefault(x => x.Value == (double)id).Key; + if (myKey != null) + { + MenuItem item = new MenuItem(); + item.Header = myKey.Replace("_", "__") + " (constant)"; + item.Click += (sender2, ev2) => + { + if (!((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)) + { + doc.Replace(line.ParentVisualLine.StartOffset + line.RelativeTextOffset, + text.Length, myKey, null); + (parent as UndertaleCodeEditor).DecompiledChanged = true; + } + }; + contextMenu.Items.Add(item); + } + contextMenu.Items.Add(new MenuItem() { Header = id + " (number)", IsEnabled = false }); + + contextMenu.IsOpen = true; + } + }; + return line; + } + + return null; + } + } + + public class NameGenerator : VisualLineElementGenerator + { + readonly static Regex regex = new Regex(@"[_a-zA-Z][_a-zA-Z0-9]*"); + + public NameGenerator() + { + } + + Match FindMatch(int startOffset, Regex r) + { + // fetch the end offset of the VisualLine being generated + int endOffset = CurrentContext.VisualLine.LastDocumentLine.EndOffset; + TextDocument document = CurrentContext.Document; + string relevantText = document.GetText(startOffset, endOffset - startOffset); + return r.Match(relevantText); + } + + /// Gets the first offset >= startOffset where the generator wants to construct + /// an element. + /// Return -1 to signal no interest. + public override int GetFirstInterestedOffset(int startOffset) + { + Match m = FindMatch(startOffset, regex); + + var textArea = CurrentContext.TextView.GetService(typeof(TextArea)) as TextArea; + var highlighter = textArea.GetService(typeof(IHighlighter)) as IHighlighter; + int line = CurrentContext.Document.GetLocation(startOffset).Line; + HighlightedLine highlighted = null; + try + { + highlighted = highlighter.HighlightLine(line); + } + catch + { + } + + while (m.Success) + { + int res = startOffset + m.Index; + int currLine = CurrentContext.Document.GetLocation(res).Line; + if (currLine != line) + { + line = currLine; + highlighted = highlighter.HighlightLine(line); + } + + foreach (var section in highlighted.Sections) + { + if (section.Color.Name == "Identifier" || section.Color.Name == "Function") + { + if (section.Offset == res) + return res; + } + } + + startOffset += m.Length; + m = FindMatch(startOffset, regex); + } + return -1; + } + + /// Constructs an element at the specified offset. + /// May return null if no element should be constructed. + public override VisualLineElement ConstructElement(int offset) + { + Match m = FindMatch(offset, regex); + + if (m.Success && m.Index == 0) + { + UndertaleData data = mainWindow.Data; + bool func = (offset + m.Length + 1 < CurrentContext.VisualLine.LastDocumentLine.EndOffset) && + (CurrentContext.Document.GetCharAt(offset + m.Length) == '('); + UndertaleNamedResource val = null; + + var doc = CurrentContext.Document; + var textArea = CurrentContext.TextView.GetService(typeof(TextArea)) as TextArea; + var editor = textArea.GetService(typeof(TextEditor)) as TextEditor; + var parent = VisualTreeHelper.GetParent(editor); + do + { + if ((parent as FrameworkElement) is UserControl) + break; + parent = VisualTreeHelper.GetParent(parent); + } while (parent != null); + + // Process the content of this identifier/function + if (func) + { + val = null; + if (!data.GMS2_3) // in GMS2.3 every custom "function" is in fact a member variable and scripts are never referenced directly + val = data.Scripts.ByName(m.Value); + if (val == null) + { + val = data.Functions.ByName(m.Value); + if (data.GMS2_3) + { + if (val != null) + { + if (data.Code.ByName(val.Name.Content) != null) + val = null; // in GMS2.3 every custom "function" is in fact a member variable, and the names in functions make no sense (they have the gml_Script_ prefix) + } + else + { + // Resolve 2.3 sub-functions for their parent entry + UndertaleFunction f = null; + if (data.KnownSubFunctions?.TryGetValue(m.Value, out f) == true) + val = data.Scripts.ByName(f.Name.Content).Code?.ParentEntry; + } + } + } + if (val == null) + { + if (data.BuiltinList.Functions.ContainsKey(m.Value)) + { + var res = new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, + new SolidColorBrush(Color.FromRgb(0xFF, 0xB8, 0x71))); + res.Bold = true; + return res; + } + } + } + else + { + val = data.ByName(m.Value); + if (data.GMS2_3 & val is UndertaleScript) + val = null; // in GMS2.3 scripts are never referenced directly + } + if (val == null) + { + if (offset >= 7) + { + if (CurrentContext.Document.GetText(offset - 7, 7) == "global.") + { + return new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, + new SolidColorBrush(Color.FromRgb(0xF9, 0x7B, 0xF9))); + } + } + if (data.BuiltinList.Constants.ContainsKey(m.Value)) + return new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, + new SolidColorBrush(Color.FromRgb(0xFF, 0x80, 0x80))); + if (data.BuiltinList.GlobalNotArray.ContainsKey(m.Value) || + data.BuiltinList.Instance.ContainsKey(m.Value) || + data.BuiltinList.GlobalArray.ContainsKey(m.Value)) + return new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, + new SolidColorBrush(Color.FromRgb(0x58, 0xE3, 0x5A))); + if ((parent as UndertaleCodeEditor).CurrentLocals.Contains(m.Value)) + return new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, + new SolidColorBrush(Color.FromRgb(0xFF, 0xF8, 0x99))); + return null; + } + + var line = new ClickVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, + func ? new SolidColorBrush(Color.FromRgb(0xFF, 0xB8, 0x71)) : + new SolidColorBrush(Color.FromRgb(0xFF, 0x80, 0x80))); + if (func) + line.Bold = true; + line.Clicked += (text) => + { + mainWindow.ChangeSelection(val); + }; + + return line; + } + + return null; + } + } + + public class ColorVisualLineText : VisualLineText + { + private string Text { get; set; } + private Brush ForegroundBrush { get; set; } + + public bool Bold { get; set; } = false; + + /// + /// Creates a visual line text element with the specified length. + /// It uses the and its + /// to find the actual text string. + /// + public ColorVisualLineText(string text, VisualLine parentVisualLine, int length, Brush foregroundBrush) + : base(parentVisualLine, length) + { + Text = text; + ForegroundBrush = foregroundBrush; + } + + public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) + { + if (ForegroundBrush != null) + TextRunProperties.SetForegroundBrush(ForegroundBrush); + if (Bold) + TextRunProperties.SetTypeface(new Typeface(TextRunProperties.Typeface.FontFamily, FontStyles.Normal, FontWeights.Bold, FontStretches.Normal)); + return base.CreateTextRun(startVisualColumn, context); + } + + protected override VisualLineText CreateInstance(int length) + { + return new ColorVisualLineText(Text, ParentVisualLine, length, null); + } + } + + public class ClickVisualLineText : VisualLineText + { + + public delegate void ClickHandler(string text); + + public event ClickHandler Clicked; + + private string Text { get; set; } + private Brush ForegroundBrush { get; set; } + + public bool Bold { get; set; } = false; + + /// + /// Creates a visual line text element with the specified length. + /// It uses the and its + /// to find the actual text string. + /// + public ClickVisualLineText(string text, VisualLine parentVisualLine, int length, Brush foregroundBrush = null) + : base(parentVisualLine, length) + { + Text = text; + ForegroundBrush = foregroundBrush; + } + + + public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) + { + if (ForegroundBrush != null) + TextRunProperties.SetForegroundBrush(ForegroundBrush); + if (Bold) + TextRunProperties.SetTypeface(new Typeface(TextRunProperties.Typeface.FontFamily, FontStyles.Normal, FontWeights.Bold, FontStretches.Normal)); + return base.CreateTextRun(startVisualColumn, context); + } + + bool LinkIsClickable() + { + if (string.IsNullOrEmpty(Text)) + return false; + return (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control; + } + + + protected override void OnQueryCursor(QueryCursorEventArgs e) + { + if (LinkIsClickable()) + { + e.Handled = true; + e.Cursor = Cursors.Hand; + } + } + + protected override void OnMouseDown(MouseButtonEventArgs e) + { + if (e.Handled) + return; + if ((e.ChangedButton == System.Windows.Input.MouseButton.Left && LinkIsClickable()) || + e.ChangedButton == System.Windows.Input.MouseButton.Middle) + { + if (Clicked != null) + { + Clicked(Text); + e.Handled = true; + } + } + } + + protected override VisualLineText CreateInstance(int length) + { + var res = new ClickVisualLineText(Text, ParentVisualLine, length); + res.Clicked += Clicked; + return res; + } + } + } +} diff --git a/UndertaleModTool/Editors/UndertaleRoomEditor.xaml b/UndertaleModTool/Editors/UndertaleRoomEditor.xaml index c07684af1..0a75b4f82 100644 --- a/UndertaleModTool/Editors/UndertaleRoomEditor.xaml +++ b/UndertaleModTool/Editors/UndertaleRoomEditor.xaml @@ -576,7 +576,7 @@ Image speed - + Pre Create code @@ -973,7 +973,7 @@ - + - @@ -1081,7 +1107,6 @@ - 0) // if GMS 2+ @@ -462,7 +462,7 @@ private void Rectangle_MouseUp(object sender, MouseButtonEventArgs e) } // recalculates room grid size - room.SetupRoom(); + this.SetupRoomWithGrids(room); } else if (obj is GameObject gameObj) { @@ -1317,7 +1317,7 @@ public void Command_Paste(object sender, ExecutedRoutedEventArgs e) } SelectObject(layer); - room.SetupRoom(false); + room.SetupRoom(false, false); } private void AddObjectInstance(UndertaleRoom room) @@ -1671,6 +1671,26 @@ private void LayerCanvas_Unloaded(object sender, RoutedEventArgs e) if (canvas.CurrentLayer is not null) ObjElemDict.Remove(canvas.CurrentLayer); } + private void SetupRoomWithGrids(UndertaleRoom room) + { + if (Settings.Instance.GridWidthEnabled) + { + room.GridWidth = Settings.Instance.GlobalGridWidth; + if (Settings.Instance.GridHeightEnabled) + { + room.GridHeight = Settings.Instance.GlobalGridHeight; + room.SetupRoom(false, false); + } + else + { + room.SetupRoom(false); + } + } + else + { + room.SetupRoom(); + } + } } public partial class RoomCanvas : Canvas @@ -1763,6 +1783,33 @@ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, } } + public class ForegroundConverter : IMultiValueConverter + { + private static readonly ColorConverter colorConv = new(); + + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Any(x => x is null || x == DependencyProperty.UnsetValue)) + return null; + + bool isGMS2 = ((RoomEntryFlags)values[1]).HasFlag(RoomEntryFlags.IsGMS2); + + (values[0] as GeometryDrawing).Brush = new SolidColorBrush(Colors.Black); + BindingOperations.SetBinding((values[0] as GeometryDrawing).Brush, SolidColorBrush.ColorProperty, new Binding(isGMS2 ? "BGColorLayer.BackgroundData.Color" : "BackgroundColor") + { + Converter = colorConv, + Mode = BindingMode.OneWay + }); + + return null; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + } + public class MultiCollectionBinding : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) diff --git a/UndertaleModTool/Settings.cs b/UndertaleModTool/Settings.cs index f02ba9742..e72f1a307 100644 --- a/UndertaleModTool/Settings.cs +++ b/UndertaleModTool/Settings.cs @@ -42,6 +42,10 @@ public class Settings public bool DeleteOldProfileOnSave { get; set; } = false; public bool WarnOnClose { get; set; } = true; + public double GlobalGridWidth { get; set; } = 0; + public bool GridWidthEnabled { get; set; } = false; + public double GlobalGridHeight { get; set; } = 0; + public bool GridHeightEnabled { get; set; } = false; public static Settings Instance; diff --git a/UndertaleModTool/Windows/SettingsWindow.xaml b/UndertaleModTool/Windows/SettingsWindow.xaml index 5e3e7b842..915b6d243 100644 --- a/UndertaleModTool/Windows/SettingsWindow.xaml +++ b/UndertaleModTool/Windows/SettingsWindow.xaml @@ -9,7 +9,9 @@ - + + + @@ -28,46 +30,68 @@ + + - + - + - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + - + - - + - - + + + + - - + + - - - + + + diff --git a/UndertaleModTool/Windows/SettingsWindow.xaml.cs b/UndertaleModTool/Windows/SettingsWindow.xaml.cs index 707c211ad..54fa6b81f 100644 --- a/UndertaleModTool/Windows/SettingsWindow.xaml.cs +++ b/UndertaleModTool/Windows/SettingsWindow.xaml.cs @@ -129,6 +129,7 @@ public static bool DeleteOldProfileOnSave Settings.Save(); } } + public static bool WarnOnClose { get => Settings.Instance.WarnOnClose; @@ -139,6 +140,46 @@ public static bool WarnOnClose } } + public static double GlobalGridWidth + { + get => Settings.Instance.GlobalGridWidth; + set + { + Settings.Instance.GlobalGridWidth = value; + Settings.Save(); + } + } + + public static bool GridWidthEnabled + { + get => Settings.Instance.GridWidthEnabled; + set + { + Settings.Instance.GridWidthEnabled = value; + Settings.Save(); + } + } + + public static double GlobalGridHeight + { + get => Settings.Instance.GlobalGridHeight; + set + { + Settings.Instance.GlobalGridHeight = value; + Settings.Save(); + } + } + + public static bool GridHeightEnabled + { + get => Settings.Instance.GridHeightEnabled; + set + { + Settings.Instance.GridHeightEnabled = value; + Settings.Save(); + } + } + public bool UpdateButtonEnabled { get => UpdateAppButton.IsEnabled; From 22e85c0bab9641257c273a055f94d94218d7563a Mon Sep 17 00:00:00 2001 From: +TEK Date: Wed, 25 May 2022 03:14:27 +0200 Subject: [PATCH 4/8] Revert "added UndertaleObject duplicating with deep copies thanks to expression trees" This reverts commit 829cdb494925fb4673d628e3d0992de4c8d0da93. --- .../Util/DeepCopyByExpressionTrees.cs | 788 ------------------ UndertaleModTool/MainWindow.xaml | 1 - UndertaleModTool/MainWindow.xaml.cs | 20 +- 3 files changed, 1 insertion(+), 808 deletions(-) delete mode 100644 UndertaleModLib/Util/DeepCopyByExpressionTrees.cs diff --git a/UndertaleModLib/Util/DeepCopyByExpressionTrees.cs b/UndertaleModLib/Util/DeepCopyByExpressionTrees.cs deleted file mode 100644 index 9a09cfecb..000000000 --- a/UndertaleModLib/Util/DeepCopyByExpressionTrees.cs +++ /dev/null @@ -1,788 +0,0 @@ -// Made by Frantisek Konopecky, Prague, 2014 - 2016 -// -// Code comes under MIT licence - Can be used without -// limitations for both personal and commercial purposes. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; - -namespace UndertaleModLib.Util -{ - /// - /// Superfast deep copier class, which uses Expression trees. - /// - public static class DeepCopyByExpressionTrees - { - private static readonly object IsStructTypeToDeepCopyDictionaryLocker = new object(); - private static Dictionary IsStructTypeToDeepCopyDictionary = new Dictionary(); - - private static readonly object CompiledCopyFunctionsDictionaryLocker = new object(); - private static Dictionary, object>> CompiledCopyFunctionsDictionary = - new Dictionary, object>>(); - - private static readonly Type ObjectType = typeof(Object); - private static readonly Type ObjectDictionaryType = typeof(Dictionary); - - /// - /// Creates a deep copy of an object. - /// - /// Object type. - /// Object to copy. - /// Dictionary of already copied objects (Keys: original objects, Values: their copies). - /// - public static T DeepCopyByExpressionTree(this T original, Dictionary copiedReferencesDict = null) - { - return (T)DeepCopyByExpressionTreeObj(original, false, copiedReferencesDict ?? new Dictionary(new ReferenceEqualityComparer())); - } - - private static object DeepCopyByExpressionTreeObj(object original, bool forceDeepCopy, Dictionary copiedReferencesDict) - { - if (original == null) - { - return null; - } - - var type = original.GetType(); - - if (IsDelegate(type)) - { - return null; - } - - if (!forceDeepCopy && !IsTypeToDeepCopy(type)) - { - return original; - } - - object alreadyCopiedObject; - - if (copiedReferencesDict.TryGetValue(original, out alreadyCopiedObject)) - { - return alreadyCopiedObject; - } - - if (type == ObjectType) - { - return new object(); - } - - var compiledCopyFunction = GetOrCreateCompiledLambdaCopyFunction(type); - - object copy = compiledCopyFunction(original, copiedReferencesDict); - - return copy; - } - - private static Func, object> GetOrCreateCompiledLambdaCopyFunction(Type type) - { - // The following structure ensures that multiple threads can use the dictionary - // even while dictionary is locked and being updated by other thread. - // That is why we do not modify the old dictionary instance but - // we replace it with a new instance everytime. - - Func, object> compiledCopyFunction; - - if (!CompiledCopyFunctionsDictionary.TryGetValue(type, out compiledCopyFunction)) - { - lock (CompiledCopyFunctionsDictionaryLocker) - { - if (!CompiledCopyFunctionsDictionary.TryGetValue(type, out compiledCopyFunction)) - { - var uncompiledCopyFunction = CreateCompiledLambdaCopyFunctionForType(type); - - compiledCopyFunction = uncompiledCopyFunction.Compile(); - - var dictionaryCopy = CompiledCopyFunctionsDictionary.ToDictionary(pair => pair.Key, pair => pair.Value); - - dictionaryCopy.Add(type, compiledCopyFunction); - - CompiledCopyFunctionsDictionary = dictionaryCopy; - } - } - } - - return compiledCopyFunction; - } - - private static Expression, object>> CreateCompiledLambdaCopyFunctionForType(Type type) - { - ParameterExpression inputParameter; - ParameterExpression inputDictionary; - ParameterExpression outputVariable; - ParameterExpression boxingVariable; - LabelTarget endLabel; - List variables; - List expressions; - - ///// INITIALIZATION OF EXPRESSIONS AND VARIABLES - - InitializeExpressions(type, - out inputParameter, - out inputDictionary, - out outputVariable, - out boxingVariable, - out endLabel, - out variables, - out expressions); - - ///// RETURN NULL IF ORIGINAL IS NULL - - IfNullThenReturnNullExpression(inputParameter, endLabel, expressions); - - ///// MEMBERWISE CLONE ORIGINAL OBJECT - - MemberwiseCloneInputToOutputExpression(type, inputParameter, outputVariable, expressions); - - ///// STORE COPIED OBJECT TO REFERENCES DICTIONARY - - if (IsClassOtherThanString(type)) - { - StoreReferencesIntoDictionaryExpression(inputParameter, inputDictionary, outputVariable, expressions); - } - - ///// COPY ALL NONVALUE OR NONPRIMITIVE FIELDS - - FieldsCopyExpressions(type, - inputParameter, - inputDictionary, - outputVariable, - boxingVariable, - expressions); - - ///// COPY ELEMENTS OF ARRAY - - if (IsArray(type) && IsTypeToDeepCopy(type.GetElementType())) - { - CreateArrayCopyLoopExpression(type, - inputParameter, - inputDictionary, - outputVariable, - variables, - expressions); - } - - ///// COMBINE ALL EXPRESSIONS INTO LAMBDA FUNCTION - - var lambda = CombineAllIntoLambdaFunctionExpression(inputParameter, inputDictionary, outputVariable, endLabel, variables, expressions); - - return lambda; - } - - private static void InitializeExpressions(Type type, - out ParameterExpression inputParameter, - out ParameterExpression inputDictionary, - out ParameterExpression outputVariable, - out ParameterExpression boxingVariable, - out LabelTarget endLabel, - out List variables, - out List expressions) - { - - inputParameter = Expression.Parameter(ObjectType); - - inputDictionary = Expression.Parameter(ObjectDictionaryType); - - outputVariable = Expression.Variable(type); - - boxingVariable = Expression.Variable(ObjectType); - - endLabel = Expression.Label(); - - variables = new List(); - - expressions = new List(); - - variables.Add(outputVariable); - variables.Add(boxingVariable); - } - - private static void IfNullThenReturnNullExpression(ParameterExpression inputParameter, LabelTarget endLabel, List expressions) - { - ///// Intended code: - ///// - ///// if (input == null) - ///// { - ///// return null; - ///// } - - var ifNullThenReturnNullExpression = - Expression.IfThen( - Expression.Equal( - inputParameter, - Expression.Constant(null, ObjectType)), - Expression.Return(endLabel)); - - expressions.Add(ifNullThenReturnNullExpression); - } - - private static void MemberwiseCloneInputToOutputExpression( - Type type, - ParameterExpression inputParameter, - ParameterExpression outputVariable, - List expressions) - { - ///// Intended code: - ///// - ///// var output = ()input.MemberwiseClone(); - - var memberwiseCloneMethod = ObjectType.GetMethod("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.Instance); - - var memberwiseCloneInputExpression = - Expression.Assign( - outputVariable, - Expression.Convert( - Expression.Call( - inputParameter, - memberwiseCloneMethod), - type)); - - expressions.Add(memberwiseCloneInputExpression); - } - - private static void StoreReferencesIntoDictionaryExpression(ParameterExpression inputParameter, - ParameterExpression inputDictionary, - ParameterExpression outputVariable, - List expressions) - { - ///// Intended code: - ///// - ///// inputDictionary[(Object)input] = (Object)output; - - var storeReferencesExpression = - Expression.Assign( - Expression.Property( - inputDictionary, - ObjectDictionaryType.GetProperty("Item"), - inputParameter), - Expression.Convert(outputVariable, ObjectType)); - - expressions.Add(storeReferencesExpression); - } - - private static Expression, object>> CombineAllIntoLambdaFunctionExpression( - ParameterExpression inputParameter, - ParameterExpression inputDictionary, - ParameterExpression outputVariable, - LabelTarget endLabel, - List variables, - List expressions) - { - expressions.Add(Expression.Label(endLabel)); - - expressions.Add(Expression.Convert(outputVariable, ObjectType)); - - var finalBody = Expression.Block(variables, expressions); - - var lambda = Expression.Lambda, object>>(finalBody, inputParameter, inputDictionary); - - return lambda; - } - - private static void CreateArrayCopyLoopExpression(Type type, - ParameterExpression inputParameter, - ParameterExpression inputDictionary, - ParameterExpression outputVariable, - List variables, - List expressions) - { - ///// Intended code: - ///// - ///// int i1, i2, ..., in; - ///// - ///// int length1 = inputarray.GetLength(0); - ///// i1 = 0; - ///// while (true) - ///// { - ///// if (i1 >= length1) - ///// { - ///// goto ENDLABELFORLOOP1; - ///// } - ///// int length2 = inputarray.GetLength(1); - ///// i2 = 0; - ///// while (true) - ///// { - ///// if (i2 >= length2) - ///// { - ///// goto ENDLABELFORLOOP2; - ///// } - ///// ... - ///// ... - ///// ... - ///// int lengthn = inputarray.GetLength(n); - ///// in = 0; - ///// while (true) - ///// { - ///// if (in >= lengthn) - ///// { - ///// goto ENDLABELFORLOOPn; - ///// } - ///// outputarray[i1, i2, ..., in] - ///// = ()DeepCopyByExpressionTreeObj( - ///// (Object)inputarray[i1, i2, ..., in]) - ///// in++; - ///// } - ///// ENDLABELFORLOOPn: - ///// ... - ///// ... - ///// ... - ///// i2++; - ///// } - ///// ENDLABELFORLOOP2: - ///// i1++; - ///// } - ///// ENDLABELFORLOOP1: - - var rank = type.GetArrayRank(); - - var indices = GenerateIndices(rank); - - variables.AddRange(indices); - - var elementType = type.GetElementType(); - - var assignExpression = ArrayFieldToArrayFieldAssignExpression(inputParameter, inputDictionary, outputVariable, elementType, type, indices); - - Expression forExpression = assignExpression; - - for (int dimension = 0; dimension < rank; dimension++) - { - var indexVariable = indices[dimension]; - - forExpression = LoopIntoLoopExpression(inputParameter, indexVariable, forExpression, dimension); - } - - expressions.Add(forExpression); - } - - private static List GenerateIndices(int arrayRank) - { - ///// Intended code: - ///// - ///// int i1, i2, ..., in; - - var indices = new List(); - - for (int i = 0; i < arrayRank; i++) - { - var indexVariable = Expression.Variable(typeof(Int32)); - - indices.Add(indexVariable); - } - - return indices; - } - - private static BinaryExpression ArrayFieldToArrayFieldAssignExpression( - ParameterExpression inputParameter, - ParameterExpression inputDictionary, - ParameterExpression outputVariable, - Type elementType, - Type arrayType, - List indices) - { - ///// Intended code: - ///// - ///// outputarray[i1, i2, ..., in] - ///// = ()DeepCopyByExpressionTreeObj( - ///// (Object)inputarray[i1, i2, ..., in]); - - var indexTo = Expression.ArrayAccess(outputVariable, indices); - - var indexFrom = Expression.ArrayIndex(Expression.Convert(inputParameter, arrayType), indices); - - var forceDeepCopy = elementType != ObjectType; - - var rightSide = - Expression.Convert( - Expression.Call( - DeepCopyByExpressionTreeObjMethod, - Expression.Convert(indexFrom, ObjectType), - Expression.Constant(forceDeepCopy, typeof(Boolean)), - inputDictionary), - elementType); - - var assignExpression = Expression.Assign(indexTo, rightSide); - - return assignExpression; - } - - private static BlockExpression LoopIntoLoopExpression( - ParameterExpression inputParameter, - ParameterExpression indexVariable, - Expression loopToEncapsulate, - int dimension) - { - ///// Intended code: - ///// - ///// int length = inputarray.GetLength(dimension); - ///// i = 0; - ///// while (true) - ///// { - ///// if (i >= length) - ///// { - ///// goto ENDLABELFORLOOP; - ///// } - ///// loopToEncapsulate; - ///// i++; - ///// } - ///// ENDLABELFORLOOP: - - var lengthVariable = Expression.Variable(typeof(Int32)); - - var endLabelForThisLoop = Expression.Label(); - - var newLoop = - Expression.Loop( - Expression.Block( - new ParameterExpression[0], - Expression.IfThen( - Expression.GreaterThanOrEqual(indexVariable, lengthVariable), - Expression.Break(endLabelForThisLoop)), - loopToEncapsulate, - Expression.PostIncrementAssign(indexVariable)), - endLabelForThisLoop); - - var lengthAssignment = GetLengthForDimensionExpression(lengthVariable, inputParameter, dimension); - - var indexAssignment = Expression.Assign(indexVariable, Expression.Constant(0)); - - return Expression.Block( - new[] { lengthVariable }, - lengthAssignment, - indexAssignment, - newLoop); - } - - private static BinaryExpression GetLengthForDimensionExpression( - ParameterExpression lengthVariable, - ParameterExpression inputParameter, - int i) - { - ///// Intended code: - ///// - ///// length = ((Array)input).GetLength(i); - - var getLengthMethod = typeof(Array).GetMethod("GetLength", BindingFlags.Public | BindingFlags.Instance); - - var dimensionConstant = Expression.Constant(i); - - return Expression.Assign( - lengthVariable, - Expression.Call( - Expression.Convert(inputParameter, typeof(Array)), - getLengthMethod, - new[] { dimensionConstant })); - } - - private static void FieldsCopyExpressions(Type type, - ParameterExpression inputParameter, - ParameterExpression inputDictionary, - ParameterExpression outputVariable, - ParameterExpression boxingVariable, - List expressions) - { - var fields = GetAllRelevantFields(type); - - var readonlyFields = fields.Where(f => f.IsInitOnly).ToList(); - var writableFields = fields.Where(f => !f.IsInitOnly).ToList(); - - ///// READONLY FIELDS COPY (with boxing) - - bool shouldUseBoxing = readonlyFields.Any(); - - if (shouldUseBoxing) - { - var boxingExpression = Expression.Assign(boxingVariable, Expression.Convert(outputVariable, ObjectType)); - - expressions.Add(boxingExpression); - } - - foreach (var field in readonlyFields) - { - if (IsDelegate(field.FieldType)) - { - ReadonlyFieldToNullExpression(field, boxingVariable, expressions); - } - else - { - ReadonlyFieldCopyExpression(type, - field, - inputParameter, - inputDictionary, - boxingVariable, - expressions); - } - } - - if (shouldUseBoxing) - { - var unboxingExpression = Expression.Assign(outputVariable, Expression.Convert(boxingVariable, type)); - - expressions.Add(unboxingExpression); - } - - ///// NOT-READONLY FIELDS COPY - - foreach (var field in writableFields) - { - if (IsDelegate(field.FieldType)) - { - WritableFieldToNullExpression(field, outputVariable, expressions); - } - else - { - WritableFieldCopyExpression(type, - field, - inputParameter, - inputDictionary, - outputVariable, - expressions); - } - } - } - - private static FieldInfo[] GetAllRelevantFields(Type type, bool forceAllFields = false) - { - var fieldsList = new List(); - - var typeCache = type; - - while (typeCache != null) - { - fieldsList.AddRange( - typeCache - .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy) - .Where(field => forceAllFields || IsTypeToDeepCopy(field.FieldType))); - - typeCache = typeCache.BaseType; - } - - return fieldsList.ToArray(); - } - - private static FieldInfo[] GetAllFields(Type type) - { - return GetAllRelevantFields(type, forceAllFields: true); - } - - private static readonly Type FieldInfoType = typeof(FieldInfo); - private static readonly MethodInfo SetValueMethod = FieldInfoType.GetMethod("SetValue", new[] { ObjectType, ObjectType }); - - private static void ReadonlyFieldToNullExpression(FieldInfo field, ParameterExpression boxingVariable, List expressions) - { - // This option must be implemented by Reflection because of the following: - // https://visualstudio.uservoice.com/forums/121579-visual-studio-2015/suggestions/2727812-allow-expression-assign-to-set-readonly-struct-f - - ///// Intended code: - ///// - ///// fieldInfo.SetValue(boxing, null); - - var fieldToNullExpression = - Expression.Call( - Expression.Constant(field), - SetValueMethod, - boxingVariable, - Expression.Constant(null, field.FieldType)); - - expressions.Add(fieldToNullExpression); - } - - private static readonly Type ThisType = typeof(DeepCopyByExpressionTrees); - private static readonly MethodInfo DeepCopyByExpressionTreeObjMethod = ThisType.GetMethod("DeepCopyByExpressionTreeObj", BindingFlags.NonPublic | BindingFlags.Static); - - private static void ReadonlyFieldCopyExpression(Type type, - FieldInfo field, - ParameterExpression inputParameter, - ParameterExpression inputDictionary, - ParameterExpression boxingVariable, - List expressions) - { - // This option must be implemented by Reflection (SetValueMethod) because of the following: - // https://visualstudio.uservoice.com/forums/121579-visual-studio-2015/suggestions/2727812-allow-expression-assign-to-set-readonly-struct-f - - ///// Intended code: - ///// - ///// fieldInfo.SetValue(boxing, DeepCopyByExpressionTreeObj((Object)(()input).)) - - var fieldFrom = Expression.Field(Expression.Convert(inputParameter, type), field); - - var forceDeepCopy = field.FieldType != ObjectType; - - var fieldDeepCopyExpression = - Expression.Call( - Expression.Constant(field, FieldInfoType), - SetValueMethod, - boxingVariable, - Expression.Call( - DeepCopyByExpressionTreeObjMethod, - Expression.Convert(fieldFrom, ObjectType), - Expression.Constant(forceDeepCopy, typeof(Boolean)), - inputDictionary)); - - expressions.Add(fieldDeepCopyExpression); - } - - private static void WritableFieldToNullExpression(FieldInfo field, ParameterExpression outputVariable, List expressions) - { - ///// Intended code: - ///// - ///// output. = ()null; - - var fieldTo = Expression.Field(outputVariable, field); - - var fieldToNullExpression = - Expression.Assign( - fieldTo, - Expression.Constant(null, field.FieldType)); - - expressions.Add(fieldToNullExpression); - } - - private static void WritableFieldCopyExpression(Type type, - FieldInfo field, - ParameterExpression inputParameter, - ParameterExpression inputDictionary, - ParameterExpression outputVariable, - List expressions) - { - ///// Intended code: - ///// - ///// output. = ()DeepCopyByExpressionTreeObj((Object)(()input).); - - var fieldFrom = Expression.Field(Expression.Convert(inputParameter, type), field); - - var fieldType = field.FieldType; - - var fieldTo = Expression.Field(outputVariable, field); - - var forceDeepCopy = field.FieldType != ObjectType; - - var fieldDeepCopyExpression = - Expression.Assign( - fieldTo, - Expression.Convert( - Expression.Call( - DeepCopyByExpressionTreeObjMethod, - Expression.Convert(fieldFrom, ObjectType), - Expression.Constant(forceDeepCopy, typeof(Boolean)), - inputDictionary), - fieldType)); - - expressions.Add(fieldDeepCopyExpression); - } - - private static bool IsArray(Type type) - { - return type.IsArray; - } - - private static bool IsDelegate(Type type) - { - return typeof(Delegate).IsAssignableFrom(type); - } - - private static bool IsTypeToDeepCopy(Type type) - { - return IsClassOtherThanString(type) - || IsStructWhichNeedsDeepCopy(type); - } - - private static bool IsClassOtherThanString(Type type) - { - return !type.IsValueType && type != typeof(String); - } - - private static bool IsStructWhichNeedsDeepCopy(Type type) - { - // The following structure ensures that multiple threads can use the dictionary - // even while dictionary is locked and being updated by other thread. - // That is why we do not modify the old dictionary instance but - // we replace it with a new instance everytime. - - bool isStructTypeToDeepCopy; - - if (!IsStructTypeToDeepCopyDictionary.TryGetValue(type, out isStructTypeToDeepCopy)) - { - lock (IsStructTypeToDeepCopyDictionaryLocker) - { - if (!IsStructTypeToDeepCopyDictionary.TryGetValue(type, out isStructTypeToDeepCopy)) - { - isStructTypeToDeepCopy = IsStructWhichNeedsDeepCopy_NoDictionaryUsed(type); - - var newDictionary = IsStructTypeToDeepCopyDictionary.ToDictionary(pair => pair.Key, pair => pair.Value); - - newDictionary[type] = isStructTypeToDeepCopy; - - IsStructTypeToDeepCopyDictionary = newDictionary; - } - } - } - - return isStructTypeToDeepCopy; - } - - private static bool IsStructWhichNeedsDeepCopy_NoDictionaryUsed(Type type) - { - return IsStructOtherThanBasicValueTypes(type) - && HasInItsHierarchyFieldsWithClasses(type); - } - - private static bool IsStructOtherThanBasicValueTypes(Type type) - { - return type.IsValueType - && !type.IsPrimitive - && !type.IsEnum - && type != typeof(Decimal); - } - - private static bool HasInItsHierarchyFieldsWithClasses(Type type, HashSet alreadyCheckedTypes = null) - { - alreadyCheckedTypes = alreadyCheckedTypes ?? new HashSet(); - - alreadyCheckedTypes.Add(type); - - var allFields = GetAllFields(type); - - var allFieldTypes = allFields.Select(f => f.FieldType).Distinct().ToList(); - - var hasFieldsWithClasses = allFieldTypes.Any(IsClassOtherThanString); - - if (hasFieldsWithClasses) - { - return true; - } - - var notBasicStructsTypes = allFieldTypes.Where(IsStructOtherThanBasicValueTypes).ToList(); - - var typesToCheck = notBasicStructsTypes.Where(t => !alreadyCheckedTypes.Contains(t)).ToList(); - - foreach (var typeToCheck in typesToCheck) - { - if (HasInItsHierarchyFieldsWithClasses(typeToCheck, alreadyCheckedTypes)) - { - return true; - } - } - - return false; - } - - public class ReferenceEqualityComparer : EqualityComparer - { - public override bool Equals(object x, object y) - { - return ReferenceEquals(x, y); - } - - public override int GetHashCode(object obj) - { - if (obj == null) return 0; - - return obj.GetHashCode(); - } - } - } -} diff --git a/UndertaleModTool/MainWindow.xaml b/UndertaleModTool/MainWindow.xaml index 7a23ef2ec..5029bf137 100644 --- a/UndertaleModTool/MainWindow.xaml +++ b/UndertaleModTool/MainWindow.xaml @@ -187,7 +187,6 @@ - diff --git a/UndertaleModTool/MainWindow.xaml.cs b/UndertaleModTool/MainWindow.xaml.cs index 1d1d10c44..6be6c988b 100644 --- a/UndertaleModTool/MainWindow.xaml.cs +++ b/UndertaleModTool/MainWindow.xaml.cs @@ -47,7 +47,6 @@ using System.Net; using System.Globalization; using System.Windows.Controls.Primitives; -using UndertaleModLib.Util; namespace UndertaleModTool { @@ -1611,19 +1610,7 @@ private TreeViewItem GetTreeViewItemFor(UndertaleObject obj) } return null; } - private void DuplicateItem(UndertaleObject obj) - { - TreeViewItem container = GetNearestParent(GetTreeViewItemFor(obj)); - object source = container.ItemsSource; - IList list = ((source as ICollectionView)?.SourceCollection as IList) ?? (source as IList); - bool isLast = list.IndexOf(obj) == list.Count - 1; - if (MessageBox.Show("Duplicate " + obj.ToString() + "?", "Confirmation", MessageBoxButton.YesNo, isLast ? MessageBoxImage.Question : MessageBoxImage.Warning) == MessageBoxResult.Yes) - { - var newObject = obj.DeepCopyByExpressionTree(); - list.Insert(list.IndexOf(obj) + 1, newObject); - UpdateTree(); - } - } + private void DeleteItem(UndertaleObject obj) { TreeViewItem container = GetNearestParent(GetTreeViewItemFor(obj)); @@ -1751,11 +1738,6 @@ private void MenuItem_CopyName_Click(object sender, RoutedEventArgs e) if (Highlighted is UndertaleNamedResource namedRes) CopyItemName(namedRes); } - private void MenuItem_Duplicate_Click(object sender, RoutedEventArgs e) - { - if (Highlighted is UndertaleObject obj) - DuplicateItem(obj); - } private void MenuItem_Delete_Click(object sender, RoutedEventArgs e) { if (Highlighted is UndertaleObject obj) From 978873469bfd7a6e42c7f586ea5a46da82634d7c Mon Sep 17 00:00:00 2001 From: +TEK Date: Wed, 25 May 2022 03:18:09 +0200 Subject: [PATCH 5/8] Clean up accidental commit of useless converter --- .../Editors/UndertaleRoomEditor.xaml.cs | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/UndertaleModTool/Editors/UndertaleRoomEditor.xaml.cs b/UndertaleModTool/Editors/UndertaleRoomEditor.xaml.cs index 363768ecf..fe00f4df4 100644 --- a/UndertaleModTool/Editors/UndertaleRoomEditor.xaml.cs +++ b/UndertaleModTool/Editors/UndertaleRoomEditor.xaml.cs @@ -1783,33 +1783,6 @@ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, } } - public class ForegroundConverter : IMultiValueConverter - { - private static readonly ColorConverter colorConv = new(); - - public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) - { - if (values.Any(x => x is null || x == DependencyProperty.UnsetValue)) - return null; - - bool isGMS2 = ((RoomEntryFlags)values[1]).HasFlag(RoomEntryFlags.IsGMS2); - - (values[0] as GeometryDrawing).Brush = new SolidColorBrush(Colors.Black); - BindingOperations.SetBinding((values[0] as GeometryDrawing).Brush, SolidColorBrush.ColorProperty, new Binding(isGMS2 ? "BGColorLayer.BackgroundData.Color" : "BackgroundColor") - { - Converter = colorConv, - Mode = BindingMode.OneWay - }); - - return null; - } - - public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) - { - throw new NotSupportedException(); - } - } - public class MultiCollectionBinding : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) From 6aa36e021866331fc30ba0221d2f6482c6a32e4a Mon Sep 17 00:00:00 2001 From: +TEK Date: Wed, 25 May 2022 15:44:34 +0200 Subject: [PATCH 6/8] Revert messed up line endings --- .../Editors/UndertaleCodeEditor.xaml.cs | 2596 ++++++++--------- 1 file changed, 1298 insertions(+), 1298 deletions(-) diff --git a/UndertaleModTool/Editors/UndertaleCodeEditor.xaml.cs b/UndertaleModTool/Editors/UndertaleCodeEditor.xaml.cs index 6325304cc..52c8a18e9 100644 --- a/UndertaleModTool/Editors/UndertaleCodeEditor.xaml.cs +++ b/UndertaleModTool/Editors/UndertaleCodeEditor.xaml.cs @@ -1,1299 +1,1299 @@ -using GraphVizWrapper; -using GraphVizWrapper.Commands; -using GraphVizWrapper.Queries; -using ICSharpCode.AvalonEdit; -using ICSharpCode.AvalonEdit.Document; -using ICSharpCode.AvalonEdit.Editing; -using ICSharpCode.AvalonEdit.Folding; -using ICSharpCode.AvalonEdit.Highlighting; -using ICSharpCode.AvalonEdit.Highlighting.Xshd; -using ICSharpCode.AvalonEdit.Rendering; -using ICSharpCode.AvalonEdit.Search; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.Versioning; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Media.TextFormatting; -using System.Windows.Navigation; -using System.Xml; -using UndertaleModLib; -using UndertaleModLib.Compiler; -using UndertaleModLib.Decompiler; -using UndertaleModLib.Models; -using static UndertaleModTool.MainWindow.CodeEditorMode; - -namespace UndertaleModTool +using GraphVizWrapper; +using GraphVizWrapper.Commands; +using GraphVizWrapper.Queries; +using ICSharpCode.AvalonEdit; +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Editing; +using ICSharpCode.AvalonEdit.Folding; +using ICSharpCode.AvalonEdit.Highlighting; +using ICSharpCode.AvalonEdit.Highlighting.Xshd; +using ICSharpCode.AvalonEdit.Rendering; +using ICSharpCode.AvalonEdit.Search; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Versioning; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Media.TextFormatting; +using System.Windows.Navigation; +using System.Xml; +using UndertaleModLib; +using UndertaleModLib.Compiler; +using UndertaleModLib.Decompiler; +using UndertaleModLib.Models; +using static UndertaleModTool.MainWindow.CodeEditorMode; + +namespace UndertaleModTool { - /// - /// Logika interakcji dla klasy UndertaleCodeEditor.xaml - /// - [SupportedOSPlatform("windows7.0")] - public partial class UndertaleCodeEditor : DataUserControl - { - private static MainWindow mainWindow = Application.Current.MainWindow as MainWindow; - - public UndertaleCode CurrentDisassembled = null; - public UndertaleCode CurrentDecompiled = null; - public List CurrentLocals = null; - public UndertaleCode CurrentGraphed = null; - public string ProfileHash = mainWindow.ProfileHash; - public string MainPath = Path.Combine(Settings.ProfilesFolder, mainWindow.ProfileHash, "Main"); - public string TempPath = Path.Combine(Settings.ProfilesFolder, mainWindow.ProfileHash, "Temp"); - - public bool DecompiledFocused = false; - public bool DecompiledChanged = false; - public bool DecompiledYet = false; - public bool DecompiledSkipped = false; - public SearchPanel DecompiledSearchPanel; - - public bool DisassemblyFocused = false; - public bool DisassemblyChanged = false; - public bool DisassembledYet = false; - public bool DisassemblySkipped = false; - public SearchPanel DisassemblySearchPanel; - - public static RoutedUICommand Compile = new RoutedUICommand("Compile code", "Compile", typeof(UndertaleCodeEditor)); - - public UndertaleCodeEditor() - { - InitializeComponent(); - - // Decompiled editor styling and functionality - DecompiledSearchPanel = SearchPanel.Install(DecompiledEditor.TextArea); - DecompiledSearchPanel.MarkerBrush = new SolidColorBrush(Color.FromRgb(90, 90, 90)); - - using (Stream stream = this.GetType().Assembly.GetManifestResourceStream("UndertaleModTool.Resources.GML.xshd")) - { - using (XmlTextReader reader = new XmlTextReader(stream)) - { - DecompiledEditor.SyntaxHighlighting = HighlightingLoader.Load(reader, HighlightingManager.Instance); - var def = DecompiledEditor.SyntaxHighlighting; - if (mainWindow.Data.GeneralInfo.Major < 2) - { - foreach (var span in def.MainRuleSet.Spans) - { - string expr = span.StartExpression.ToString(); - if (expr == "\"" || expr == "'") - { - span.RuleSet.Spans.Clear(); - } - } - } - // This was an attempt to only highlight - // GMS 2.3+ keywords if the game is - // made in such a version. - // However despite what StackOverflow - // says, this isn't working so it's just - // hardcoded in the XML for now - /* - if(mainWindow.Data.GMS2_3) - { - HighlightingColor color = null; - foreach (var rule in def.MainRuleSet.Rules) - { - if (rule.Regex.IsMatch("if")) - { - color = rule.Color; - break; - } - } - if (color != null) - { - string[] keywords = - { - "new", - "function", - "keywords" - }; - var rule = new HighlightingRule(); - var regex = String.Format(@"\b(?>{0})\b", String.Join("|", keywords)); - - rule.Regex = new Regex(regex); - rule.Color = color; - - def.MainRuleSet.Rules.Add(rule); - } - }*/ - } - } - - DecompiledEditor.Options.ConvertTabsToSpaces = true; - - DecompiledEditor.TextArea.TextView.ElementGenerators.Add(new NumberGenerator()); - DecompiledEditor.TextArea.TextView.ElementGenerators.Add(new NameGenerator()); - - DecompiledEditor.TextArea.TextView.Options.HighlightCurrentLine = true; - DecompiledEditor.TextArea.TextView.CurrentLineBackground = new SolidColorBrush(Color.FromRgb(60, 60, 60)); - DecompiledEditor.TextArea.TextView.CurrentLineBorder = new Pen() { Thickness = 0 }; - - DecompiledEditor.Document.TextChanged += (s, e) => - { - DecompiledFocused = true; - DecompiledChanged = true; - }; - - DecompiledEditor.TextArea.SelectionBrush = new SolidColorBrush(Color.FromRgb(100, 100, 100)); - DecompiledEditor.TextArea.SelectionForeground = null; - DecompiledEditor.TextArea.SelectionBorder = null; - DecompiledEditor.TextArea.SelectionCornerRadius = 0; - - // Disassembly editor styling and functionality - DisassemblySearchPanel = SearchPanel.Install(DisassemblyEditor.TextArea); - DisassemblySearchPanel.MarkerBrush = new SolidColorBrush(Color.FromRgb(90, 90, 90)); - - using (Stream stream = this.GetType().Assembly.GetManifestResourceStream("UndertaleModTool.Resources.VMASM.xshd")) - { - using (XmlTextReader reader = new XmlTextReader(stream)) - { - DisassemblyEditor.SyntaxHighlighting = HighlightingLoader.Load(reader, HighlightingManager.Instance); - } - } - - DisassemblyEditor.TextArea.TextView.ElementGenerators.Add(new NameGenerator()); - - DisassemblyEditor.TextArea.TextView.Options.HighlightCurrentLine = true; - DisassemblyEditor.TextArea.TextView.CurrentLineBackground = new SolidColorBrush(Color.FromRgb(60, 60, 60)); - DisassemblyEditor.TextArea.TextView.CurrentLineBorder = new Pen() { Thickness = 0 }; - - DisassemblyEditor.Document.TextChanged += (s, e) => DisassemblyChanged = true; - - DisassemblyEditor.TextArea.SelectionBrush = new SolidColorBrush(Color.FromRgb(100, 100, 100)); - DisassemblyEditor.TextArea.SelectionForeground = null; - DisassemblyEditor.TextArea.SelectionBorder = null; - DisassemblyEditor.TextArea.SelectionCornerRadius = 0; - } - - private async void TabControl_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - UndertaleCode code = this.DataContext as UndertaleCode; - Directory.CreateDirectory(MainPath); - Directory.CreateDirectory(TempPath); - if (code == null) - return; - DecompiledSearchPanel.Close(); - DisassemblySearchPanel.Close(); - await DecompiledLostFocusBody(sender, null); - DisassemblyEditor_LostFocus(sender, null); - if (DisassemblyTab.IsSelected && code != CurrentDisassembled) - { - DisassembleCode(code, !DisassembledYet); - DisassembledYet = true; - } - if (DecompiledTab.IsSelected && code != CurrentDecompiled) - { - _ = DecompileCode(code, !DecompiledYet); - DecompiledYet = true; - } - if (GraphTab.IsSelected && code != CurrentGraphed) - { - GraphCode(code); - } - } - - private async void UserControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) - { - UndertaleCode code = this.DataContext as UndertaleCode; - if (code == null) - return; - - // compile/disassemble previously edited code (save changes) - if (DecompiledTab.IsSelected && DecompiledFocused && DecompiledChanged && - CurrentDecompiled is not null && CurrentDecompiled != code) - { - DecompiledSkipped = true; - DecompiledEditor_LostFocus(sender, null); - - } - else if (DisassemblyTab.IsSelected && DisassemblyFocused && DisassemblyChanged && - CurrentDisassembled is not null && CurrentDisassembled != code) - { - DisassemblySkipped = true; - DisassemblyEditor_LostFocus(sender, null); - } - - DecompiledEditor_LostFocus(sender, null); - DisassemblyEditor_LostFocus(sender, null); - - if (MainWindow.CodeEditorDecompile != Unstated) //if opened from the code search results "link" - { - if (MainWindow.CodeEditorDecompile == DontDecompile && code != CurrentDisassembled) - { - if (CodeModeTabs.SelectedItem != DisassemblyTab) - CodeModeTabs.SelectedItem = DisassemblyTab; - else - DisassembleCode(code, true); - } - - if (MainWindow.CodeEditorDecompile == Decompile && code != CurrentDecompiled) - { - if (CodeModeTabs.SelectedItem != DecompiledTab) - CodeModeTabs.SelectedItem = DecompiledTab; - else - _ = DecompileCode(code, true); - } - - MainWindow.CodeEditorDecompile = Unstated; - } - else - { - if (DisassemblyTab.IsSelected && code != CurrentDisassembled) - { - DisassembleCode(code, true); - } - if (DecompiledTab.IsSelected && code != CurrentDecompiled) - { - _ = DecompileCode(code, true); - } - if (GraphTab.IsSelected && code != CurrentGraphed) - { - GraphCode(code); - } - } - } - - public static readonly RoutedEvent CtrlKEvent = EventManager.RegisterRoutedEvent( - "CtrlK", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(UndertaleCodeEditor)); - - private async Task CompileCommandBody(object sender, EventArgs e) - { - if (DecompiledFocused) - { - await DecompiledLostFocusBody(sender, new RoutedEventArgs(CtrlKEvent)); - } - else if (DisassemblyFocused) - { - DisassemblyEditor_LostFocus(sender, new RoutedEventArgs(CtrlKEvent)); - DisassemblyEditor_GotFocus(sender, null); - } - - await Task.Delay(1); //dummy await - } - private void Command_Compile(object sender, EventArgs e) - { - _ = CompileCommandBody(sender, e); - } - public async Task SaveChanges() - { - await CompileCommandBody(null, null); - } - - private void DisassembleCode(UndertaleCode code, bool first) - { - code.UpdateAddresses(); - - string text; - - DisassemblyEditor.TextArea.ClearSelection(); - if (code.ParentEntry != null) - { - DisassemblyEditor.IsReadOnly = true; - text = "; This code entry is a reference to an anonymous function within " + code.ParentEntry.Name.Content + ", view it there"; - } - else - { - DisassemblyEditor.IsReadOnly = false; - - var data = mainWindow.Data; - text = code.Disassemble(data.Variables, data.CodeLocals.For(code)); - - CurrentLocals = new List(); - } - - DisassemblyEditor.Document.BeginUpdate(); - DisassemblyEditor.Document.Text = text; - DisassemblyEditor.Document.EndUpdate(); - - if (first) - DisassemblyEditor.Document.UndoStack.ClearAll(); - - CurrentDisassembled = code; - DisassemblyChanged = false; - } - - public static Dictionary gettext = null; - private void UpdateGettext(UndertaleCode gettextCode) - { - gettext = new Dictionary(); - string[] decompilationOutput; - if (!SettingsWindow.ProfileModeEnabled) - decompilationOutput = Decompiler.Decompile(gettextCode, new GlobalDecompileContext(null, false)).Replace("\r\n", "\n").Split('\n'); - else - { - try - { - string path = Path.Combine(TempPath, gettextCode.Name.Content + ".gml"); - if (File.Exists(path)) - decompilationOutput = File.ReadAllText(path).Replace("\r\n", "\n").Split('\n'); - else - decompilationOutput = Decompiler.Decompile(gettextCode, new GlobalDecompileContext(null, false)).Replace("\r\n", "\n").Split('\n'); - } - catch - { - decompilationOutput = Decompiler.Decompile(gettextCode, new GlobalDecompileContext(null, false)).Replace("\r\n", "\n").Split('\n'); - } - } - Regex textdataRegex = new Regex("^ds_map_add\\(global\\.text_data_en, \\\"(.*)\\\", \\\"(.*)\\\"\\)"); - foreach (var line in decompilationOutput) - { - Match m = textdataRegex.Match(line); - if (m.Success) - { - try - { - gettext.Add(m.Groups[1].Value, m.Groups[2].Value); - } - catch (ArgumentException) - { - MessageBox.Show("There is a duplicate key in textdata_en, being " + m.Groups[1].Value + ". This may cause errors in the comment display of text.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); - } - catch - { - MessageBox.Show("Unknown error in textdata_en. This may cause errors in the comment display of text.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); - } - } - } - } - - public static Dictionary gettextJSON = null; - private string UpdateGettextJSON(string json) - { - try - { - gettextJSON = JsonConvert.DeserializeObject>(json); - } - catch (Exception e) - { - gettextJSON = new Dictionary(); - return "Failed to parse language file: " + e.Message; - } - return null; - } - - private async Task DecompileCode(UndertaleCode code, bool first, LoaderDialog existingDialog = null) - { - DecompiledEditor.IsReadOnly = true; - DecompiledEditor.TextArea.ClearSelection(); - if (code.ParentEntry != null) - { - DecompiledEditor.Text = "// This code entry is a reference to an anonymous function within " + code.ParentEntry.Name.Content + ", view it there"; - CurrentDecompiled = code; - existingDialog?.TryClose(); - } - else - { - LoaderDialog dialog; - if (existingDialog != null) - { - dialog = existingDialog; - dialog.Message = "Decompiling, please wait..."; - } - else - { - dialog = new LoaderDialog("Decompiling", "Decompiling, please wait... This can take a while on complex scripts."); - dialog.Owner = Window.GetWindow(this); - try - { - _ = Dispatcher.BeginInvoke(new Action(() => { if (!dialog.IsClosed) dialog.TryShowDialog(); })); - } - catch - { - // This is still a problem in rare cases for some unknown reason - } - } - - bool openSaveDialog = false; - - UndertaleCode gettextCode = null; - if (gettext == null) - gettextCode = mainWindow.Data.Code.ByName("gml_Script_textdata_en"); - - string dataPath = Path.GetDirectoryName(mainWindow.FilePath); - string gettextJsonPath = null; - if (dataPath is not null) - { - gettextJsonPath = Path.Combine(dataPath, "lang", "lang_en.json"); - if (!File.Exists(gettextJsonPath)) - gettextJsonPath = Path.Combine(dataPath, "lang", "lang_en_ch1.json"); - } - - var dataa = mainWindow.Data; - Task t = Task.Run(() => - { - GlobalDecompileContext context = new GlobalDecompileContext(dataa, false); - string decompiled = null; - Exception e = null; - try - { - string path = Path.Combine(TempPath, code.Name.Content + ".gml"); - if (!SettingsWindow.ProfileModeEnabled || !File.Exists(path)) - { - decompiled = Decompiler.Decompile(code, context); - } - else - decompiled = File.ReadAllText(path); - } - catch (Exception ex) - { - e = ex; - } - - if (gettextCode != null) - UpdateGettext(gettextCode); - - try - { - if (gettextJSON == null && gettextJsonPath != null && File.Exists(gettextJsonPath)) - { - string err = UpdateGettextJSON(File.ReadAllText(gettextJsonPath)); - if (err != null) - e = new Exception(err); - } - } - catch (Exception exc) - { - MessageBox.Show(exc.ToString()); - } - - if (decompiled != null) - { - string[] decompiledLines; - if (gettext != null && decompiled.Contains("scr_gettext")) - { - decompiledLines = decompiled.Split('\n'); - for (int i = 0; i < decompiledLines.Length; i++) - { - var matches = Regex.Matches(decompiledLines[i], "scr_gettext\\(\\\"(\\w*)\\\"\\)"); - foreach (Match match in matches) - { - if (match.Success) - { - if (gettext.TryGetValue(match.Groups[1].Value, out string text)) - decompiledLines[i] += $" // {text}"; - } - } - } - decompiled = string.Join('\n', decompiledLines); - } - else if (gettextJSON != null && decompiled.Contains("scr_84_get_lang_string")) - { - decompiledLines = decompiled.Split('\n'); - for (int i = 0; i < decompiledLines.Length; i++) - { - var matches = Regex.Matches(decompiledLines[i], "scr_84_get_lang_string(\\w*)\\(\\\"(\\w*)\\\"\\)"); - foreach (Match match in matches) - { - if (match.Success) - { - if (gettextJSON.TryGetValue(match.Groups[^1].Value, out string text)) - decompiledLines[i] += $" // {text}"; - } - } - } - decompiled = string.Join('\n', decompiledLines); - } - } - - Dispatcher.Invoke(() => - { - if (DataContext != code) - return; // Switched to another code entry or otherwise - - DecompiledEditor.Document.BeginUpdate(); - if (e != null) - DecompiledEditor.Document.Text = "/* EXCEPTION!\n " + e.ToString() + "\n*/"; - else if (decompiled != null) - { - DecompiledEditor.Document.Text = decompiled; - CurrentLocals = new List(); - - var locals = dataa.CodeLocals.ByName(code.Name.Content); - if (locals != null) - { - foreach (var local in locals.Locals) - CurrentLocals.Add(local.Name.Content); - } - - if (existingDialog is not null) //if code was edited (and compiles after it) - { - dataa.GMLCacheChanged.Add(code.Name.Content); - dataa.GMLCacheFailed?.Remove(code.Name.Content); //remove that code name, since that code compiles now - - openSaveDialog = mainWindow.IsSaving; - } - } - DecompiledEditor.Document.EndUpdate(); - DecompiledEditor.IsReadOnly = false; - if (first) - DecompiledEditor.Document.UndoStack.ClearAll(); - DecompiledChanged = false; - - CurrentDecompiled = code; - dialog.Hide(); - }); - }); - await t; - dialog.Close(); - - mainWindow.IsSaving = false; - - if (openSaveDialog) - await mainWindow.DoSaveDialog(); - } - } - - private async void GraphCode(UndertaleCode code) - { - if (code.ParentEntry != null) - { - GraphView.Source = null; - CurrentGraphed = code; - return; - } - - LoaderDialog dialog = new LoaderDialog("Generating graph", "Generating graph, please wait..."); - dialog.Owner = Window.GetWindow(this); - Task t = Task.Run(() => - { - ImageSource image = null; - try - { - code.UpdateAddresses(); - List entryPoints = new List(); - entryPoints.Add(0); - foreach (UndertaleCode duplicate in code.ChildEntries) - entryPoints.Add(duplicate.Offset / 4); - var blocks = Decompiler.DecompileFlowGraph(code, entryPoints); - string dot = Decompiler.ExportFlowGraph(blocks); - - try - { - var getStartProcessQuery = new GetStartProcessQuery(); - var getProcessStartInfoQuery = new GetProcessStartInfoQuery(); - var registerLayoutPluginCommand = new RegisterLayoutPluginCommand(getProcessStartInfoQuery, getStartProcessQuery); - var wrapper = new GraphGeneration(getStartProcessQuery, getProcessStartInfoQuery, registerLayoutPluginCommand); - wrapper.GraphvizPath = Settings.Instance.GraphVizPath; - - byte[] output = wrapper.GenerateGraph(dot, Enums.GraphReturnType.Png); // TODO: Use SVG instead - - image = new ImageSourceConverter().ConvertFrom(output) as ImageSource; - } - catch (Exception e) - { - Debug.WriteLine(e.ToString()); - if (MessageBox.Show("Unable to execute GraphViz: " + e.Message + "\nMake sure you have downloaded it and set the path in settings.\nDo you want to open the download page now?", "Graph generation failed", MessageBoxButton.YesNo, MessageBoxImage.Error) == MessageBoxResult.Yes) - MainWindow.OpenBrowser("https://graphviz.gitlab.io/_pages/Download/Download_windows.html"); - } - } - catch (Exception e) - { - Debug.WriteLine(e.ToString()); - MessageBox.Show(e.Message, "Graph generation failed", MessageBoxButton.OK, MessageBoxImage.Error); - } - - Dispatcher.Invoke(() => - { - GraphView.Source = image; - CurrentGraphed = code; - dialog.Hide(); - }); - }); - dialog.ShowDialog(); - await t; - } - - private void DecompiledEditor_GotFocus(object sender, RoutedEventArgs e) - { - if (DecompiledEditor.IsReadOnly) - return; - DecompiledFocused = true; - } - - private static string Truncate(string value, int maxChars) - { - return value.Length <= maxChars ? value : value.Substring(0, maxChars) + "..."; - } - - private async Task DecompiledLostFocusBody(object sender, RoutedEventArgs e) - { - if (!DecompiledFocused) - return; - if (DecompiledEditor.IsReadOnly) - return; - DecompiledFocused = false; - - if (!DecompiledChanged) - return; - - UndertaleCode code; - if (DecompiledSkipped) - { - code = CurrentDecompiled; - DecompiledSkipped = false; - } - else - code = this.DataContext as UndertaleCode; - - if (code == null) - { - if (IsLoaded) - code = CurrentDecompiled; // switched to the tab with different object type - else - return; // probably loaded another data.win or something. - } - - if (code.ParentEntry != null) - return; - - // Check to make sure this isn't an element inside of the textbox, or another tab - IInputElement elem = Keyboard.FocusedElement; - if (elem is UIElement) - { - if (e != null && e.RoutedEvent?.Name != "CtrlK" && (elem as UIElement).IsDescendantOf(DecompiledEditor)) - return; - } - - UndertaleData data = mainWindow.Data; - - LoaderDialog dialog = new LoaderDialog("Compiling", "Compiling, please wait..."); - dialog.Owner = Window.GetWindow(this); - try - { - _ = Dispatcher.BeginInvoke(new Action(() => { if (!dialog.IsClosed) dialog.TryShowDialog(); })); - } - catch - { - // This is still a problem in rare cases for some unknown reason - } - - CompileContext compileContext = null; - string text = DecompiledEditor.Text; - var dispatcher = Dispatcher; - Task t = Task.Run(() => - { - compileContext = Compiler.CompileGMLText(text, data, code, (f) => { dispatcher.Invoke(() => f()); }); - }); - await t; - - if (compileContext == null) - { - dialog.TryClose(); - MessageBox.Show("Compile context was null for some reason...", "This shouldn't happen", MessageBoxButton.OK, MessageBoxImage.Error); - return; - } - - if (compileContext.HasError) - { - dialog.TryClose(); - MessageBox.Show(Truncate(compileContext.ResultError, 512), "Compiler error", MessageBoxButton.OK, MessageBoxImage.Error); - return; - } - - if (!compileContext.SuccessfulCompile) - { - dialog.TryClose(); - MessageBox.Show("(unknown error message)", "Compile failed", MessageBoxButton.OK, MessageBoxImage.Error); - return; - } - - code.Replace(compileContext.ResultAssembly); - - if (!mainWindow.Data.GMS2_3) - { - try - { - string path = Path.Combine(TempPath, code.Name.Content + ".gml"); - if (SettingsWindow.ProfileModeEnabled) - { - // Write text, only if in the profile mode. - File.WriteAllText(path, DecompiledEditor.Text); - } - else - { - // Destroy file with comments if it's been edited outside the profile mode. - // We're dealing with the decompiled code only, it has to happen. - // Otherwise it will cause a desync, which is more important to prevent. - if (File.Exists(path)) - File.Delete(path); - } - } - catch (Exception exc) - { - MessageBox.Show("Error during writing of GML code to profile:\n" + exc.ToString()); - } - } - - // Invalidate gettext if necessary - if (code.Name.Content == "gml_Script_textdata_en") - gettext = null; - - // Show new code, decompiled. - CurrentDisassembled = null; - CurrentDecompiled = null; - CurrentGraphed = null; - - // Tab switch - if (e == null) - { - dialog.TryClose(); - return; - } - - // Decompile new code - await DecompileCode(code, false, dialog); - - //GMLCacheChanged.Add() is inside DecompileCode() - } - private void DecompiledEditor_LostFocus(object sender, RoutedEventArgs e) - { - _ = DecompiledLostFocusBody(sender, e); - } - - private void DisassemblyEditor_GotFocus(object sender, RoutedEventArgs e) - { - if (DisassemblyEditor.IsReadOnly) - return; - DisassemblyFocused = true; - } - - private void DisassemblyEditor_LostFocus(object sender, RoutedEventArgs e) - { - if (!DisassemblyFocused) - return; - if (DisassemblyEditor.IsReadOnly) - return; - DisassemblyFocused = false; - - if (!DisassemblyChanged) - return; - - UndertaleCode code; - if (DisassemblySkipped) - { - code = CurrentDisassembled; - DisassemblySkipped = false; - } - else - code = this.DataContext as UndertaleCode; - - if (code == null) - { - if (IsLoaded) - code = CurrentDisassembled; // switched to the tab with different object type - else - return; // probably loaded another data.win or something. - } - - // Check to make sure this isn't an element inside of the textbox, or another tab - IInputElement elem = Keyboard.FocusedElement; - if (elem is UIElement) - { - if (e != null && e.RoutedEvent?.Name != "CtrlK" && (elem as UIElement).IsDescendantOf(DisassemblyEditor)) - return; - } - - UndertaleData data = mainWindow.Data; - try - { - var instructions = Assembler.Assemble(DisassemblyEditor.Text, data); - code.Replace(instructions); - mainWindow.NukeProfileGML(code.Name.Content); - } - catch (Exception ex) - { - MessageBox.Show(ex.ToString(), "Assembler error", MessageBoxButton.OK, MessageBoxImage.Error); - return; - } - - // Get rid of old code - CurrentDisassembled = null; - CurrentDecompiled = null; - CurrentGraphed = null; - - // Tab switch - if (e == null) - return; - - // Disassemble new code - DisassembleCode(code, false); - - if (!DisassemblyEditor.IsReadOnly) - { - data.GMLCacheChanged.Add(code.Name.Content); - - if (mainWindow.IsSaving) - { - mainWindow.IsSaving = false; - - _ = mainWindow.DoSaveDialog(); - } - } - } - - // Based on https://stackoverflow.com/questions/28379206/custom-hyperlinks-using-avalonedit - public class NumberGenerator : VisualLineElementGenerator - { - readonly static Regex regex = new Regex(@"-?\d+\.?"); - - public NumberGenerator() - { - } - - Match FindMatch(int startOffset, Regex r) - { - // fetch the end offset of the VisualLine being generated - int endOffset = CurrentContext.VisualLine.LastDocumentLine.EndOffset; - TextDocument document = CurrentContext.Document; - string relevantText = document.GetText(startOffset, endOffset - startOffset); - return r.Match(relevantText); - } - - /// Gets the first offset >= startOffset where the generator wants to construct - /// an element. - /// Return -1 to signal no interest. - public override int GetFirstInterestedOffset(int startOffset) - { - Match m = FindMatch(startOffset, regex); - - var textArea = CurrentContext.TextView.GetService(typeof(TextArea)) as TextArea; - var highlighter = textArea.GetService(typeof(IHighlighter)) as IHighlighter; - int line = CurrentContext.Document.GetLocation(startOffset).Line; - HighlightedLine highlighted = null; - try - { - highlighted = highlighter.HighlightLine(line); - } - catch - { - } - - while (m.Success) - { - int res = startOffset + m.Index; - int currLine = CurrentContext.Document.GetLocation(res).Line; - if (currLine != line) - { - line = currLine; - highlighted = highlighter.HighlightLine(line); - } - - foreach (var section in highlighted.Sections) - { - if (section.Color.Name == "Number" && - section.Offset == res) - return res; - } - - startOffset += m.Length; - m = FindMatch(startOffset, regex); - } - - return -1; - } - - /// Constructs an element at the specified offset. - /// May return null if no element should be constructed. - public override VisualLineElement ConstructElement(int offset) - { - Match m = FindMatch(offset, regex); - - if (m.Success && m.Index == 0) - { - var line = new ClickVisualLineText(m.Value, CurrentContext.VisualLine, m.Length); - var doc = CurrentContext.Document; - var textArea = CurrentContext.TextView.GetService(typeof(TextArea)) as TextArea; - var editor = textArea.GetService(typeof(TextEditor)) as TextEditor; - var parent = VisualTreeHelper.GetParent(editor); - do - { - if ((parent as FrameworkElement) is UserControl) - break; - parent = VisualTreeHelper.GetParent(parent); - } while (parent != null); - line.Clicked += (text) => - { - if (text.EndsWith(".")) - return; - if (int.TryParse(text, out int id)) - { - (parent as UndertaleCodeEditor).DecompiledFocused = true; - UndertaleData data = mainWindow.Data; - - List possibleObjects = new List(); - if (id >= 0) - { - if (id < data.Sprites.Count) - possibleObjects.Add(data.Sprites[id]); - if (id < data.Rooms.Count) - possibleObjects.Add(data.Rooms[id]); - if (id < data.GameObjects.Count) - possibleObjects.Add(data.GameObjects[id]); - if (id < data.Backgrounds.Count) - possibleObjects.Add(data.Backgrounds[id]); - if (id < data.Scripts.Count) - possibleObjects.Add(data.Scripts[id]); - if (id < data.Paths.Count) - possibleObjects.Add(data.Paths[id]); - if (id < data.Fonts.Count) - possibleObjects.Add(data.Fonts[id]); - if (id < data.Sounds.Count) - possibleObjects.Add(data.Sounds[id]); - if (id < data.Shaders.Count) - possibleObjects.Add(data.Shaders[id]); - if (id < data.Timelines.Count) - possibleObjects.Add(data.Timelines[id]); - } - - ContextMenu contextMenu = new ContextMenu(); - foreach (UndertaleObject obj in possibleObjects) - { - MenuItem item = new MenuItem(); - item.Header = obj.ToString().Replace("_", "__"); - item.Click += (sender2, ev2) => - { - if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) - mainWindow.ChangeSelection(obj); - else - { - doc.Replace(line.ParentVisualLine.StartOffset + line.RelativeTextOffset, - text.Length, (obj as UndertaleNamedResource).Name.Content, null); - (parent as UndertaleCodeEditor).DecompiledChanged = true; - } - }; - contextMenu.Items.Add(item); - } - if (id > 0x00050000) - { - MenuItem item = new MenuItem(); - item.Header = "0x" + id.ToString("X6") + " (color)"; - item.Click += (sender2, ev2) => - { - if (!((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)) - { - doc.Replace(line.ParentVisualLine.StartOffset + line.RelativeTextOffset, - text.Length, "0x" + id.ToString("X6"), null); - (parent as UndertaleCodeEditor).DecompiledChanged = true; - } - }; - contextMenu.Items.Add(item); - } - BuiltinList list = mainWindow.Data.BuiltinList; - var myKey = list.Constants.FirstOrDefault(x => x.Value == (double)id).Key; - if (myKey != null) - { - MenuItem item = new MenuItem(); - item.Header = myKey.Replace("_", "__") + " (constant)"; - item.Click += (sender2, ev2) => - { - if (!((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)) - { - doc.Replace(line.ParentVisualLine.StartOffset + line.RelativeTextOffset, - text.Length, myKey, null); - (parent as UndertaleCodeEditor).DecompiledChanged = true; - } - }; - contextMenu.Items.Add(item); - } - contextMenu.Items.Add(new MenuItem() { Header = id + " (number)", IsEnabled = false }); - - contextMenu.IsOpen = true; - } - }; - return line; - } - - return null; - } - } - - public class NameGenerator : VisualLineElementGenerator - { - readonly static Regex regex = new Regex(@"[_a-zA-Z][_a-zA-Z0-9]*"); - - public NameGenerator() - { - } - - Match FindMatch(int startOffset, Regex r) - { - // fetch the end offset of the VisualLine being generated - int endOffset = CurrentContext.VisualLine.LastDocumentLine.EndOffset; - TextDocument document = CurrentContext.Document; - string relevantText = document.GetText(startOffset, endOffset - startOffset); - return r.Match(relevantText); - } - - /// Gets the first offset >= startOffset where the generator wants to construct - /// an element. - /// Return -1 to signal no interest. - public override int GetFirstInterestedOffset(int startOffset) - { - Match m = FindMatch(startOffset, regex); - - var textArea = CurrentContext.TextView.GetService(typeof(TextArea)) as TextArea; - var highlighter = textArea.GetService(typeof(IHighlighter)) as IHighlighter; - int line = CurrentContext.Document.GetLocation(startOffset).Line; - HighlightedLine highlighted = null; - try - { - highlighted = highlighter.HighlightLine(line); - } - catch - { - } - - while (m.Success) - { - int res = startOffset + m.Index; - int currLine = CurrentContext.Document.GetLocation(res).Line; - if (currLine != line) - { - line = currLine; - highlighted = highlighter.HighlightLine(line); - } - - foreach (var section in highlighted.Sections) - { - if (section.Color.Name == "Identifier" || section.Color.Name == "Function") - { - if (section.Offset == res) - return res; - } - } - - startOffset += m.Length; - m = FindMatch(startOffset, regex); - } - return -1; - } - - /// Constructs an element at the specified offset. - /// May return null if no element should be constructed. - public override VisualLineElement ConstructElement(int offset) - { - Match m = FindMatch(offset, regex); - - if (m.Success && m.Index == 0) - { - UndertaleData data = mainWindow.Data; - bool func = (offset + m.Length + 1 < CurrentContext.VisualLine.LastDocumentLine.EndOffset) && - (CurrentContext.Document.GetCharAt(offset + m.Length) == '('); - UndertaleNamedResource val = null; - - var doc = CurrentContext.Document; - var textArea = CurrentContext.TextView.GetService(typeof(TextArea)) as TextArea; - var editor = textArea.GetService(typeof(TextEditor)) as TextEditor; - var parent = VisualTreeHelper.GetParent(editor); - do - { - if ((parent as FrameworkElement) is UserControl) - break; - parent = VisualTreeHelper.GetParent(parent); - } while (parent != null); - - // Process the content of this identifier/function - if (func) - { - val = null; - if (!data.GMS2_3) // in GMS2.3 every custom "function" is in fact a member variable and scripts are never referenced directly - val = data.Scripts.ByName(m.Value); - if (val == null) - { - val = data.Functions.ByName(m.Value); - if (data.GMS2_3) - { - if (val != null) - { - if (data.Code.ByName(val.Name.Content) != null) - val = null; // in GMS2.3 every custom "function" is in fact a member variable, and the names in functions make no sense (they have the gml_Script_ prefix) - } - else - { - // Resolve 2.3 sub-functions for their parent entry - UndertaleFunction f = null; - if (data.KnownSubFunctions?.TryGetValue(m.Value, out f) == true) - val = data.Scripts.ByName(f.Name.Content).Code?.ParentEntry; - } - } - } - if (val == null) - { - if (data.BuiltinList.Functions.ContainsKey(m.Value)) - { - var res = new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, - new SolidColorBrush(Color.FromRgb(0xFF, 0xB8, 0x71))); - res.Bold = true; - return res; - } - } - } - else - { - val = data.ByName(m.Value); - if (data.GMS2_3 & val is UndertaleScript) - val = null; // in GMS2.3 scripts are never referenced directly - } - if (val == null) - { - if (offset >= 7) - { - if (CurrentContext.Document.GetText(offset - 7, 7) == "global.") - { - return new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, - new SolidColorBrush(Color.FromRgb(0xF9, 0x7B, 0xF9))); - } - } - if (data.BuiltinList.Constants.ContainsKey(m.Value)) - return new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, - new SolidColorBrush(Color.FromRgb(0xFF, 0x80, 0x80))); - if (data.BuiltinList.GlobalNotArray.ContainsKey(m.Value) || - data.BuiltinList.Instance.ContainsKey(m.Value) || - data.BuiltinList.GlobalArray.ContainsKey(m.Value)) - return new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, - new SolidColorBrush(Color.FromRgb(0x58, 0xE3, 0x5A))); - if ((parent as UndertaleCodeEditor).CurrentLocals.Contains(m.Value)) - return new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, - new SolidColorBrush(Color.FromRgb(0xFF, 0xF8, 0x99))); - return null; - } - - var line = new ClickVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, - func ? new SolidColorBrush(Color.FromRgb(0xFF, 0xB8, 0x71)) : - new SolidColorBrush(Color.FromRgb(0xFF, 0x80, 0x80))); - if (func) - line.Bold = true; - line.Clicked += (text) => - { - mainWindow.ChangeSelection(val); - }; - - return line; - } - - return null; - } - } - - public class ColorVisualLineText : VisualLineText - { - private string Text { get; set; } - private Brush ForegroundBrush { get; set; } - - public bool Bold { get; set; } = false; - - /// - /// Creates a visual line text element with the specified length. - /// It uses the and its - /// to find the actual text string. - /// - public ColorVisualLineText(string text, VisualLine parentVisualLine, int length, Brush foregroundBrush) - : base(parentVisualLine, length) - { - Text = text; - ForegroundBrush = foregroundBrush; - } - - public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) - { - if (ForegroundBrush != null) - TextRunProperties.SetForegroundBrush(ForegroundBrush); - if (Bold) - TextRunProperties.SetTypeface(new Typeface(TextRunProperties.Typeface.FontFamily, FontStyles.Normal, FontWeights.Bold, FontStretches.Normal)); - return base.CreateTextRun(startVisualColumn, context); - } - - protected override VisualLineText CreateInstance(int length) - { - return new ColorVisualLineText(Text, ParentVisualLine, length, null); - } - } - - public class ClickVisualLineText : VisualLineText - { - - public delegate void ClickHandler(string text); - - public event ClickHandler Clicked; - - private string Text { get; set; } - private Brush ForegroundBrush { get; set; } - - public bool Bold { get; set; } = false; - - /// - /// Creates a visual line text element with the specified length. - /// It uses the and its - /// to find the actual text string. - /// - public ClickVisualLineText(string text, VisualLine parentVisualLine, int length, Brush foregroundBrush = null) - : base(parentVisualLine, length) - { - Text = text; - ForegroundBrush = foregroundBrush; - } - - - public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) - { - if (ForegroundBrush != null) - TextRunProperties.SetForegroundBrush(ForegroundBrush); - if (Bold) - TextRunProperties.SetTypeface(new Typeface(TextRunProperties.Typeface.FontFamily, FontStyles.Normal, FontWeights.Bold, FontStretches.Normal)); - return base.CreateTextRun(startVisualColumn, context); - } - - bool LinkIsClickable() - { - if (string.IsNullOrEmpty(Text)) - return false; - return (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control; - } - - - protected override void OnQueryCursor(QueryCursorEventArgs e) - { - if (LinkIsClickable()) - { - e.Handled = true; - e.Cursor = Cursors.Hand; - } - } - - protected override void OnMouseDown(MouseButtonEventArgs e) - { - if (e.Handled) - return; - if ((e.ChangedButton == System.Windows.Input.MouseButton.Left && LinkIsClickable()) || - e.ChangedButton == System.Windows.Input.MouseButton.Middle) - { - if (Clicked != null) - { - Clicked(Text); - e.Handled = true; - } - } - } - - protected override VisualLineText CreateInstance(int length) - { - var res = new ClickVisualLineText(Text, ParentVisualLine, length); - res.Clicked += Clicked; - return res; - } - } - } -} + /// + /// Logika interakcji dla klasy UndertaleCodeEditor.xaml + /// + [SupportedOSPlatform("windows7.0")] + public partial class UndertaleCodeEditor : DataUserControl + { + private static MainWindow mainWindow = Application.Current.MainWindow as MainWindow; + + public UndertaleCode CurrentDisassembled = null; + public UndertaleCode CurrentDecompiled = null; + public List CurrentLocals = null; + public UndertaleCode CurrentGraphed = null; + public string ProfileHash = mainWindow.ProfileHash; + public string MainPath = Path.Combine(Settings.ProfilesFolder, mainWindow.ProfileHash, "Main"); + public string TempPath = Path.Combine(Settings.ProfilesFolder, mainWindow.ProfileHash, "Temp"); + + public bool DecompiledFocused = false; + public bool DecompiledChanged = false; + public bool DecompiledYet = false; + public bool DecompiledSkipped = false; + public SearchPanel DecompiledSearchPanel; + + public bool DisassemblyFocused = false; + public bool DisassemblyChanged = false; + public bool DisassembledYet = false; + public bool DisassemblySkipped = false; + public SearchPanel DisassemblySearchPanel; + + public static RoutedUICommand Compile = new RoutedUICommand("Compile code", "Compile", typeof(UndertaleCodeEditor)); + + public UndertaleCodeEditor() + { + InitializeComponent(); + + // Decompiled editor styling and functionality + DecompiledSearchPanel = SearchPanel.Install(DecompiledEditor.TextArea); + DecompiledSearchPanel.MarkerBrush = new SolidColorBrush(Color.FromRgb(90, 90, 90)); + + using (Stream stream = this.GetType().Assembly.GetManifestResourceStream("UndertaleModTool.Resources.GML.xshd")) + { + using (XmlTextReader reader = new XmlTextReader(stream)) + { + DecompiledEditor.SyntaxHighlighting = HighlightingLoader.Load(reader, HighlightingManager.Instance); + var def = DecompiledEditor.SyntaxHighlighting; + if (mainWindow.Data.GeneralInfo.Major < 2) + { + foreach (var span in def.MainRuleSet.Spans) + { + string expr = span.StartExpression.ToString(); + if (expr == "\"" || expr == "'") + { + span.RuleSet.Spans.Clear(); + } + } + } + // This was an attempt to only highlight + // GMS 2.3+ keywords if the game is + // made in such a version. + // However despite what StackOverflow + // says, this isn't working so it's just + // hardcoded in the XML for now + /* + if(mainWindow.Data.GMS2_3) + { + HighlightingColor color = null; + foreach (var rule in def.MainRuleSet.Rules) + { + if (rule.Regex.IsMatch("if")) + { + color = rule.Color; + break; + } + } + if (color != null) + { + string[] keywords = + { + "new", + "function", + "keywords" + }; + var rule = new HighlightingRule(); + var regex = String.Format(@"\b(?>{0})\b", String.Join("|", keywords)); + + rule.Regex = new Regex(regex); + rule.Color = color; + + def.MainRuleSet.Rules.Add(rule); + } + }*/ + } + } + + DecompiledEditor.Options.ConvertTabsToSpaces = true; + + DecompiledEditor.TextArea.TextView.ElementGenerators.Add(new NumberGenerator()); + DecompiledEditor.TextArea.TextView.ElementGenerators.Add(new NameGenerator()); + + DecompiledEditor.TextArea.TextView.Options.HighlightCurrentLine = true; + DecompiledEditor.TextArea.TextView.CurrentLineBackground = new SolidColorBrush(Color.FromRgb(60, 60, 60)); + DecompiledEditor.TextArea.TextView.CurrentLineBorder = new Pen() { Thickness = 0 }; + + DecompiledEditor.Document.TextChanged += (s, e) => + { + DecompiledFocused = true; + DecompiledChanged = true; + }; + + DecompiledEditor.TextArea.SelectionBrush = new SolidColorBrush(Color.FromRgb(100, 100, 100)); + DecompiledEditor.TextArea.SelectionForeground = null; + DecompiledEditor.TextArea.SelectionBorder = null; + DecompiledEditor.TextArea.SelectionCornerRadius = 0; + + // Disassembly editor styling and functionality + DisassemblySearchPanel = SearchPanel.Install(DisassemblyEditor.TextArea); + DisassemblySearchPanel.MarkerBrush = new SolidColorBrush(Color.FromRgb(90, 90, 90)); + + using (Stream stream = this.GetType().Assembly.GetManifestResourceStream("UndertaleModTool.Resources.VMASM.xshd")) + { + using (XmlTextReader reader = new XmlTextReader(stream)) + { + DisassemblyEditor.SyntaxHighlighting = HighlightingLoader.Load(reader, HighlightingManager.Instance); + } + } + + DisassemblyEditor.TextArea.TextView.ElementGenerators.Add(new NameGenerator()); + + DisassemblyEditor.TextArea.TextView.Options.HighlightCurrentLine = true; + DisassemblyEditor.TextArea.TextView.CurrentLineBackground = new SolidColorBrush(Color.FromRgb(60, 60, 60)); + DisassemblyEditor.TextArea.TextView.CurrentLineBorder = new Pen() { Thickness = 0 }; + + DisassemblyEditor.Document.TextChanged += (s, e) => DisassemblyChanged = true; + + DisassemblyEditor.TextArea.SelectionBrush = new SolidColorBrush(Color.FromRgb(100, 100, 100)); + DisassemblyEditor.TextArea.SelectionForeground = null; + DisassemblyEditor.TextArea.SelectionBorder = null; + DisassemblyEditor.TextArea.SelectionCornerRadius = 0; + } + + private async void TabControl_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + UndertaleCode code = this.DataContext as UndertaleCode; + Directory.CreateDirectory(MainPath); + Directory.CreateDirectory(TempPath); + if (code == null) + return; + DecompiledSearchPanel.Close(); + DisassemblySearchPanel.Close(); + await DecompiledLostFocusBody(sender, null); + DisassemblyEditor_LostFocus(sender, null); + if (DisassemblyTab.IsSelected && code != CurrentDisassembled) + { + DisassembleCode(code, !DisassembledYet); + DisassembledYet = true; + } + if (DecompiledTab.IsSelected && code != CurrentDecompiled) + { + _ = DecompileCode(code, !DecompiledYet); + DecompiledYet = true; + } + if (GraphTab.IsSelected && code != CurrentGraphed) + { + GraphCode(code); + } + } + + private async void UserControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) + { + UndertaleCode code = this.DataContext as UndertaleCode; + if (code == null) + return; + + // compile/disassemble previously edited code (save changes) + if (DecompiledTab.IsSelected && DecompiledFocused && DecompiledChanged && + CurrentDecompiled is not null && CurrentDecompiled != code) + { + DecompiledSkipped = true; + DecompiledEditor_LostFocus(sender, null); + + } + else if (DisassemblyTab.IsSelected && DisassemblyFocused && DisassemblyChanged && + CurrentDisassembled is not null && CurrentDisassembled != code) + { + DisassemblySkipped = true; + DisassemblyEditor_LostFocus(sender, null); + } + + DecompiledEditor_LostFocus(sender, null); + DisassemblyEditor_LostFocus(sender, null); + + if (MainWindow.CodeEditorDecompile != Unstated) //if opened from the code search results "link" + { + if (MainWindow.CodeEditorDecompile == DontDecompile && code != CurrentDisassembled) + { + if (CodeModeTabs.SelectedItem != DisassemblyTab) + CodeModeTabs.SelectedItem = DisassemblyTab; + else + DisassembleCode(code, true); + } + + if (MainWindow.CodeEditorDecompile == Decompile && code != CurrentDecompiled) + { + if (CodeModeTabs.SelectedItem != DecompiledTab) + CodeModeTabs.SelectedItem = DecompiledTab; + else + _ = DecompileCode(code, true); + } + + MainWindow.CodeEditorDecompile = Unstated; + } + else + { + if (DisassemblyTab.IsSelected && code != CurrentDisassembled) + { + DisassembleCode(code, true); + } + if (DecompiledTab.IsSelected && code != CurrentDecompiled) + { + _ = DecompileCode(code, true); + } + if (GraphTab.IsSelected && code != CurrentGraphed) + { + GraphCode(code); + } + } + } + + public static readonly RoutedEvent CtrlKEvent = EventManager.RegisterRoutedEvent( + "CtrlK", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(UndertaleCodeEditor)); + + private async Task CompileCommandBody(object sender, EventArgs e) + { + if (DecompiledFocused) + { + await DecompiledLostFocusBody(sender, new RoutedEventArgs(CtrlKEvent)); + } + else if (DisassemblyFocused) + { + DisassemblyEditor_LostFocus(sender, new RoutedEventArgs(CtrlKEvent)); + DisassemblyEditor_GotFocus(sender, null); + } + + await Task.Delay(1); //dummy await + } + private void Command_Compile(object sender, EventArgs e) + { + _ = CompileCommandBody(sender, e); + } + public async Task SaveChanges() + { + await CompileCommandBody(null, null); + } + + private void DisassembleCode(UndertaleCode code, bool first) + { + code.UpdateAddresses(); + + string text; + + DisassemblyEditor.TextArea.ClearSelection(); + if (code.ParentEntry != null) + { + DisassemblyEditor.IsReadOnly = true; + text = "; This code entry is a reference to an anonymous function within " + code.ParentEntry.Name.Content + ", view it there"; + } + else + { + DisassemblyEditor.IsReadOnly = false; + + var data = mainWindow.Data; + text = code.Disassemble(data.Variables, data.CodeLocals.For(code)); + + CurrentLocals = new List(); + } + + DisassemblyEditor.Document.BeginUpdate(); + DisassemblyEditor.Document.Text = text; + DisassemblyEditor.Document.EndUpdate(); + + if (first) + DisassemblyEditor.Document.UndoStack.ClearAll(); + + CurrentDisassembled = code; + DisassemblyChanged = false; + } + + public static Dictionary gettext = null; + private void UpdateGettext(UndertaleCode gettextCode) + { + gettext = new Dictionary(); + string[] decompilationOutput; + if (!SettingsWindow.ProfileModeEnabled) + decompilationOutput = Decompiler.Decompile(gettextCode, new GlobalDecompileContext(null, false)).Replace("\r\n", "\n").Split('\n'); + else + { + try + { + string path = Path.Combine(TempPath, gettextCode.Name.Content + ".gml"); + if (File.Exists(path)) + decompilationOutput = File.ReadAllText(path).Replace("\r\n", "\n").Split('\n'); + else + decompilationOutput = Decompiler.Decompile(gettextCode, new GlobalDecompileContext(null, false)).Replace("\r\n", "\n").Split('\n'); + } + catch + { + decompilationOutput = Decompiler.Decompile(gettextCode, new GlobalDecompileContext(null, false)).Replace("\r\n", "\n").Split('\n'); + } + } + Regex textdataRegex = new Regex("^ds_map_add\\(global\\.text_data_en, \\\"(.*)\\\", \\\"(.*)\\\"\\)"); + foreach (var line in decompilationOutput) + { + Match m = textdataRegex.Match(line); + if (m.Success) + { + try + { + gettext.Add(m.Groups[1].Value, m.Groups[2].Value); + } + catch (ArgumentException) + { + MessageBox.Show("There is a duplicate key in textdata_en, being " + m.Groups[1].Value + ". This may cause errors in the comment display of text.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + } + catch + { + MessageBox.Show("Unknown error in textdata_en. This may cause errors in the comment display of text.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + } + } + + public static Dictionary gettextJSON = null; + private string UpdateGettextJSON(string json) + { + try + { + gettextJSON = JsonConvert.DeserializeObject>(json); + } + catch (Exception e) + { + gettextJSON = new Dictionary(); + return "Failed to parse language file: " + e.Message; + } + return null; + } + + private async Task DecompileCode(UndertaleCode code, bool first, LoaderDialog existingDialog = null) + { + DecompiledEditor.IsReadOnly = true; + DecompiledEditor.TextArea.ClearSelection(); + if (code.ParentEntry != null) + { + DecompiledEditor.Text = "// This code entry is a reference to an anonymous function within " + code.ParentEntry.Name.Content + ", view it there"; + CurrentDecompiled = code; + existingDialog?.TryClose(); + } + else + { + LoaderDialog dialog; + if (existingDialog != null) + { + dialog = existingDialog; + dialog.Message = "Decompiling, please wait..."; + } + else + { + dialog = new LoaderDialog("Decompiling", "Decompiling, please wait... This can take a while on complex scripts."); + dialog.Owner = Window.GetWindow(this); + try + { + _ = Dispatcher.BeginInvoke(new Action(() => { if (!dialog.IsClosed) dialog.TryShowDialog(); })); + } + catch + { + // This is still a problem in rare cases for some unknown reason + } + } + + bool openSaveDialog = false; + + UndertaleCode gettextCode = null; + if (gettext == null) + gettextCode = mainWindow.Data.Code.ByName("gml_Script_textdata_en"); + + string dataPath = Path.GetDirectoryName(mainWindow.FilePath); + string gettextJsonPath = null; + if (dataPath is not null) + { + gettextJsonPath = Path.Combine(dataPath, "lang", "lang_en.json"); + if (!File.Exists(gettextJsonPath)) + gettextJsonPath = Path.Combine(dataPath, "lang", "lang_en_ch1.json"); + } + + var dataa = mainWindow.Data; + Task t = Task.Run(() => + { + GlobalDecompileContext context = new GlobalDecompileContext(dataa, false); + string decompiled = null; + Exception e = null; + try + { + string path = Path.Combine(TempPath, code.Name.Content + ".gml"); + if (!SettingsWindow.ProfileModeEnabled || !File.Exists(path)) + { + decompiled = Decompiler.Decompile(code, context); + } + else + decompiled = File.ReadAllText(path); + } + catch (Exception ex) + { + e = ex; + } + + if (gettextCode != null) + UpdateGettext(gettextCode); + + try + { + if (gettextJSON == null && gettextJsonPath != null && File.Exists(gettextJsonPath)) + { + string err = UpdateGettextJSON(File.ReadAllText(gettextJsonPath)); + if (err != null) + e = new Exception(err); + } + } + catch (Exception exc) + { + MessageBox.Show(exc.ToString()); + } + + if (decompiled != null) + { + string[] decompiledLines; + if (gettext != null && decompiled.Contains("scr_gettext")) + { + decompiledLines = decompiled.Split('\n'); + for (int i = 0; i < decompiledLines.Length; i++) + { + var matches = Regex.Matches(decompiledLines[i], "scr_gettext\\(\\\"(\\w*)\\\"\\)"); + foreach (Match match in matches) + { + if (match.Success) + { + if (gettext.TryGetValue(match.Groups[1].Value, out string text)) + decompiledLines[i] += $" // {text}"; + } + } + } + decompiled = string.Join('\n', decompiledLines); + } + else if (gettextJSON != null && decompiled.Contains("scr_84_get_lang_string")) + { + decompiledLines = decompiled.Split('\n'); + for (int i = 0; i < decompiledLines.Length; i++) + { + var matches = Regex.Matches(decompiledLines[i], "scr_84_get_lang_string(\\w*)\\(\\\"(\\w*)\\\"\\)"); + foreach (Match match in matches) + { + if (match.Success) + { + if (gettextJSON.TryGetValue(match.Groups[^1].Value, out string text)) + decompiledLines[i] += $" // {text}"; + } + } + } + decompiled = string.Join('\n', decompiledLines); + } + } + + Dispatcher.Invoke(() => + { + if (DataContext != code) + return; // Switched to another code entry or otherwise + + DecompiledEditor.Document.BeginUpdate(); + if (e != null) + DecompiledEditor.Document.Text = "/* EXCEPTION!\n " + e.ToString() + "\n*/"; + else if (decompiled != null) + { + DecompiledEditor.Document.Text = decompiled; + CurrentLocals = new List(); + + var locals = dataa.CodeLocals.ByName(code.Name.Content); + if (locals != null) + { + foreach (var local in locals.Locals) + CurrentLocals.Add(local.Name.Content); + } + + if (existingDialog is not null) //if code was edited (and compiles after it) + { + dataa.GMLCacheChanged.Add(code.Name.Content); + dataa.GMLCacheFailed?.Remove(code.Name.Content); //remove that code name, since that code compiles now + + openSaveDialog = mainWindow.IsSaving; + } + } + DecompiledEditor.Document.EndUpdate(); + DecompiledEditor.IsReadOnly = false; + if (first) + DecompiledEditor.Document.UndoStack.ClearAll(); + DecompiledChanged = false; + + CurrentDecompiled = code; + dialog.Hide(); + }); + }); + await t; + dialog.Close(); + + mainWindow.IsSaving = false; + + if (openSaveDialog) + await mainWindow.DoSaveDialog(); + } + } + + private async void GraphCode(UndertaleCode code) + { + if (code.ParentEntry != null) + { + GraphView.Source = null; + CurrentGraphed = code; + return; + } + + LoaderDialog dialog = new LoaderDialog("Generating graph", "Generating graph, please wait..."); + dialog.Owner = Window.GetWindow(this); + Task t = Task.Run(() => + { + ImageSource image = null; + try + { + code.UpdateAddresses(); + List entryPoints = new List(); + entryPoints.Add(0); + foreach (UndertaleCode duplicate in code.ChildEntries) + entryPoints.Add(duplicate.Offset / 4); + var blocks = Decompiler.DecompileFlowGraph(code, entryPoints); + string dot = Decompiler.ExportFlowGraph(blocks); + + try + { + var getStartProcessQuery = new GetStartProcessQuery(); + var getProcessStartInfoQuery = new GetProcessStartInfoQuery(); + var registerLayoutPluginCommand = new RegisterLayoutPluginCommand(getProcessStartInfoQuery, getStartProcessQuery); + var wrapper = new GraphGeneration(getStartProcessQuery, getProcessStartInfoQuery, registerLayoutPluginCommand); + wrapper.GraphvizPath = Settings.Instance.GraphVizPath; + + byte[] output = wrapper.GenerateGraph(dot, Enums.GraphReturnType.Png); // TODO: Use SVG instead + + image = new ImageSourceConverter().ConvertFrom(output) as ImageSource; + } + catch (Exception e) + { + Debug.WriteLine(e.ToString()); + if (MessageBox.Show("Unable to execute GraphViz: " + e.Message + "\nMake sure you have downloaded it and set the path in settings.\nDo you want to open the download page now?", "Graph generation failed", MessageBoxButton.YesNo, MessageBoxImage.Error) == MessageBoxResult.Yes) + MainWindow.OpenBrowser("https://graphviz.gitlab.io/_pages/Download/Download_windows.html"); + } + } + catch (Exception e) + { + Debug.WriteLine(e.ToString()); + MessageBox.Show(e.Message, "Graph generation failed", MessageBoxButton.OK, MessageBoxImage.Error); + } + + Dispatcher.Invoke(() => + { + GraphView.Source = image; + CurrentGraphed = code; + dialog.Hide(); + }); + }); + dialog.ShowDialog(); + await t; + } + + private void DecompiledEditor_GotFocus(object sender, RoutedEventArgs e) + { + if (DecompiledEditor.IsReadOnly) + return; + DecompiledFocused = true; + } + + private static string Truncate(string value, int maxChars) + { + return value.Length <= maxChars ? value : value.Substring(0, maxChars) + "..."; + } + + private async Task DecompiledLostFocusBody(object sender, RoutedEventArgs e) + { + if (!DecompiledFocused) + return; + if (DecompiledEditor.IsReadOnly) + return; + DecompiledFocused = false; + + if (!DecompiledChanged) + return; + + UndertaleCode code; + if (DecompiledSkipped) + { + code = CurrentDecompiled; + DecompiledSkipped = false; + } + else + code = this.DataContext as UndertaleCode; + + if (code == null) + { + if (IsLoaded) + code = CurrentDecompiled; // switched to the tab with different object type + else + return; // probably loaded another data.win or something. + } + + if (code.ParentEntry != null) + return; + + // Check to make sure this isn't an element inside of the textbox, or another tab + IInputElement elem = Keyboard.FocusedElement; + if (elem is UIElement) + { + if (e != null && e.RoutedEvent?.Name != "CtrlK" && (elem as UIElement).IsDescendantOf(DecompiledEditor)) + return; + } + + UndertaleData data = mainWindow.Data; + + LoaderDialog dialog = new LoaderDialog("Compiling", "Compiling, please wait..."); + dialog.Owner = Window.GetWindow(this); + try + { + _ = Dispatcher.BeginInvoke(new Action(() => { if (!dialog.IsClosed) dialog.TryShowDialog(); })); + } + catch + { + // This is still a problem in rare cases for some unknown reason + } + + CompileContext compileContext = null; + string text = DecompiledEditor.Text; + var dispatcher = Dispatcher; + Task t = Task.Run(() => + { + compileContext = Compiler.CompileGMLText(text, data, code, (f) => { dispatcher.Invoke(() => f()); }); + }); + await t; + + if (compileContext == null) + { + dialog.TryClose(); + MessageBox.Show("Compile context was null for some reason...", "This shouldn't happen", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + + if (compileContext.HasError) + { + dialog.TryClose(); + MessageBox.Show(Truncate(compileContext.ResultError, 512), "Compiler error", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + + if (!compileContext.SuccessfulCompile) + { + dialog.TryClose(); + MessageBox.Show("(unknown error message)", "Compile failed", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + + code.Replace(compileContext.ResultAssembly); + + if (!mainWindow.Data.GMS2_3) + { + try + { + string path = Path.Combine(TempPath, code.Name.Content + ".gml"); + if (SettingsWindow.ProfileModeEnabled) + { + // Write text, only if in the profile mode. + File.WriteAllText(path, DecompiledEditor.Text); + } + else + { + // Destroy file with comments if it's been edited outside the profile mode. + // We're dealing with the decompiled code only, it has to happen. + // Otherwise it will cause a desync, which is more important to prevent. + if (File.Exists(path)) + File.Delete(path); + } + } + catch (Exception exc) + { + MessageBox.Show("Error during writing of GML code to profile:\n" + exc.ToString()); + } + } + + // Invalidate gettext if necessary + if (code.Name.Content == "gml_Script_textdata_en") + gettext = null; + + // Show new code, decompiled. + CurrentDisassembled = null; + CurrentDecompiled = null; + CurrentGraphed = null; + + // Tab switch + if (e == null) + { + dialog.TryClose(); + return; + } + + // Decompile new code + await DecompileCode(code, false, dialog); + + //GMLCacheChanged.Add() is inside DecompileCode() + } + private void DecompiledEditor_LostFocus(object sender, RoutedEventArgs e) + { + _ = DecompiledLostFocusBody(sender, e); + } + + private void DisassemblyEditor_GotFocus(object sender, RoutedEventArgs e) + { + if (DisassemblyEditor.IsReadOnly) + return; + DisassemblyFocused = true; + } + + private void DisassemblyEditor_LostFocus(object sender, RoutedEventArgs e) + { + if (!DisassemblyFocused) + return; + if (DisassemblyEditor.IsReadOnly) + return; + DisassemblyFocused = false; + + if (!DisassemblyChanged) + return; + + UndertaleCode code; + if (DisassemblySkipped) + { + code = CurrentDisassembled; + DisassemblySkipped = false; + } + else + code = this.DataContext as UndertaleCode; + + if (code == null) + { + if (IsLoaded) + code = CurrentDisassembled; // switched to the tab with different object type + else + return; // probably loaded another data.win or something. + } + + // Check to make sure this isn't an element inside of the textbox, or another tab + IInputElement elem = Keyboard.FocusedElement; + if (elem is UIElement) + { + if (e != null && e.RoutedEvent?.Name != "CtrlK" && (elem as UIElement).IsDescendantOf(DisassemblyEditor)) + return; + } + + UndertaleData data = mainWindow.Data; + try + { + var instructions = Assembler.Assemble(DisassemblyEditor.Text, data); + code.Replace(instructions); + mainWindow.NukeProfileGML(code.Name.Content); + } + catch (Exception ex) + { + MessageBox.Show(ex.ToString(), "Assembler error", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + + // Get rid of old code + CurrentDisassembled = null; + CurrentDecompiled = null; + CurrentGraphed = null; + + // Tab switch + if (e == null) + return; + + // Disassemble new code + DisassembleCode(code, false); + + if (!DisassemblyEditor.IsReadOnly) + { + data.GMLCacheChanged.Add(code.Name.Content); + + if (mainWindow.IsSaving) + { + mainWindow.IsSaving = false; + + _ = mainWindow.DoSaveDialog(); + } + } + } + + // Based on https://stackoverflow.com/questions/28379206/custom-hyperlinks-using-avalonedit + public class NumberGenerator : VisualLineElementGenerator + { + readonly static Regex regex = new Regex(@"-?\d+\.?"); + + public NumberGenerator() + { + } + + Match FindMatch(int startOffset, Regex r) + { + // fetch the end offset of the VisualLine being generated + int endOffset = CurrentContext.VisualLine.LastDocumentLine.EndOffset; + TextDocument document = CurrentContext.Document; + string relevantText = document.GetText(startOffset, endOffset - startOffset); + return r.Match(relevantText); + } + + /// Gets the first offset >= startOffset where the generator wants to construct + /// an element. + /// Return -1 to signal no interest. + public override int GetFirstInterestedOffset(int startOffset) + { + Match m = FindMatch(startOffset, regex); + + var textArea = CurrentContext.TextView.GetService(typeof(TextArea)) as TextArea; + var highlighter = textArea.GetService(typeof(IHighlighter)) as IHighlighter; + int line = CurrentContext.Document.GetLocation(startOffset).Line; + HighlightedLine highlighted = null; + try + { + highlighted = highlighter.HighlightLine(line); + } + catch + { + } + + while (m.Success) + { + int res = startOffset + m.Index; + int currLine = CurrentContext.Document.GetLocation(res).Line; + if (currLine != line) + { + line = currLine; + highlighted = highlighter.HighlightLine(line); + } + + foreach (var section in highlighted.Sections) + { + if (section.Color.Name == "Number" && + section.Offset == res) + return res; + } + + startOffset += m.Length; + m = FindMatch(startOffset, regex); + } + + return -1; + } + + /// Constructs an element at the specified offset. + /// May return null if no element should be constructed. + public override VisualLineElement ConstructElement(int offset) + { + Match m = FindMatch(offset, regex); + + if (m.Success && m.Index == 0) + { + var line = new ClickVisualLineText(m.Value, CurrentContext.VisualLine, m.Length); + var doc = CurrentContext.Document; + var textArea = CurrentContext.TextView.GetService(typeof(TextArea)) as TextArea; + var editor = textArea.GetService(typeof(TextEditor)) as TextEditor; + var parent = VisualTreeHelper.GetParent(editor); + do + { + if ((parent as FrameworkElement) is UserControl) + break; + parent = VisualTreeHelper.GetParent(parent); + } while (parent != null); + line.Clicked += (text) => + { + if (text.EndsWith(".")) + return; + if (int.TryParse(text, out int id)) + { + (parent as UndertaleCodeEditor).DecompiledFocused = true; + UndertaleData data = mainWindow.Data; + + List possibleObjects = new List(); + if (id >= 0) + { + if (id < data.Sprites.Count) + possibleObjects.Add(data.Sprites[id]); + if (id < data.Rooms.Count) + possibleObjects.Add(data.Rooms[id]); + if (id < data.GameObjects.Count) + possibleObjects.Add(data.GameObjects[id]); + if (id < data.Backgrounds.Count) + possibleObjects.Add(data.Backgrounds[id]); + if (id < data.Scripts.Count) + possibleObjects.Add(data.Scripts[id]); + if (id < data.Paths.Count) + possibleObjects.Add(data.Paths[id]); + if (id < data.Fonts.Count) + possibleObjects.Add(data.Fonts[id]); + if (id < data.Sounds.Count) + possibleObjects.Add(data.Sounds[id]); + if (id < data.Shaders.Count) + possibleObjects.Add(data.Shaders[id]); + if (id < data.Timelines.Count) + possibleObjects.Add(data.Timelines[id]); + } + + ContextMenu contextMenu = new ContextMenu(); + foreach (UndertaleObject obj in possibleObjects) + { + MenuItem item = new MenuItem(); + item.Header = obj.ToString().Replace("_", "__"); + item.Click += (sender2, ev2) => + { + if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) + mainWindow.ChangeSelection(obj); + else + { + doc.Replace(line.ParentVisualLine.StartOffset + line.RelativeTextOffset, + text.Length, (obj as UndertaleNamedResource).Name.Content, null); + (parent as UndertaleCodeEditor).DecompiledChanged = true; + } + }; + contextMenu.Items.Add(item); + } + if (id > 0x00050000) + { + MenuItem item = new MenuItem(); + item.Header = "0x" + id.ToString("X6") + " (color)"; + item.Click += (sender2, ev2) => + { + if (!((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)) + { + doc.Replace(line.ParentVisualLine.StartOffset + line.RelativeTextOffset, + text.Length, "0x" + id.ToString("X6"), null); + (parent as UndertaleCodeEditor).DecompiledChanged = true; + } + }; + contextMenu.Items.Add(item); + } + BuiltinList list = mainWindow.Data.BuiltinList; + var myKey = list.Constants.FirstOrDefault(x => x.Value == (double)id).Key; + if (myKey != null) + { + MenuItem item = new MenuItem(); + item.Header = myKey.Replace("_", "__") + " (constant)"; + item.Click += (sender2, ev2) => + { + if (!((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)) + { + doc.Replace(line.ParentVisualLine.StartOffset + line.RelativeTextOffset, + text.Length, myKey, null); + (parent as UndertaleCodeEditor).DecompiledChanged = true; + } + }; + contextMenu.Items.Add(item); + } + contextMenu.Items.Add(new MenuItem() { Header = id + " (number)", IsEnabled = false }); + + contextMenu.IsOpen = true; + } + }; + return line; + } + + return null; + } + } + + public class NameGenerator : VisualLineElementGenerator + { + readonly static Regex regex = new Regex(@"[_a-zA-Z][_a-zA-Z0-9]*"); + + public NameGenerator() + { + } + + Match FindMatch(int startOffset, Regex r) + { + // fetch the end offset of the VisualLine being generated + int endOffset = CurrentContext.VisualLine.LastDocumentLine.EndOffset; + TextDocument document = CurrentContext.Document; + string relevantText = document.GetText(startOffset, endOffset - startOffset); + return r.Match(relevantText); + } + + /// Gets the first offset >= startOffset where the generator wants to construct + /// an element. + /// Return -1 to signal no interest. + public override int GetFirstInterestedOffset(int startOffset) + { + Match m = FindMatch(startOffset, regex); + + var textArea = CurrentContext.TextView.GetService(typeof(TextArea)) as TextArea; + var highlighter = textArea.GetService(typeof(IHighlighter)) as IHighlighter; + int line = CurrentContext.Document.GetLocation(startOffset).Line; + HighlightedLine highlighted = null; + try + { + highlighted = highlighter.HighlightLine(line); + } + catch + { + } + + while (m.Success) + { + int res = startOffset + m.Index; + int currLine = CurrentContext.Document.GetLocation(res).Line; + if (currLine != line) + { + line = currLine; + highlighted = highlighter.HighlightLine(line); + } + + foreach (var section in highlighted.Sections) + { + if (section.Color.Name == "Identifier" || section.Color.Name == "Function") + { + if (section.Offset == res) + return res; + } + } + + startOffset += m.Length; + m = FindMatch(startOffset, regex); + } + return -1; + } + + /// Constructs an element at the specified offset. + /// May return null if no element should be constructed. + public override VisualLineElement ConstructElement(int offset) + { + Match m = FindMatch(offset, regex); + + if (m.Success && m.Index == 0) + { + UndertaleData data = mainWindow.Data; + bool func = (offset + m.Length + 1 < CurrentContext.VisualLine.LastDocumentLine.EndOffset) && + (CurrentContext.Document.GetCharAt(offset + m.Length) == '('); + UndertaleNamedResource val = null; + + var doc = CurrentContext.Document; + var textArea = CurrentContext.TextView.GetService(typeof(TextArea)) as TextArea; + var editor = textArea.GetService(typeof(TextEditor)) as TextEditor; + var parent = VisualTreeHelper.GetParent(editor); + do + { + if ((parent as FrameworkElement) is UserControl) + break; + parent = VisualTreeHelper.GetParent(parent); + } while (parent != null); + + // Process the content of this identifier/function + if (func) + { + val = null; + if (!data.GMS2_3) // in GMS2.3 every custom "function" is in fact a member variable and scripts are never referenced directly + val = data.Scripts.ByName(m.Value); + if (val == null) + { + val = data.Functions.ByName(m.Value); + if (data.GMS2_3) + { + if (val != null) + { + if (data.Code.ByName(val.Name.Content) != null) + val = null; // in GMS2.3 every custom "function" is in fact a member variable, and the names in functions make no sense (they have the gml_Script_ prefix) + } + else + { + // Resolve 2.3 sub-functions for their parent entry + UndertaleFunction f = null; + if (data.KnownSubFunctions?.TryGetValue(m.Value, out f) == true) + val = data.Scripts.ByName(f.Name.Content).Code?.ParentEntry; + } + } + } + if (val == null) + { + if (data.BuiltinList.Functions.ContainsKey(m.Value)) + { + var res = new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, + new SolidColorBrush(Color.FromRgb(0xFF, 0xB8, 0x71))); + res.Bold = true; + return res; + } + } + } + else + { + val = data.ByName(m.Value); + if (data.GMS2_3 & val is UndertaleScript) + val = null; // in GMS2.3 scripts are never referenced directly + } + if (val == null) + { + if (offset >= 7) + { + if (CurrentContext.Document.GetText(offset - 7, 7) == "global.") + { + return new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, + new SolidColorBrush(Color.FromRgb(0xF9, 0x7B, 0xF9))); + } + } + if (data.BuiltinList.Constants.ContainsKey(m.Value)) + return new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, + new SolidColorBrush(Color.FromRgb(0xFF, 0x80, 0x80))); + if (data.BuiltinList.GlobalNotArray.ContainsKey(m.Value) || + data.BuiltinList.Instance.ContainsKey(m.Value) || + data.BuiltinList.GlobalArray.ContainsKey(m.Value)) + return new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, + new SolidColorBrush(Color.FromRgb(0x58, 0xE3, 0x5A))); + if ((parent as UndertaleCodeEditor).CurrentLocals.Contains(m.Value)) + return new ColorVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, + new SolidColorBrush(Color.FromRgb(0xFF, 0xF8, 0x99))); + return null; + } + + var line = new ClickVisualLineText(m.Value, CurrentContext.VisualLine, m.Length, + func ? new SolidColorBrush(Color.FromRgb(0xFF, 0xB8, 0x71)) : + new SolidColorBrush(Color.FromRgb(0xFF, 0x80, 0x80))); + if (func) + line.Bold = true; + line.Clicked += (text) => + { + mainWindow.ChangeSelection(val); + }; + + return line; + } + + return null; + } + } + + public class ColorVisualLineText : VisualLineText + { + private string Text { get; set; } + private Brush ForegroundBrush { get; set; } + + public bool Bold { get; set; } = false; + + /// + /// Creates a visual line text element with the specified length. + /// It uses the and its + /// to find the actual text string. + /// + public ColorVisualLineText(string text, VisualLine parentVisualLine, int length, Brush foregroundBrush) + : base(parentVisualLine, length) + { + Text = text; + ForegroundBrush = foregroundBrush; + } + + public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) + { + if (ForegroundBrush != null) + TextRunProperties.SetForegroundBrush(ForegroundBrush); + if (Bold) + TextRunProperties.SetTypeface(new Typeface(TextRunProperties.Typeface.FontFamily, FontStyles.Normal, FontWeights.Bold, FontStretches.Normal)); + return base.CreateTextRun(startVisualColumn, context); + } + + protected override VisualLineText CreateInstance(int length) + { + return new ColorVisualLineText(Text, ParentVisualLine, length, null); + } + } + + public class ClickVisualLineText : VisualLineText + { + + public delegate void ClickHandler(string text); + + public event ClickHandler Clicked; + + private string Text { get; set; } + private Brush ForegroundBrush { get; set; } + + public bool Bold { get; set; } = false; + + /// + /// Creates a visual line text element with the specified length. + /// It uses the and its + /// to find the actual text string. + /// + public ClickVisualLineText(string text, VisualLine parentVisualLine, int length, Brush foregroundBrush = null) + : base(parentVisualLine, length) + { + Text = text; + ForegroundBrush = foregroundBrush; + } + + + public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) + { + if (ForegroundBrush != null) + TextRunProperties.SetForegroundBrush(ForegroundBrush); + if (Bold) + TextRunProperties.SetTypeface(new Typeface(TextRunProperties.Typeface.FontFamily, FontStyles.Normal, FontWeights.Bold, FontStretches.Normal)); + return base.CreateTextRun(startVisualColumn, context); + } + + bool LinkIsClickable() + { + if (string.IsNullOrEmpty(Text)) + return false; + return (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control; + } + + + protected override void OnQueryCursor(QueryCursorEventArgs e) + { + if (LinkIsClickable()) + { + e.Handled = true; + e.Cursor = Cursors.Hand; + } + } + + protected override void OnMouseDown(MouseButtonEventArgs e) + { + if (e.Handled) + return; + if ((e.ChangedButton == System.Windows.Input.MouseButton.Left && LinkIsClickable()) || + e.ChangedButton == System.Windows.Input.MouseButton.Middle) + { + if (Clicked != null) + { + Clicked(Text); + e.Handled = true; + } + } + } + + protected override VisualLineText CreateInstance(int length) + { + var res = new ClickVisualLineText(Text, ParentVisualLine, length); + res.Clicked += Clicked; + return res; + } + } + } +} From 047c81579dd347d1acb1e8727f5958ad4b01dfa1 Mon Sep 17 00:00:00 2001 From: plrusek Date: Wed, 25 May 2022 16:40:53 +0200 Subject: [PATCH 7/8] Apply suggestions from code review Add empty lines back in Co-authored-by: VladiStep --- UndertaleModTool/Editors/UndertaleRoomEditor.xaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/UndertaleModTool/Editors/UndertaleRoomEditor.xaml b/UndertaleModTool/Editors/UndertaleRoomEditor.xaml index 0a75b4f82..231ab1fb5 100644 --- a/UndertaleModTool/Editors/UndertaleRoomEditor.xaml +++ b/UndertaleModTool/Editors/UndertaleRoomEditor.xaml @@ -1080,6 +1080,7 @@ + @@ -1107,6 +1108,7 @@ + Date: Wed, 25 May 2022 20:38:13 +0200 Subject: [PATCH 8/8] Fixed broken height and width assignment logic. Added the foreground grid changes to UndertaleRoomRenderer. Condensed some if statements. Reverted default grid thickness. --- UndertaleModLib/Models/UndertaleRoom.cs | 11 +---- .../Controls/UndertaleRoomRenderer.xaml | 45 +++++++++++++++---- .../Editors/UndertaleRoomEditor.xaml.cs | 18 ++------ 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/UndertaleModLib/Models/UndertaleRoom.cs b/UndertaleModLib/Models/UndertaleRoom.cs index 7bffda745..ad3019da8 100644 --- a/UndertaleModLib/Models/UndertaleRoom.cs +++ b/UndertaleModLib/Models/UndertaleRoom.cs @@ -129,7 +129,7 @@ public enum RoomEntryFlags : uint /// /// The thickness of the room grid in pixels. /// - public double GridThicknessPx { get; set; } = 0.5d; + public double GridThicknessPx { get; set; } = 1d; private UndertalePointerList _layers = new(); /// @@ -393,13 +393,9 @@ public void SetupRoom(bool calculateGridWidth = true, bool calculateGridHeight = { Point scale = new((int)tile.Width, (int)tile.Height); if (tileSizes.ContainsKey(scale)) - { tileSizes[scale]++; - } else - { tileSizes.Add(scale, 1); - } } // If tiles exist at all, grab the most used tile size and use that as our grid size @@ -408,13 +404,10 @@ public void SetupRoom(bool calculateGridWidth = true, bool calculateGridHeight = { var largestKey = tileSizes.Aggregate((x, y) => x.Value > y.Value ? x : y).Key; if (calculateGridWidth) - { GridWidth = largestKey.X; - } + if (calculateGridHeight) - { GridHeight = largestKey.Y; - } } } } diff --git a/UndertaleModTool/Controls/UndertaleRoomRenderer.xaml b/UndertaleModTool/Controls/UndertaleRoomRenderer.xaml index 9f3ca7110..0ab918ca7 100644 --- a/UndertaleModTool/Controls/UndertaleRoomRenderer.xaml +++ b/UndertaleModTool/Controls/UndertaleRoomRenderer.xaml @@ -52,7 +52,7 @@ - +