diff --git a/UndertaleModLib/Compiler/AssemblyWriter.cs b/UndertaleModLib/Compiler/AssemblyWriter.cs index 19935c746..e1e0f9c65 100644 --- a/UndertaleModLib/Compiler/AssemblyWriter.cs +++ b/UndertaleModLib/Compiler/AssemblyWriter.cs @@ -1249,6 +1249,14 @@ private static void AssembleExpression(CodeWriter cw, Parser.Statement e, Parser cw.Emit(Opcode.Push, DataType.Int64).Value = value.valueInt64; cw.typeStack.Push(DataType.Int64); break; + case Parser.ExpressionConstant.Kind.Reference: + { + var instr = cw.Emit(Opcode.Break, DataType.Int32); + instr.Value = (short)-11; // pushref + instr.IntArgument = (int)value.valueNumber; + cw.typeStack.Push(DataType.Variable); + break; + } default: cw.typeStack.Push(DataType.Variable); AssemblyWriterError(cw, "Invalid constant type.", e.Token); diff --git a/UndertaleModLib/Compiler/Compiler.cs b/UndertaleModLib/Compiler/Compiler.cs index f00463e18..3fa2ae6b8 100644 --- a/UndertaleModLib/Compiler/Compiler.cs +++ b/UndertaleModLib/Compiler/Compiler.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using UndertaleModLib.Models; using static UndertaleModLib.Compiler.Compiler.AssemblyWriter; +using AssetRefType = UndertaleModLib.Decompiler.Decompiler.ExpressionAssetRef.RefType; namespace UndertaleModLib.Compiler { @@ -17,6 +18,7 @@ public class CompileContext public bool ensureFunctionsDefined = true; public bool ensureVariablesDefined = true; public static bool GMS2_3; + public bool TypedAssetRefs => Data.IsVersionAtLeast(2023, 8); public int LastCompiledArgumentCount = 0; public Dictionary LocalVars = new Dictionary(); public Dictionary GlobalVars = new Dictionary(); @@ -80,18 +82,21 @@ public void Setup(bool redoAssets = false) private void MakeAssetDictionary() { assetIds.Clear(); - AddAssetsFromList(Data?.GameObjects); - AddAssetsFromList(Data?.Sprites); - AddAssetsFromList(Data?.Sounds); - AddAssetsFromList(Data?.Backgrounds); - AddAssetsFromList(Data?.Paths); - AddAssetsFromList(Data?.Fonts); - AddAssetsFromList(Data?.Timelines); + AddAssetsFromList(Data?.GameObjects, AssetRefType.Object); + AddAssetsFromList(Data?.Sprites, AssetRefType.Sprite); + AddAssetsFromList(Data?.Sounds, AssetRefType.Sound); + AddAssetsFromList(Data?.Backgrounds, AssetRefType.Background); + AddAssetsFromList(Data?.Paths, AssetRefType.Path); + AddAssetsFromList(Data?.Fonts, AssetRefType.Font); + AddAssetsFromList(Data?.Timelines, AssetRefType.Timeline); if (!GMS2_3) - AddAssetsFromList(Data?.Scripts); - AddAssetsFromList(Data?.Shaders); - AddAssetsFromList(Data?.Rooms); - AddAssetsFromList(Data?.AudioGroups); + AddAssetsFromList(Data?.Scripts, AssetRefType.Object /* not actually used */); + AddAssetsFromList(Data?.Shaders, AssetRefType.Shader); + AddAssetsFromList(Data?.Rooms, AssetRefType.Room); + AddAssetsFromList(Data?.AudioGroups, AssetRefType.Sound /* apparently? */); + AddAssetsFromList(Data?.AnimationCurves, AssetRefType.AnimCurve); + AddAssetsFromList(Data?.Sequences, AssetRefType.Sequence); + AddAssetsFromList(Data?.ParticleSystems, AssetRefType.ParticleSystem); scripts.Clear(); if (Data?.Scripts != null) @@ -116,15 +121,30 @@ private void MakeAssetDictionary() } } - private void AddAssetsFromList(IList list) where T : UndertaleNamedResource + private void AddAssetsFromList(IList list, AssetRefType type) where T : UndertaleNamedResource { if (list == null) return; - for (int i = 0; i < list.Count; i++) + if (TypedAssetRefs) { - string name = list[i].Name?.Content; - if (name != null) - assetIds[name] = i; + for (int i = 0; i < list.Count; i++) + { + string name = list[i].Name?.Content; + if (name != null) + { + // Typed asset refs pack their type into the ID + assetIds[name] = (i & 0xffffff) | (((int)type & 0x7f) << 24); + } + } + } + else + { + for (int i = 0; i < list.Count; i++) + { + string name = list[i].Name?.Content; + if (name != null) + assetIds[name] = i; + } } } } diff --git a/UndertaleModLib/Compiler/Parser.cs b/UndertaleModLib/Compiler/Parser.cs index 9854fc849..9638cc1de 100644 --- a/UndertaleModLib/Compiler/Parser.cs +++ b/UndertaleModLib/Compiler/Parser.cs @@ -31,7 +31,8 @@ public enum Kind Number, String, Constant, - Int64 + Int64, + Reference } public ExpressionConstant(double val) @@ -1338,7 +1339,7 @@ private static Statement ParsePostAndRef(CompileContext context) // Parse chain variable reference Statement result = new Statement(Statement.StatementKind.ExprVariableRef, remainingStageOne.Peek().Token); bool combine = false; - if (left.Kind != Statement.StatementKind.ExprConstant) + if (left.Kind != Statement.StatementKind.ExprConstant || left.Constant.kind == ExpressionConstant.Kind.Reference /* TODO: will this ever change? */) result.Children.Add(left); else combine = true; @@ -2707,6 +2708,8 @@ private static bool ResolveIdentifier(CompileContext context, string identifier, } return false; } + if (context.TypedAssetRefs) + constant.kind = ExpressionConstant.Kind.Reference; constant.valueNumber = (double)index; return true; } diff --git a/UndertaleModLib/Decompiler/Assembler.cs b/UndertaleModLib/Decompiler/Assembler.cs index e45662e47..5179f5af6 100644 --- a/UndertaleModLib/Decompiler/Assembler.cs +++ b/UndertaleModLib/Decompiler/Assembler.cs @@ -23,7 +23,8 @@ public static class Assembler { -7, "setstatic" }, { -8, "savearef" }, { -9, "restorearef" }, - { -10, "chknullish" } + { -10, "chknullish" }, + { -11, "pushref" } }; public static Dictionary NameToBreakID = new Dictionary() { @@ -36,7 +37,8 @@ public static class Assembler { "setstatic", -7 }, { "savearef", -8 }, { "restorearef", -9 }, - { "chknullish", -10 } + { "chknullish", -10 }, + { "pushref", -11 } }; // TODO: Improve the error messages @@ -217,7 +219,14 @@ public static UndertaleInstruction AssembleOne(string source, IList !GlobalContext.Data.IsVersionAtLeast(2023, 8); public DecompileContext(GlobalDecompileContext globalContext, UndertaleCode code, bool computeObject = true) { @@ -490,7 +491,7 @@ public override string ToString(DecompileContext context) break;*/ } - if (context.GlobalContext.Data != null && AssetType != AssetIDType.Other) + if ((context.AssetResolutionEnabled || AssetType == AssetIDType.Script) && context.GlobalContext.Data != null && AssetType != AssetIDType.Other) { IList assetList = null; switch (AssetType) @@ -575,6 +576,128 @@ internal override AssetIDType DoTypePropagation(DecompileContext context, AssetI } } + // Represents a reference to an asset in the resource tree, used in 2023.8+ only + public class ExpressionAssetRef : Expression + { + // NOTE: Also see generalized "ResourceType" enum. This has slightly differing values, though + public enum RefType + { + Object = 0, + Sprite = 1, + Sound = 2, + Room = 3, + Background = 4, + Path = 5, + // missing 6 + Font = 7, + Timeline = 8, + // missing 9 + Shader = 10, + Sequence = 11, + AnimCurve = 12, + ParticleSystem = 13 + } + + public int AssetIndex; + public RefType AssetRefType; + + public ExpressionAssetRef(int encodedResourceIndex) + { + Type = UndertaleInstruction.DataType.Variable; + + // Break down index - first 24 bits are the ID, the rest is the ref type + AssetIndex = encodedResourceIndex & 0xffffff; + AssetRefType = (RefType)(encodedResourceIndex >> 24); + } + + public ExpressionAssetRef(int resourceIndex, RefType resourceType) + { + Type = UndertaleInstruction.DataType.Variable; + AssetIndex = resourceIndex; + AssetRefType = resourceType; + } + + internal override bool IsDuplicationSafe() + { + return true; + } + + public override Statement CleanStatement(DecompileContext context, BlockHLStatement block) + { + return this; + } + public override string ToString(DecompileContext context) + { + if (context.GlobalContext.Data != null) + { + IList assetList = null; + switch (AssetRefType) + { + case RefType.Sprite: + assetList = (IList)context.GlobalContext.Data.Sprites; + break; + case RefType.Background: + assetList = (IList)context.GlobalContext.Data.Backgrounds; + break; + case RefType.Sound: + assetList = (IList)context.GlobalContext.Data.Sounds; + break; + case RefType.Font: + assetList = (IList)context.GlobalContext.Data.Fonts; + break; + case RefType.Path: + assetList = (IList)context.GlobalContext.Data.Paths; + break; + case RefType.Timeline: + assetList = (IList)context.GlobalContext.Data.Timelines; + break; + case RefType.Room: + assetList = (IList)context.GlobalContext.Data.Rooms; + break; + case RefType.Object: + assetList = (IList)context.GlobalContext.Data.GameObjects; + break; + case RefType.Shader: + assetList = (IList)context.GlobalContext.Data.Shaders; + break; + case RefType.AnimCurve: + assetList = (IList)context.GlobalContext.Data.AnimationCurves; + break; + case RefType.Sequence: + assetList = (IList)context.GlobalContext.Data.Sequences; + break; + case RefType.ParticleSystem: + assetList = (IList)context.GlobalContext.Data.ParticleSystems; + break; + } + + if (assetList != null && AssetIndex >= 0 && AssetIndex < assetList.Count) + return ((UndertaleNamedResource)assetList[AssetIndex]).Name.Content; + } + return $"/* ERROR: missing {AssetRefType} asset, using ID instead */ {AssetIndex}"; + } + internal override AssetIDType DoTypePropagation(DecompileContext context, AssetIDType suggestedType) + { + // Convert type to corresponding AssetIDType equivalent + return AssetRefType switch + { + RefType.Object => AssetIDType.GameObject, + RefType.Sprite => AssetIDType.Sprite, + RefType.Sound => AssetIDType.Sound, + RefType.Room => AssetIDType.Room, + RefType.Background => AssetIDType.Background, + RefType.Path => AssetIDType.Path, + RefType.Font => AssetIDType.Font, + RefType.Timeline => AssetIDType.Timeline, + RefType.Shader => AssetIDType.Shader, + RefType.Sequence => AssetIDType.Sequence, + RefType.AnimCurve => AssetIDType.AnimCurve, + RefType.ParticleSystem => AssetIDType.ParticleSystem, + _ => throw new NotImplementedException($"Missing ref type {AssetRefType}") + }; + } + } + // Represents an expression converted to one of another data type - makes no difference on high-level code. public class ExpressionCast : Expression { @@ -1596,8 +1719,6 @@ cast.Argument is ExpressionConstant constant && return String.Format("{0}({1})", OverridenName != string.Empty ? OverridenName : Function.Name.Content, argumentString.ToString()); } - - } public override Statement CleanStatement(DecompileContext context, BlockHLStatement block) @@ -2586,6 +2707,9 @@ assign.Value is FunctionDefinition funcDef && // Note that this operator peeks from the stack, it does not pop directly. break; + case -11: // GM 2023.8+, pushref + stack.Push(new ExpressionAssetRef(instr.IntArgument)); + break; } } diff --git a/UndertaleModLib/Models/UndertaleCode.cs b/UndertaleModLib/Models/UndertaleCode.cs index 273c3b630..b351b718c 100644 --- a/UndertaleModLib/Models/UndertaleCode.cs +++ b/UndertaleModLib/Models/UndertaleCode.cs @@ -209,7 +209,9 @@ public enum ComparisonType : byte public object Value { get; set; } public Reference Destination { get; set; } public Reference Function { get; set; } - public int JumpOffset { get; set; } + private int _IntegerArgument; + public int JumpOffset { get => _IntegerArgument; set => _IntegerArgument = value; } + public int IntArgument { get => _IntegerArgument; set => _IntegerArgument = value; } public bool JumpOffsetPopenvExitMagic { get; set; } public ushort ArgumentsCount { get; set; } public byte Extra { get; set; } @@ -560,6 +562,7 @@ public void Serialize(UndertaleWriter writer) writer.Write((short)Value); writer.Write((byte)Type1); writer.Write((byte)Kind); + if (Type1 == DataType.Int32) writer.Write(IntArgument); } break; @@ -745,6 +748,12 @@ public void Unserialize(UndertaleReader reader) Value = reader.ReadInt16(); Type1 = (DataType)reader.ReadByte(); if (reader.ReadByte() != (byte)Kind) throw new Exception("really shouldn't happen"); + if (Type1 == DataType.Int32) + { + IntArgument = reader.ReadInt32(); + if (!reader.undertaleData.IsVersionAtLeast(2023, 8)) + reader.undertaleData.SetGMS2Version(2023, 8); + } } break; @@ -771,7 +780,6 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader) case InstructionType.DoubleTypeInstruction: case InstructionType.ComparisonInstruction: case InstructionType.GotoInstruction: - case InstructionType.BreakInstruction: reader.Position += 4; break; @@ -817,6 +825,17 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader) reader.Position += 8; return 1; // "Function" + case InstructionType.BreakInstruction: + { + reader.Position += 2; + DataType Type1 = (DataType)reader.ReadByte(); + if (Type1 == DataType.Int32) + reader.Position += 5; + else + reader.Position += 1; + break; + } + default: throw new IOException("Unknown opcode " + Kind.ToString().ToUpper(CultureInfo.InvariantCulture)); } @@ -946,6 +965,11 @@ public string ToString(UndertaleCode code, List blocks = null) sb.Append(" "); sb.Append(Value); } + if (Type1 == DataType.Int32) + { + sb.Append(" "); + sb.Append(IntArgument); + } break; } return sb.ToString(); @@ -960,6 +984,8 @@ public uint CalculateInstructionSize() return 3; else if (Type1 != DataType.Int16) return 2; + if (Kind == Opcode.Break && Type1 == DataType.Int32) + return 2; return 1; } } diff --git a/UndertaleModLib/Models/UndertaleTags.cs b/UndertaleModLib/Models/UndertaleTags.cs index 78f11d1a1..f8ccffe9f 100644 --- a/UndertaleModLib/Models/UndertaleTags.cs +++ b/UndertaleModLib/Models/UndertaleTags.cs @@ -29,6 +29,7 @@ public static int GetAssetTagID(UndertaleData data, UndertaleNamedResource resou UndertaleShader => ResourceType.Shader, UndertaleSequence => ResourceType.Sequence, UndertaleAnimationCurve => ResourceType.AnimCurve, + UndertaleParticleSystem => ResourceType.ParticleSystem, _ => throw new ArgumentException("Invalid resource type!") }; IList list = data[resource.GetType()]; diff --git a/UndertaleModLib/UndertaleBaseTypes.cs b/UndertaleModLib/UndertaleBaseTypes.cs index 44d5604a9..b4341c7c9 100644 --- a/UndertaleModLib/UndertaleBaseTypes.cs +++ b/UndertaleModLib/UndertaleBaseTypes.cs @@ -87,7 +87,10 @@ public enum ResourceType // GMS2.3+ Sequence = 11, - AnimCurve = 12 + AnimCurve = 12, + + // GM 2023+ + ParticleSystem = 13 } public interface UndertaleResource : UndertaleObject diff --git a/UndertaleModTests/GameLoadingTests.cs b/UndertaleModTests/GameLoadingTests.cs index 4092c514e..9680a1688 100644 --- a/UndertaleModTests/GameLoadingTests.cs +++ b/UndertaleModTests/GameLoadingTests.cs @@ -96,7 +96,7 @@ public void DisassembleAndReassembleAllScripts() Assert.AreEqual(code.Instructions[i].ArgumentsCount, reasm[i].ArgumentsCount, errMsg); Assert.AreEqual(code.Instructions[i].JumpOffsetPopenvExitMagic, reasm[i].JumpOffsetPopenvExitMagic, errMsg); if (!code.Instructions[i].JumpOffsetPopenvExitMagic) - Assert.AreEqual(code.Instructions[i].JumpOffset, reasm[i].JumpOffset, errMsg); + Assert.AreEqual(code.Instructions[i].JumpOffset, reasm[i].JumpOffset, errMsg); // note: also handles IntArgument implicitly Assert.AreSame(code.Instructions[i].Destination?.Target, reasm[i].Destination?.Target, errMsg); Assert.AreEqual(code.Instructions[i].Destination?.Type, reasm[i].Destination?.Type, errMsg); Assert.AreSame(code.Instructions[i].Function?.Target, reasm[i].Function?.Target, errMsg); diff --git a/UndertaleModTool/Editors/UndertaleCodeEditor.xaml.cs b/UndertaleModTool/Editors/UndertaleCodeEditor.xaml.cs index bd9fa9849..687f4163d 100644 --- a/UndertaleModTool/Editors/UndertaleCodeEditor.xaml.cs +++ b/UndertaleModTool/Editors/UndertaleCodeEditor.xaml.cs @@ -1105,6 +1105,12 @@ public override VisualLineElement ConstructElement(int offset) possibleObjects.Add(data.Shaders[id]); if (id < data.Timelines.Count) possibleObjects.Add(data.Timelines[id]); + if (id < (data.AnimationCurves?.Count ?? 0)) + possibleObjects.Add(data.AnimationCurves[id]); + if (id < (data.Sequences?.Count ?? 0)) + possibleObjects.Add(data.Sequences[id]); + if (id < (data.ParticleSystems?.Count ?? 0)) + possibleObjects.Add(data.ParticleSystems[id]); } ContextMenuDark contextMenu = new();