diff --git a/.github/workflows/Info.plist b/.github/workflows/Info.plist
new file mode 100644
index 000000000..8ddaa60ce
--- /dev/null
+++ b/.github/workflows/Info.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ CFBundleIdentifier
+ io.github.underminersteam.undertalemodtool
+ CFBundleName
+ UTMTA
+ CFBundleDisplayName
+ UndertaleModToolAvalonia
+ CFBundleVersion
+ 0.0.0
+ LSMinimumSystemVersion
+ 10.12
+ CFBundleExecutable
+ UndertaleModToolAvalonia
+ CFBundleInfoDictionaryVersion
+ 6.0
+ NSHighResolutionCapable
+
+
+
\ No newline at end of file
diff --git a/.github/workflows/publish_gui_avalonia.yml b/.github/workflows/publish_gui_avalonia.yml
new file mode 100644
index 000000000..1750a36a0
--- /dev/null
+++ b/.github/workflows/publish_gui_avalonia.yml
@@ -0,0 +1,76 @@
+name: Publish UndertaleModToolAvalonia
+
+on:
+ push:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build_gui:
+ defaults:
+ run:
+ shell: bash
+
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, macOS-latest, windows-latest]
+ configuration: [Release]
+ bundled: [true]
+ singlefile: [false]
+ include:
+ - os: ubuntu-latest
+ rid: linux-x64
+ - os: macOS-latest
+ rid: osx-x64
+ - os: windows-latest
+ rid: win-x64
+
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ submodules: true
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v6
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Publish ${{ matrix.os }}
+ run: |
+ dotnet publish UndertaleModToolAvalonia -c ${{ matrix.configuration }} -r ${{ matrix.rid }} --self-contained ${{ matrix.bundled }} -p:PublishSingleFile=${{ matrix.singlefile }} --output Publish/${{ matrix.os }}
+ cp ./README.md ./Publish/${{ matrix.os }}
+ cp ./SCRIPTS.md ./Publish/${{ matrix.os }}
+ cp ./LICENSE.txt ./Publish/${{ matrix.os }}
+
+ - name: ubuntu-latest specific
+ if: ${{ matrix.os == 'ubuntu-latest' }}
+ run: |
+ echo "artifact_location=Publish/ubuntu-latest/" >> "$GITHUB_ENV"
+
+ - name: windows-latest specific
+ if: ${{ matrix.os == 'windows-latest' }}
+ run: |
+ echo "artifact_location=Publish/windows-latest/" >> "$GITHUB_ENV"
+
+ - name: macOS-latest specific
+ if: ${{ matrix.os == 'macOS-latest' }}
+ run: |
+ APP_NAME="./Publish/macOS-latest-app/UndertaleModToolAvalonia.app"
+
+ mkdir -p "$APP_NAME/Contents/MacOS"
+
+ cp "./.github/workflows/Info.plist" "$APP_NAME/Contents/Info.plist"
+ cp -a "./Publish/macOS-latest/." "$APP_NAME/Contents/MacOS"
+
+ echo "artifact_location=Publish/macOS-latest-app/" >> "$GITHUB_ENV"
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v7
+ with:
+ name: UndertaleModToolAvalonia-${{ matrix.os }}-${{ github.sha }}
+ path: ${{ env.artifact_location }}
diff --git a/UndertaleModLib/Models/UndertaleAnimationCurve.cs b/UndertaleModLib/Models/UndertaleAnimationCurve.cs
index 0cc64de7f..05e1216da 100644
--- a/UndertaleModLib/Models/UndertaleAnimationCurve.cs
+++ b/UndertaleModLib/Models/UndertaleAnimationCurve.cs
@@ -38,7 +38,7 @@ public enum GraphTypeEnum : uint
///
/// The channels this animation curve has.
///
- public UndertaleSimpleList Channels { get; set; }
+ public UndertaleSimpleList Channels { get; set; } = new UndertaleSimpleList();
///
public void Serialize(UndertaleWriter writer)
@@ -158,7 +158,7 @@ public enum CurveType : uint
///
/// The points in the channel.
///
- public UndertaleSimpleList Points { get; set; }
+ public UndertaleSimpleList Points { get; set; } = new UndertaleSimpleList();
///
public void Serialize(UndertaleWriter writer)
@@ -230,37 +230,38 @@ public void Dispose()
///
/// A point which can exist on a .
///
+ [PropertyChanged.AddINotifyPropertyChangedInterface]
public class Point : UndertaleObject
{
///
/// The X coordinate of this point. GameMaker abbreviates this to "h".
///
- public float X;
+ public float X { get; set; }
///
/// The Y coordinate of this point. GameMaker abbreviates this to "v".
///
- public float Value;
+ public float Value { get; set; }
///
/// The Y position for the first bezier handle. Only used if the Channel is set to Bezier.
///
- public float BezierX0;
-
+ public float BezierX0 { get; set; }
+
///
/// The Y position for the first bezier handle. Only used if the Channel is set to Bezier.
///
- public float BezierY0;
-
+ public float BezierY0 { get; set; }
+
///
/// The X position for the second bezier handle. Only used if the Channel is set to Bezier.
///
- public float BezierX1;
-
+ public float BezierX1 { get; set; }
+
///
/// The Y position for the second bezier handle. Only used if the Channel is set to Bezier.
///
- public float BezierY1;
+ public float BezierY1 { get; set; }
///
public void Serialize(UndertaleWriter writer)
diff --git a/UndertaleModLib/Models/UndertaleGeneralInfo.cs b/UndertaleModLib/Models/UndertaleGeneralInfo.cs
index 297efc374..ba9eccf7e 100644
--- a/UndertaleModLib/Models/UndertaleGeneralInfo.cs
+++ b/UndertaleModLib/Models/UndertaleGeneralInfo.cs
@@ -11,7 +11,7 @@ namespace UndertaleModLib.Models;
/// General info about a data file.
///
[PropertyChanged.AddINotifyPropertyChangedInterface]
-public class UndertaleGeneralInfo : UndertaleObject, IDisposable
+public partial class UndertaleGeneralInfo : UndertaleObject, IDisposable
{
///
/// Information flags a data file can use.
diff --git a/UndertaleModLib/Models/UndertaleRoom.cs b/UndertaleModLib/Models/UndertaleRoom.cs
index 119ebeb50..18459f730 100644
--- a/UndertaleModLib/Models/UndertaleRoom.cs
+++ b/UndertaleModLib/Models/UndertaleRoom.cs
@@ -1114,6 +1114,26 @@ protected void OnPropertyChanged([CallerMemberName] string name = null)
///
public int YOffset => Y + SpriteYOffset;
+ public GameObject Clone()
+ {
+ return new()
+ {
+ X = this.X,
+ Y = this.Y,
+ ObjectDefinition = this.ObjectDefinition,
+ InstanceID = this.InstanceID,
+ CreationCode = this.CreationCode,
+ ScaleX = this.ScaleX,
+ ScaleY = this.ScaleY,
+ Color = this.Color,
+ Rotation = this.Rotation,
+ PreCreateCode = this.PreCreateCode,
+ ImageSpeed = this.ImageSpeed,
+ ImageIndex = this.ImageIndex,
+ Nonexistent = this.Nonexistent,
+ };
+ }
+
///
public virtual void Serialize(UndertaleWriter writer)
{
diff --git a/UndertaleModLib/Models/UndertaleSprite.cs b/UndertaleModLib/Models/UndertaleSprite.cs
index b9886bc00..6a8dea2fd 100644
--- a/UndertaleModLib/Models/UndertaleSprite.cs
+++ b/UndertaleModLib/Models/UndertaleSprite.cs
@@ -171,7 +171,7 @@ public int OriginYWrapper
public UndertaleSequence V2Sequence;
- public NineSlice V3NineSlice;
+ public NineSlice V3NineSlice { get; set; }
///
public event PropertyChangedEventHandler PropertyChanged;
diff --git a/UndertaleModLib/Models/UndertaleVariable.cs b/UndertaleModLib/Models/UndertaleVariable.cs
index 8b5a43439..315fde426 100644
--- a/UndertaleModLib/Models/UndertaleVariable.cs
+++ b/UndertaleModLib/Models/UndertaleVariable.cs
@@ -6,7 +6,7 @@ namespace UndertaleModLib.Models;
///
/// A variable entry in a GameMaker data file.
///
-// TODO: INotifyPropertyChanged
+[PropertyChanged.AddINotifyPropertyChangedInterface]
public class UndertaleVariable : UndertaleNamedResource, ISearchable, UndertaleInstruction.IReferencedObject, IDisposable, IGMVariable
{
/// The name of the Variable.
diff --git a/UndertaleModLib/UndertaleChunks.cs b/UndertaleModLib/UndertaleChunks.cs
index c6ef9d251..45da9a435 100644
--- a/UndertaleModLib/UndertaleChunks.cs
+++ b/UndertaleModLib/UndertaleChunks.cs
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics;
using System.IO;
using UndertaleModLib.Models;
using UndertaleModLib.Util;
@@ -237,7 +240,7 @@ internal override uint UnserializeObjectCount(UndertaleReader reader)
public class UndertaleChunkEXTN : UndertaleListChunk
{
public override string Name => "EXTN";
- public List productIdData = new List();
+ public ObservableCollection productIdData { get; set; } = new ObservableCollection(); // TODO: Capitalize property
private bool checkedFor2022_6 = false;
private bool checkedFor2023_4 = false;
@@ -361,7 +364,7 @@ internal override void UnserializeChunk(UndertaleReader reader)
// Strange data for each extension, some kind of unique identifier based on
// the product ID for each of them
- productIdData = new List();
+ productIdData.Clear();
if (UndertaleExtension.ProductDataEligible(reader.undertaleData))
{
for (int i = 0; i < List.Count; i++)
@@ -397,6 +400,14 @@ internal override uint UnserializeObjectCount(UndertaleReader reader)
return base.UnserializeObjectCount(reader);
}
+
+ public class ByteArrayWrapper(byte[] v)
+ {
+ public byte[] ByteArray { get; set; } = v;
+
+ public static implicit operator byte[](ByteArrayWrapper v) => v.ByteArray;
+ public static implicit operator ByteArrayWrapper(byte[] v) => new(v);
+ }
}
public class UndertaleChunkSOND : UndertaleListChunk
@@ -1746,7 +1757,7 @@ public class UndertaleChunkVARI : UndertaleChunk
public uint VarCount2 { get; set; }
public uint MaxLocalVarCount { get; set; }
public bool DifferentVarCounts { get; set; }
- public List List = new List();
+ public ObservableCollection List = new ObservableCollection();
[Obsolete]
public uint InstanceVarCount { get => VarCount1; set => VarCount1 = value; }
@@ -1796,7 +1807,7 @@ internal override void UnserializeChunk(UndertaleReader reader)
else
varLength = 12;
List.Clear();
- List.Capacity = (int)(Length / varLength);
+ //List.Capacity = (int)(Length / varLength);
while (reader.Position + varLength <= startPosition + Length)
List.Add(reader.ReadUndertaleObject());
}
diff --git a/UndertaleModLib/UndertaleData.cs b/UndertaleModLib/UndertaleData.cs
index e05791052..32d04d882 100644
--- a/UndertaleModLib/UndertaleData.cs
+++ b/UndertaleModLib/UndertaleData.cs
@@ -719,6 +719,147 @@ public static UndertaleData CreateNew()
return data;
}
+ ///
+ /// Creates a new resource based on type of the list.
+ ///
+ public static UndertaleResource CreateResource(IList list)
+ {
+ Type resourceType = list.GetType().GetGenericArguments()[0];
+ return (Activator.CreateInstance(resourceType) as UndertaleResource)!;
+ }
+
+ ///
+ /// Get the default name of a resource based on a list (e.g., sprite0)
+ ///
+ public static string GetDefaultResourceName(IList list)
+ {
+ Type resourceType = list.GetType().GetGenericArguments()[0];
+ if (resourceType == typeof(UndertaleTexturePageItem) ||
+ resourceType == typeof(UndertaleEmbeddedAudio) ||
+ resourceType == typeof(UndertaleEmbeddedTexture))
+ {
+ return null;
+ }
+
+ string typeName = resourceType.Name.Replace("Undertale", "").Replace("GameObject", "Object").ToLower();
+ string resourceName = typeName + list.Count;
+
+ return resourceName;
+ }
+
+ ///
+ /// Initialize newly created resource.
+ ///
+ /// The resource to initialize.
+ /// The list where the resource might reside.
+ /// Name to set to resource if supported.
+ public IList InitializeResource(UndertaleResource resource, IList list, string resourceName)
+ {
+ IList newResources = [];
+
+ // Set up name
+ if (resource is UndertaleNamedResource namedResource)
+ {
+ UndertaleString name = resource switch
+ {
+ // UTMT only names.
+ UndertaleTexturePageItem => new UndertaleString("PageItem " + list.Count),
+ UndertaleEmbeddedAudio => new UndertaleString("EmbeddedSound " + list.Count),
+ UndertaleEmbeddedTexture => new UndertaleString("Texture " + list.Count),
+ _ => Strings.MakeString(resourceName),
+ };
+
+ namedResource.Name = name;
+ }
+
+ if (resource is UndertaleString _string)
+ {
+ _string.Content = resourceName;
+ }
+ else if (resource is UndertaleRoom room)
+ {
+ if (IsVersionAtLeast(2))
+ {
+ room.Caption = null;
+ room.Backgrounds.Clear();
+ if (IsVersionAtLeast(2024, 13))
+ {
+ room.Flags |= IsVersionAtLeast(2024, 13) ? UndertaleRoom.RoomEntryFlags.IsGM2024_13 : UndertaleRoom.RoomEntryFlags.IsGMS2;
+ room.InstanceCreationOrderIDs ??= new();
+ }
+ else
+ {
+ room.Flags |= UndertaleRoom.RoomEntryFlags.IsGMS2;
+ if (IsVersionAtLeast(2, 3))
+ {
+ room.Flags |= UndertaleRoom.RoomEntryFlags.IsGMS2_3;
+ }
+ }
+ }
+ else
+ {
+ room.Caption = Strings.MakeString("");
+ }
+ }
+ else if (resource is UndertaleScript script)
+ {
+ if (IsVersionAtLeast(2, 3))
+ {
+ script.Code = UndertaleCode.CreateEmptyEntry(this, Strings.MakeString($"gml_GlobalScript_{script.Name.Content}"));
+ if (GlobalInitScripts is IList globalInitScripts)
+ {
+ globalInitScripts.Add(new UndertaleGlobalInit()
+ {
+ Code = script.Code,
+ });
+ }
+ }
+ else
+ {
+ script.Code = UndertaleCode.CreateEmptyEntry(this, Strings.MakeString($"gml_Script_{script.Name.Content}"));
+ }
+
+ newResources.Add(script.Code);
+ }
+ else if (resource is UndertaleCode code)
+ {
+ if (CodeLocals is not null)
+ {
+ code.LocalsCount = 1;
+ UndertaleCodeLocals.CreateEmptyEntry(this, code.Name);
+ }
+ else
+ {
+ code.WeirdLocalFlag = true;
+ }
+ }
+ else if (resource is UndertaleExtension extension)
+ {
+ extension.FolderName = Strings.MakeString("");
+ extension.ClassName = Strings.MakeString("");
+ extension.Version = Strings.MakeString("1.0.0");
+
+ if (GeneralInfo?.Major >= 2 ||
+ (GeneralInfo?.Major == 1 && GeneralInfo?.Build >= 1773) ||
+ (GeneralInfo?.Major == 1 && GeneralInfo?.Build == 1539))
+ {
+ var newProductID = new byte[] { 0xBA, 0x5E, 0xBA, 0x11, 0xBA, 0xDD, 0x06, 0x60, 0xBE, 0xEF, 0xED, 0xBA, 0x0B, 0xAB, 0xBA, 0xBE };
+ FORM.EXTN.productIdData.Add(newProductID);
+ }
+ }
+ else if (resource is UndertaleShader shader)
+ {
+ shader.GLSL_ES_Vertex = Strings.MakeString("");
+ shader.GLSL_ES_Fragment = Strings.MakeString("");
+ shader.GLSL_Vertex = Strings.MakeString("");
+ shader.GLSL_Fragment = Strings.MakeString("");
+ shader.HLSL9_Vertex = Strings.MakeString("");
+ shader.HLSL9_Fragment = Strings.MakeString("");
+ }
+
+ return newResources;
+ }
+
///
public void Dispose()
{
diff --git a/UndertaleModLib/UndertaleIO.cs b/UndertaleModLib/UndertaleIO.cs
index d48f6616f..dc6075d36 100644
--- a/UndertaleModLib/UndertaleIO.cs
+++ b/UndertaleModLib/UndertaleIO.cs
@@ -1,6 +1,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
@@ -23,6 +24,7 @@ public interface UndertaleResourceRef : UndertaleObject
int SerializeById(UndertaleWriter writer);
}
+ [PropertyChanged.AddINotifyPropertyChangedInterface]
public class UndertaleResourceById : UndertaleResourceRef, IStaticChildObjectsSize, IDisposable where T : UndertaleResource, new() where ChunkT : UndertaleListChunk
{
///
diff --git a/UndertaleModLib/UndertaleModLib.csproj b/UndertaleModLib/UndertaleModLib.csproj
index 4db33e9c8..b6aa5b7fb 100644
--- a/UndertaleModLib/UndertaleModLib.csproj
+++ b/UndertaleModLib/UndertaleModLib.csproj
@@ -10,8 +10,6 @@
0.9.2.0
0.9.2.0
embedded
- AnyCPU;x64
- win-x64;win-x86
en
en
en
diff --git a/UndertaleModLib/Util/GMImage.cs b/UndertaleModLib/Util/GMImage.cs
index fb06b6703..e7e9ad6c1 100644
--- a/UndertaleModLib/Util/GMImage.cs
+++ b/UndertaleModLib/Util/GMImage.cs
@@ -968,6 +968,14 @@ public Span GetRawImageData()
return _data.AsSpan();
}
+ ///
+ /// Returns the image data in the current format.
+ ///
+ public byte[] GetData()
+ {
+ return _data;
+ }
+
///
/// Writes this image, in its current format (as seen on disk), to the current position of the specified .
///
diff --git a/UndertaleModLib/Util/TextureWorker.cs b/UndertaleModLib/Util/TextureWorker.cs
index e727bcdc5..b7ece5e38 100644
--- a/UndertaleModLib/Util/TextureWorker.cs
+++ b/UndertaleModLib/Util/TextureWorker.cs
@@ -14,6 +14,11 @@ public class TextureWorker : IDisposable
private Dictionary embeddedDictionary = new();
private readonly object embeddedDictionaryLock = new();
+ private static MagickReadSettings magickReadSettings = new()
+ {
+ ColorSpace = ColorSpace.sRGB,
+ };
+
///
/// Retrieves an image representing the supplied texture page.
///
@@ -53,6 +58,19 @@ public void ExportAsPNG(UndertaleTexturePageItem texPageItem, string filePath, s
SaveImageToFile(image, filePath);
}
+ ///
+ /// Exports the given texture page item, as a PNG, to the supplied stream. (With or without padding.)
+ ///
+ /// Texture page item to export.
+ /// Stream to export to.
+ /// Image name to be used when throwing exceptions.
+ /// True if padding should be exported; false otherwise.
+ public void ExportAsPNG(UndertaleTexturePageItem texPageItem, Stream stream, string imageName = null, bool includePadding = false)
+ {
+ using var image = GetTextureFor(texPageItem, imageName ?? "Image", includePadding);
+ SaveImageToStream(image, stream);
+ }
+
///
/// Creates an image representing the sole texture page item supplied, with or without padding.
///
@@ -115,11 +133,16 @@ public IMagickImage GetTextureFor(UndertaleTexturePageItem texPageItem, st
/// An image, in uncompressed BGRA format, containing the contents of the image file at the given path.
public static MagickImage ReadBGRAImageFromFile(string filePath)
{
- MagickReadSettings settings = new()
- {
- ColorSpace = ColorSpace.sRGB,
- };
- MagickImage image = new(filePath, settings);
+ return ConfigureReadBGRAImage(new(filePath, magickReadSettings));
+ }
+
+ public static MagickImage ReadBGRAImageFromStream(Stream stream)
+ {
+ return ConfigureReadBGRAImage(new(stream, magickReadSettings));
+ }
+
+ private static MagickImage ConfigureReadBGRAImage(MagickImage image)
+ {
image.Alpha(AlphaOption.Set);
image.Format = MagickFormat.Bgra;
image.Depth = 8;
@@ -259,6 +282,16 @@ public static void ExportCollisionMaskPNG(UndertaleSprite.MaskEntry mask, string
public static void SaveImageToFile(IMagickImage image, string filePath)
{
using var stream = new FileStream(filePath, FileMode.Create);
+ SaveImageToStream(image, stream);
+ }
+
+ ///
+ /// Saves the provided image as a PNG file, at the specified stream.
+ ///
+ /// Image to save.
+ /// Stream to save the image to.
+ public static void SaveImageToStream(IMagickImage image, Stream stream)
+ {
image.Write(stream, MagickFormat.Png32);
}
diff --git a/UndertaleModTool.sln b/UndertaleModTool.sln
index 18e2bfe07..0f573cd0d 100644
--- a/UndertaleModTool.sln
+++ b/UndertaleModTool.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0.32014.148
+# Visual Studio Version 18
+VisualStudioVersion = 18.3.11505.172
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UndertaleModTool", "UndertaleModTool\UndertaleModTool.csproj", "{C703A485-DBFB-4601-8A26-42E64CCF812F}"
EndProject
@@ -17,6 +17,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UndertaleModLibTests", "Und
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Underanalyzer", "Underanalyzer\Underanalyzer\Underanalyzer.csproj", "{A171D18F-274E-427A-AFB3-D7A161F5B9CF}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UndertaleModToolAvalonia", "UndertaleModToolAvalonia\UndertaleModToolAvalonia.csproj", "{96FA167A-9D79-3328-BCB4-0564D2575718}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -81,6 +83,14 @@ Global
{A171D18F-274E-427A-AFB3-D7A161F5B9CF}.Release|Any CPU.Build.0 = Release|Any CPU
{A171D18F-274E-427A-AFB3-D7A161F5B9CF}.Release|x64.ActiveCfg = Release|Any CPU
{A171D18F-274E-427A-AFB3-D7A161F5B9CF}.Release|x64.Build.0 = Release|Any CPU
+ {96FA167A-9D79-3328-BCB4-0564D2575718}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {96FA167A-9D79-3328-BCB4-0564D2575718}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {96FA167A-9D79-3328-BCB4-0564D2575718}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {96FA167A-9D79-3328-BCB4-0564D2575718}.Debug|x64.Build.0 = Debug|Any CPU
+ {96FA167A-9D79-3328-BCB4-0564D2575718}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {96FA167A-9D79-3328-BCB4-0564D2575718}.Release|Any CPU.Build.0 = Release|Any CPU
+ {96FA167A-9D79-3328-BCB4-0564D2575718}.Release|x64.ActiveCfg = Release|Any CPU
+ {96FA167A-9D79-3328-BCB4-0564D2575718}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/UndertaleModTool/Editors/UndertaleBackgroundEditor.xaml.cs b/UndertaleModTool/Editors/UndertaleBackgroundEditor.xaml.cs
index ac07d94f1..eb2f400a9 100644
--- a/UndertaleModTool/Editors/UndertaleBackgroundEditor.xaml.cs
+++ b/UndertaleModTool/Editors/UndertaleBackgroundEditor.xaml.cs
@@ -178,7 +178,9 @@ private bool SelectTileRegion(object sender, MouseButtonEventArgs e)
e.Handled = true;
- int tileIndex = bg.GMS2TileIds.FindIndex(x => x.ID == tileID);
+ var tileIndex = bg.GMS2TileIds
+ .Select((item, index) => new { Item = item, Index = index })
+ .FirstOrDefault(x => x.Item.ID == tileID)?.Index ?? -1;
if (tileIndex == -1)
return false;
diff --git a/UndertaleModTool/Editors/UndertaleRoomEditor/UndertaleTileEditor.xaml.cs b/UndertaleModTool/Editors/UndertaleRoomEditor/UndertaleTileEditor.xaml.cs
index 0dbce7edd..19ba3d03d 100644
--- a/UndertaleModTool/Editors/UndertaleRoomEditor/UndertaleTileEditor.xaml.cs
+++ b/UndertaleModTool/Editors/UndertaleRoomEditor/UndertaleTileEditor.xaml.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
+using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
@@ -560,9 +561,9 @@ private void FindPaletteCursor()
PaletteCursorVisibility = Visibility.Visible;
uint brushTile = BrushTilesData.TileData[0][0] & TILE_INDEX;
- int index = PaletteTilesData.Background.GMS2TileIds.FindIndex(
- id => id.ID == brushTile
- );
+ int index = PaletteTilesData.Background.GMS2TileIds
+ .Select((item, index) => new { Item = item, Index = index })
+ .FirstOrDefault(id => id.Item.ID == brushTile)?.Index ?? -1;
if (index == -1)
index = 0;
MovePaletteCursor((int)(index / PaletteTilesData.Background.GMS2ItemsPerTileCount));
diff --git a/UndertaleModToolAvalonia/App.axaml b/UndertaleModToolAvalonia/App.axaml
new file mode 100644
index 000000000..9e5026456
--- /dev/null
+++ b/UndertaleModToolAvalonia/App.axaml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
diff --git a/UndertaleModToolAvalonia/App.axaml.cs b/UndertaleModToolAvalonia/App.axaml.cs
new file mode 100644
index 000000000..352e0e311
--- /dev/null
+++ b/UndertaleModToolAvalonia/App.axaml.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Reflection;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using Avalonia.Styling;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace UndertaleModToolAvalonia;
+
+public partial class App : Application
+{
+ public static string VersionString = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "?.?.?.?";
+ public static string InformationalVersionString = Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion ?? "";
+
+ public static IServiceProvider Services = null!;
+ public static IStyle? CurrentCustomStyles = null;
+
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ // Dependency injection.
+ ServiceCollection collection = new();
+ collection.AddSingleton();
+
+ Services = collection.BuildServiceProvider();
+
+ MainViewModel vm = Services.GetRequiredService();
+ vm.Initialize();
+
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ desktop.MainWindow = new MainWindow
+ {
+ DataContext = vm,
+ };
+
+ Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (source, e) =>
+ {
+ if (source is Window window)
+ {
+ bool isDark = (ActualThemeVariant == ThemeVariant.Dark);
+ window.SetDarkTitleBar(isDark);
+ }
+ });
+
+ ActualThemeVariantChanged += (s, e) =>
+ {
+ bool isDark = (ActualThemeVariant == ThemeVariant.Dark);
+ foreach (var window in desktop.Windows)
+ {
+ window.SetDarkTitleBar(isDark);
+ }
+ };
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+}
diff --git a/UndertaleModToolAvalonia/Assets/Icon.ico b/UndertaleModToolAvalonia/Assets/Icon.ico
new file mode 100644
index 000000000..16e06e564
Binary files /dev/null and b/UndertaleModToolAvalonia/Assets/Icon.ico differ
diff --git a/UndertaleModToolAvalonia/Assets/SyntaxASM.xshd b/UndertaleModToolAvalonia/Assets/SyntaxASM.xshd
new file mode 100644
index 000000000..809893d42
--- /dev/null
+++ b/UndertaleModToolAvalonia/Assets/SyntaxASM.xshd
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ^;.*|(?:^|\n)\..*
+
+
+
+ ^(?:\:|\>).*|(?:^|\n)\..*
+
+
+
+ \"(?:\\.|[^"\\])*\"@\d+
+
+
+
+ \[[0-9a-zA-Z_\-]+\]
+
+
+
+
+ self
+ other
+ all
+ noone
+ global
+ undefined
+ local
+ builtin
+ arg
+
+
+
+
+ (?:^|\n)\d+(?=:)
+
+
+
+
+ \[(?:array|stacktop)\]
+ |
+ argc\=\d+
+ |
+ <drop>
+
+
+
+
+ \b0x[0-9a-fA-F]+ # hex number
+ | \$[0-9a-fA-F]+ # alternate hex number
+ |
+ (?: -?\d+(?:\.[0-9]+)? # digits with optional . and -
+ | -?\.[0-9]+ # start with ., then digits
+ )
+ \b
+
+
+
+
+ \bargument[0-9]\b |
+ \bargument1[0-5]\b
+
+
+
+
+ [_a-zA-Z@][_a-zA-Z0-9@]*(?=\()
+
+
+
+
+ @+[_a-zA-Z][_a-zA-Z0-9]*@+(?=\()
+
+
+
+
+ ^(bf|bt|b|exit|ret|pushenv|popenv)
+
+
+ ^[a-z]+(?=\.|\s)
+
+
+
+
+ [_a-zA-Z][_a-zA-Z0-9]*
+
+
+
\ No newline at end of file
diff --git a/UndertaleModToolAvalonia/Assets/SyntaxGML.xshd b/UndertaleModToolAvalonia/Assets/SyntaxGML.xshd
new file mode 100644
index 000000000..b29094dfe
--- /dev/null
+++ b/UndertaleModToolAvalonia/Assets/SyntaxGML.xshd
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ "
+ "
+
+
+
+
+
+
+
+ '
+ '
+
+
+
+
+
+
+
+ \$"
+ "
+
+
+
+
+
+
+
+
+
+
+ if
+ else
+ do
+ while
+ for
+ repeat
+ switch
+ case
+ default
+ break
+ continue
+ with
+ new
+ constructor
+ function
+ return
+ exit
+ var
+ until
+ and
+ or
+ xor
+ begin
+ end
+ then
+ mod
+ div
+ throw
+ static
+ try
+ catch
+ finally
+ enum
+
+
+
+
+ true
+ false
+ self
+ other
+ all
+ noone
+ global
+ undefined
+
+
+
+
+ \b0x[0-9a-fA-F]+ # hex number
+ | \$[0-9a-fA-F]+ # alternate hex number
+ | \#[0-9a-fA-F]{6} # css color hex number
+ |
+ (?: -?\d+(?:\.[0-9]+)? # digits with optional . and -
+ | -?\.[0-9]+ # start with ., then digits
+ )
+ \b
+
+
+
+
+ \bargument[0-9]\b |
+ \bargument1[0-5]\b
+
+
+
+
+ [_a-zA-Z][_a-zA-Z0-9]*(?=\()
+
+
+
+
+ @+[_a-zA-Z][_a-zA-Z0-9]*@+(?=\()
+
+
+
+
+ [_a-zA-Z][_a-zA-Z0-9]*
+
+
+
\ No newline at end of file
diff --git a/UndertaleModToolAvalonia/Controls/DataExplorerView.axaml b/UndertaleModToolAvalonia/Controls/DataExplorerView.axaml
new file mode 100644
index 000000000..4c1ff2824
--- /dev/null
+++ b/UndertaleModToolAvalonia/Controls/DataExplorerView.axaml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UndertaleModToolAvalonia/Controls/DataExplorerView.axaml.cs b/UndertaleModToolAvalonia/Controls/DataExplorerView.axaml.cs
new file mode 100644
index 000000000..eb8102f17
--- /dev/null
+++ b/UndertaleModToolAvalonia/Controls/DataExplorerView.axaml.cs
@@ -0,0 +1,344 @@
+using System.Collections;
+using System.Collections.Generic;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Models.TreeDataGrid;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+using Avalonia.Input;
+using Avalonia.Input.Platform;
+using Avalonia.Interactivity;
+using Avalonia.LogicalTree;
+using Avalonia.VisualTree;
+using UndertaleModLib;
+using UndertaleModLib.Models;
+
+namespace UndertaleModToolAvalonia;
+
+public partial class DataExplorerView : UserControl
+{
+ HierarchicalTreeDataGridSource? treeDataGridSource;
+
+ public DataExplorerView()
+ {
+ InitializeComponent();
+
+ MainTreeDataGrid.AddHandler(TreeDataGrid.PointerReleasedEvent, MainTreeDataGrid_PointerReleased_HandledEventsToo, handledEventsToo: true);
+ }
+
+ protected override void OnInitialized()
+ {
+ if (DataContext is not DataExplorerViewModel vm)
+ return;
+
+ vm.OnExpandItemOnTree = ExpandItemOnTree;
+ vm.OnSelectValueInTree = SelectValueInTree;
+
+ treeDataGridSource = new HierarchicalTreeDataGridSource(vm.TreeDataGridData)
+ {
+ Columns = {
+ new HierarchicalExpanderColumn(
+ new TemplateColumn(null,
+ new FuncDataTemplate((value, namescope) =>
+ {
+ if (value is null)
+ return null;
+
+ TextBlock textBlock = new() { Text = value.Text };
+
+ if (value.Value is UndertaleNamedResource namedResource)
+ {
+ textBlock[!TextBlock.TextProperty] = CompiledBinding.Create(x =>
+ ((UndertaleNamedResource)x.Value!).Name.Content);
+
+ if (namedResource is UndertaleCode code)
+ {
+ textBlock.BindClass("DisableForeground", CompiledBinding.Create(x =>
+ ((UndertaleCode)x.Value!).ParentEntry, converter: ObjectConverters.IsNotNull), null!);
+ }
+ }
+ else if (value.Value is UndertaleString _string)
+ {
+ textBlock[!TextBlock.TextProperty] = CompiledBinding.Create(x =>
+ ((UndertaleString)x.Value!).Content);
+ }
+ else if (value.Value is null)
+ {
+ textBlock.Text = "(null)";
+ textBlock.Classes.Add("DisableForeground");
+ }
+
+ return textBlock;
+ }), width: GridLength.Star
+ ),
+ childSelector: x => x.Children)
+ }
+ };
+
+ treeDataGridSource.RowSelection!.SingleSelect = false;
+
+ MainTreeDataGrid.Source = treeDataGridSource;
+
+ base.OnInitialized();
+ }
+
+ #region Events
+ private void MainTreeDataGrid_DoubleTapped(object? sender, TappedEventArgs e)
+ {
+ OpenItemFromTreeDataGridControl(e.Source);
+ }
+
+ private void MainTreeDataGrid_KeyDown(object? sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Enter)
+ {
+ OpenItemFromTreeDataGridControl(e.Source);
+ }
+ }
+
+ private void MainTreeDataGrid_PointerReleased_HandledEventsToo(object? sender, PointerReleasedEventArgs e)
+ {
+ if (e.InitialPressMouseButton == MouseButton.Middle
+ && ((e.Source as Visual)?.GetTransformedBounds()?.Contains(e.GetPosition(null)) ?? false))
+ {
+ OpenItemFromTreeDataGridControl(e.Source, inNewTab: true);
+ }
+ }
+
+ private void OpenItemFromTreeDataGridControl(object? source, bool inNewTab = false)
+ {
+ if (DataContext is not DataExplorerViewModel vm)
+ return;
+
+ if (source is Control control)
+ {
+ TreeDataGridRow? row = control.FindLogicalAncestorOfType(includeSelf: true);
+ if (row?.DataContext is DataExplorerViewModel.Item item)
+ {
+ if (row.Rows?[row.RowIndex] is HierarchicalRow hierarchicalRow)
+ {
+ hierarchicalRow.IsExpanded = !hierarchicalRow.IsExpanded;
+ }
+ _ = vm.MainVM.TabOpen(item.Value, inNewTab);
+ }
+ }
+ }
+ #endregion
+
+ #region Context menus
+
+ public void ContextMenu_Add_Click(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is not DataExplorerViewModel vm)
+ return;
+
+ DataExplorerViewModel.Item? item = GetItemFromTreeDataGridControl(e.Source);
+ if (item is not null && vm.MainVM.Data is not null)
+ {
+ // This could probably be better
+ IList list = (item.Value switch
+ {
+ "AudioGroups" => vm.MainVM.Data.AudioGroups as IList,
+ "Sounds" => vm.MainVM.Data.Sounds as IList,
+ "Sprites" => vm.MainVM.Data.Sprites as IList,
+ "Backgrounds" => vm.MainVM.Data.Backgrounds as IList,
+ "Paths" => vm.MainVM.Data.Paths as IList,
+ "Scripts" => vm.MainVM.Data.Scripts as IList,
+ "Shaders" => vm.MainVM.Data.Shaders as IList,
+ "Fonts" => vm.MainVM.Data.Fonts as IList,
+ "Timelines" => vm.MainVM.Data.Timelines as IList,
+ "GameObjects" => vm.MainVM.Data.GameObjects as IList,
+ "Rooms" => vm.MainVM.Data.Rooms as IList,
+ "Extensions" => vm.MainVM.Data.Extensions as IList,
+ "TexturePageItems" => vm.MainVM.Data.TexturePageItems as IList,
+ "Code" => vm.MainVM.Data.Code as IList,
+ "Variables" => vm.MainVM.Data.Variables as IList,
+ "Functions" => vm.MainVM.Data.Functions as IList,
+ "CodeLocals" => vm.MainVM.Data.CodeLocals as IList,
+ "Strings" => vm.MainVM.Data.Strings as IList,
+ "EmbeddedTextures" => vm.MainVM.Data.EmbeddedTextures as IList,
+ "EmbeddedAudio" => vm.MainVM.Data.EmbeddedAudio as IList,
+ "TextureGroupInformation" => vm.MainVM.Data.TextureGroupInfo as IList,
+ "EmbeddedImages" => vm.MainVM.Data.EmbeddedImages as IList,
+ "AnimationCurves" => vm.MainVM.Data.AnimationCurves as IList,
+ "ParticleSystems" => vm.MainVM.Data.ParticleSystems as IList,
+ "ParticleSystemEmitters" => vm.MainVM.Data.ParticleSystemEmitters as IList,
+ _ => null,
+ })!;
+
+ vm.MainVM.DataItemAdd(list);
+ }
+ }
+
+ public void ContextMenu_Open_Click(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is not DataExplorerViewModel vm || vm.MainVM.Data is null)
+ return;
+
+ DataExplorerViewModel.Item? item = GetItemFromTreeDataGridControl(e.Source);
+ _ = vm.MainVM.TabOpen(item?.Value);
+ }
+
+ public void ContextMenu_OpenInNewTab_Click(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is not DataExplorerViewModel vm || vm.MainVM.Data is null || treeDataGridSource is null)
+ return;
+
+ foreach (var item in treeDataGridSource.RowSelection!.SelectedItems)
+ {
+ _ = vm.MainVM.TabOpen(item?.Value, inNewTab: true);
+ }
+ }
+
+ public async void ContextMenu_CopyName_Click(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is not DataExplorerViewModel vm || vm.MainVM.Data is null || treeDataGridSource is null)
+ return;
+
+ List names = new();
+
+ foreach (var item in treeDataGridSource.RowSelection!.SelectedItems)
+ {
+ string name = item?.Value switch
+ {
+ UndertaleNamedResource namedResource => namedResource.Name?.Content,
+ UndertaleString _string => _string.Content,
+ _ => null,
+ } ?? "";
+
+ names.Add(name);
+ }
+
+ TopLevel topLevel = TopLevel.GetTopLevel(this)!;
+ await topLevel.Clipboard!.SetTextAsync(string.Join("\n", names));
+ }
+
+ public async void ContextMenu_FindReferences_Click(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is not DataExplorerViewModel vm)
+ return;
+
+ DataExplorerViewModel.Item? item = GetItemFromTreeDataGridControl(e.Source);
+ if (item is not null && item.Value is UndertaleResource resource && vm.MainVM.Data is not null)
+ {
+ vm.MainVM.OpenFindReferences(resource);
+ }
+ }
+
+ public async void ContextMenu_Move_Click(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is not DataExplorerViewModel vm)
+ return;
+
+ DataExplorerViewModel.Item? item = GetItemFromTreeDataGridControl(e.Source);
+ if (item is not null && vm.MainVM.Data is not null && vm.MainVM.View is not null)
+ {
+ UndertaleResource resource = (item.Value as UndertaleResource)!;
+ IList list = vm.MainVM.Data[resource.GetType()];
+ int oldIndex = list.IndexOf(resource);
+
+ string? input = await vm.MainVM.View.TextBoxDialog("Swap to position:", oldIndex.ToString());
+ if (input is null)
+ return;
+
+ if (!int.TryParse(input, out int newIndex))
+ {
+ await vm.MainVM.View.MessageDialog($"\"{input}\" is not a integer");
+ return;
+ }
+ if (newIndex < 0 || newIndex >= list.Count)
+ {
+ await vm.MainVM.View.MessageDialog($"{newIndex} is out of range of the list");
+ return;
+ }
+
+ object? temp = list[newIndex];
+ list[newIndex] = list[oldIndex];
+ list[oldIndex] = temp;
+ }
+ }
+
+ public async void ContextMenu_Remove_Click(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is not DataExplorerViewModel vm)
+ return;
+
+ DataExplorerViewModel.Item? item = GetItemFromTreeDataGridControl(e.Source);
+ if (item is not null && vm.MainVM.Data is not null)
+ {
+ UndertaleResource resource = (item.Value as UndertaleResource)!;
+ vm.MainVM.DataItemRemove(resource);
+ }
+ }
+
+ static DataExplorerViewModel.Item? GetItemFromTreeDataGridControl(object? source)
+ {
+ if (source is Control control)
+ {
+ TreeDataGridRow? row = control.FindLogicalAncestorOfType(includeSelf: true);
+ if (row?.DataContext is DataExplorerViewModel.Item item)
+ {
+ return item;
+ }
+ }
+
+ return null;
+ }
+ #endregion
+
+ public void ExpandItemOnTree(DataExplorerViewModel.Item item)
+ {
+ if (DataContext is not DataExplorerViewModel vm)
+ return;
+
+ IndexPath? foundIndex = FindTreeIndexPathFromValue(item, vm.TreeDataGridData);
+
+ if (foundIndex is IndexPath index)
+ {
+ var source = (MainTreeDataGrid.Source as HierarchicalTreeDataGridSource)!;
+ source.Expand(index);
+ }
+ }
+
+ public void SelectValueInTree(object value)
+ {
+ if (DataContext is not DataExplorerViewModel vm)
+ return;
+
+ IndexPath? foundIndex = FindTreeIndexPathFromValue(value, vm.TreeDataGridData);
+
+ if (foundIndex is IndexPath index)
+ {
+ var source = (MainTreeDataGrid.Source as HierarchicalTreeDataGridSource)!;
+ source.Expand(index);
+
+ int rowIndex = MainTreeDataGrid.Rows!.ModelIndexToRowIndex(index);
+ MainTreeDataGrid.RowsPresenter!.BringIntoView(rowIndex);
+
+ source.RowSelection!.Select(index);
+ }
+ }
+
+ static IndexPath? FindTreeIndexPathFromValue(object value, IList? list, IndexPath indexPath = new())
+ {
+ if (list is null)
+ return null;
+
+ for (int i = 0; i < list.Count; i++)
+ {
+ DataExplorerViewModel.Item? item = list[i];
+ if (item.Value == value || item == value)
+ {
+ return indexPath.Append(i);
+ }
+
+ IndexPath? result = FindTreeIndexPathFromValue(value, item.Children, indexPath.Append(i));
+ if (result is not null)
+ return result;
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/UndertaleModToolAvalonia/Controls/DataExplorerViewModel.cs b/UndertaleModToolAvalonia/Controls/DataExplorerViewModel.cs
new file mode 100644
index 000000000..64c5722be
--- /dev/null
+++ b/UndertaleModToolAvalonia/Controls/DataExplorerViewModel.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using UndertaleModLib;
+using UndertaleModLib.Models;
+
+namespace UndertaleModToolAvalonia;
+
+public partial class DataExplorerViewModel : ObservableObject
+{
+ public MainViewModel MainVM;
+
+ [ObservableProperty]
+ public partial ObservableCollection- TreeDataGridData { get; set; } = [];
+
+ public delegate void OnExpandItemOnTreeDelegate(DataExplorerViewModel.Item item);
+ public OnExpandItemOnTreeDelegate? OnExpandItemOnTree;
+
+ public delegate void OnSelectValueInTreeDelegate(object value);
+ public OnSelectValueInTreeDelegate? OnSelectValueInTree;
+
+ readonly List observableCollectionViewList = [];
+
+ public DataExplorerViewModel(MainViewModel mainVM)
+ {
+ MainVM = mainVM;
+ }
+
+ public void UpdateFromData()
+ {
+ TreeDataGridData.Clear();
+
+ observableCollectionViewList.Clear();
+
+ if (MainVM.Data is null)
+ return;
+
+ Item dataItem = new()
+ {
+ Value = MainVM.Data,
+ Text = "Data",
+ Children = [],
+ };
+
+ void AddItem(object? item, string value, string text)
+ {
+ if (item is not null)
+ dataItem.Children.Add(new() { Value = value, Text = text });
+ }
+
+ void AddList(IList? list, string value, string text) where T : class?
+ {
+ if (list is not null)
+ dataItem.Children.Add(new() { Tag = "list", Value = value, Text = text, Children = CreateListObservableCollectionView(list) });
+ }
+
+ AddItem(MainVM.Data.GeneralInfo, "GeneralInfo", "General info");
+ AddItem(MainVM.Data.GlobalInitScripts, "GlobalInitScripts", "Global init scripts");
+ AddItem(MainVM.Data.GameEndScripts, "GameEndScripts", "Game End scripts");
+
+ AddList(MainVM.Data.AudioGroups, "AudioGroups", "Audio groups");
+ AddList(MainVM.Data.Sounds, "Sounds", "Sounds");
+ AddList(MainVM.Data.Sprites, "Sprites", "Sprites");
+ AddList(MainVM.Data.Backgrounds, "Backgrounds", "Backgrounds & Tile sets");
+ AddList(MainVM.Data.Paths, "Paths", "Paths");
+ AddList(MainVM.Data.Scripts, "Scripts", "Scripts");
+ AddList(MainVM.Data.Shaders, "Shaders", "Shaders");
+ AddList(MainVM.Data.Fonts, "Fonts", "Fonts");
+ AddList(MainVM.Data.Timelines, "Timelines", "Timelines");
+ AddList(MainVM.Data.GameObjects, "GameObjects", "Game objects");
+ AddList(MainVM.Data.Rooms, "Rooms", "Rooms");
+ AddList(MainVM.Data.Extensions, "Extensions", "Extensions");
+ AddList(MainVM.Data.TexturePageItems, "TexturePageItems", "Texture page items");
+ AddList(MainVM.Data.Code, "Code", "Code");
+ AddList(MainVM.Data.Variables, "Variables", "Variables");
+ AddList(MainVM.Data.Functions, "Functions", "Functions");
+ AddList(MainVM.Data.CodeLocals, "CodeLocals", "Code locals");
+ AddList(MainVM.Data.Strings, "Strings", "Strings");
+ AddList(MainVM.Data.EmbeddedTextures, "EmbeddedTextures", "Embedded textures");
+ AddList(MainVM.Data.EmbeddedAudio, "EmbeddedAudio", "Embedded audio");
+ AddList(MainVM.Data.TextureGroupInfo, "TextureGroupInformation", "Texture group information");
+ AddList(MainVM.Data.EmbeddedImages, "EmbeddedImages", "Embedded images");
+ AddList(MainVM.Data.AnimationCurves, "AnimationCurves", "Animation curves");
+ AddList(MainVM.Data.ParticleSystems, "ParticleSystems", "Particle systems");
+ AddList(MainVM.Data.ParticleSystemEmitters, "ParticleSystemEmitters", "Particle system emitters");
+
+ TreeDataGridData.Add(dataItem);
+ }
+
+ ObservableCollectionView.CustomObservableCollection
- ? CreateListObservableCollectionView(IList? list) where T : class?
+ {
+ if (list is not null)
+ {
+ ObservableCollectionView view = new(list,
+ transform: x => new Item() { Text = "", Value = x },
+ filter: item => AssetNameContainsText(item.Value, MainVM.FilterText));
+
+ observableCollectionViewList.Add(view);
+
+ return view.Output;
+ }
+ return null;
+ }
+
+ public void SetFilter()
+ {
+ foreach (ObservableCollectionView view in observableCollectionViewList)
+ {
+ view.SetFilter(item => AssetNameContainsText(((Item)item!).Value, MainVM.FilterText));
+ }
+ }
+
+ public void SetSort()
+ {
+ Comparison