Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,021 changes: 565 additions & 456 deletions UndertaleModLib/Decompiler/Assembler.cs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion UndertaleModLib/Decompiler/GameSpecificResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ private enum ConditionResult

private static readonly Dictionary<string, Func<UndertaleData, string, ConditionResult>> _conditionEvaluators = new()
{
["Always"] = (UndertaleData data, string value) =>
["Always"] = (UndertaleData _, string _) =>
{
return ConditionResult.Accept;
},
Expand Down
36 changes: 19 additions & 17 deletions UndertaleModLib/Models/UndertaleCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Underanalyzer;
using UndertaleModLib.Decompiler;
using UndertaleModLib.Util;
// ReSharper disable All

namespace UndertaleModLib.Models;

Expand Down Expand Up @@ -210,30 +211,34 @@ public enum DataType : byte

public enum InstanceType : short
{
Undefined = 0, // actually, this is just object 0, but also occurs in places where no instance type was set

/// Note: This is just object 0, but also occurs in places where no instance type was set
Undefined = 0,
Self = -1,
Other = -2,
All = -3,
Noone = -4,
Global = -5,
Builtin = -6, // Note: Used only in UndertaleVariable.VarID (which is not really even InstanceType)
/// Note: Used only in UndertaleVariable.VarID (which is not really even InstanceType)
Builtin = -6,
Local = -7,
Stacktop = -9,
Arg = -15,
Static = -16

// anything > 0 => GameObjectIndex
// anything > 0 --> GameObjectIndex (or Instance ID if VariableType.Instance)
}

public enum VariableType : byte
{
Array = 0x00,
StackTop = 0x80,
Normal = 0xA0,
Instance = 0xE0, // the InstanceType is an instance ID inside the room -100000
ArrayPushAF = 0x10, // GMS2.3+, multidimensional array with pushaf
ArrayPopAF = 0x90, // GMS2.3+, multidimensional array with pushaf or popaf
/// The InstanceType is an Instance ID inside a room, minus 100000
Instance = 0xE0,
/// GMS2.3+, multidimensional array with pushaf
ArrayPushAF = 0x10,
/// GMS2.3+, multidimensional array with pushaf or popaf
ArrayPopAF = 0x90,
}

public enum ComparisonType : byte
Expand Down Expand Up @@ -505,17 +510,15 @@ public void Serialize(UndertaleWriter writer)
// Transform opcode as needed for bytecode 14
firstWord = (firstWord & 0xFFFFFF) | ((uint)ConvertNewKindToOldKind((byte)(firstWord >> 24)) << 24);
}
else
else if ((firstWord & 0xFFFFFF) != 0xF00000 && (firstWord & 0x800000) != 0)
{
// Additionally, after bytecode 14, transform 24-bit negative branch into a 23-bit negative branch
if ((firstWord & 0xFFFFFF) != 0xF00000 && (firstWord & 0x800000) != 0)
{
// Unset 24-bit sign bit
firstWord &= ~0x800000u;

// Unset 24-bit sign bit
firstWord &= ~0x800000u;

// Set 23-bit sign bit
firstWord |= 0x400000;
}
// Set 23-bit sign bit
firstWord |= 0x400000;
}
writer.Write(firstWord);
break;
Expand Down Expand Up @@ -958,7 +961,7 @@ public void ToString(StringBuilder stringBuilder, UndertaleCode code, uint addre
bool unknownBreak = false;
if (type == InstructionType.BreakInstruction)
{
if (!Assembler.BreakIDToName.TryGetValue(ExtendedKind, out kind))
if (!Assembler.ExtendedIDToName.TryGetValue(ExtendedKind, out kind))
{
kind = kind.ToLower(CultureInfo.InvariantCulture);
unknownBreak = true;
Expand Down Expand Up @@ -1104,7 +1107,6 @@ public void ToString(StringBuilder stringBuilder, UndertaleCode code, uint addre
if (type1 == DataType.Int64)
{
sbh.Append(stringBuilder, ValueLong.ToString(null, CultureInfo.InvariantCulture));
break;
}
break;

Expand Down
15 changes: 15 additions & 0 deletions UndertaleModLib/Models/UndertaleExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -470,4 +470,19 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader)

return count;
}

public static bool ProductDataEligible(UndertaleData data)
{
uint major = data?.GeneralInfo?.Major ?? 0;
if (major >= 2)
{
return true;
}
uint build = data?.GeneralInfo?.Build ?? 0;
if (build >= 1773 || build == 1559) // NOTE: unknown if 1773 is the earliest version which contains product IDs/data
{
return true;
}
return false;
}
}
2 changes: 1 addition & 1 deletion UndertaleModLib/Models/UndertaleString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public bool SearchMatches(string filter)
/// <returns>A string which features the <b>text</b> <c>\n</c>, <c>\r</c>, <c>"</c> and <c>\</c> being properly unescaped.</returns>
public static string UnescapeText(string text)
{
// TODO: optimize this? seems like a very whacky thing to do... why do they have escaped text in the first place?
// TODO: optimize this? seems like a very whacky thing to do... (could use a stringbuilder or something)
return text.Replace("\\r", "\r").Replace("\\n", "\n").Replace("\\\"", "\"").Replace("\\\\", "\\");
}
}
14 changes: 1 addition & 13 deletions UndertaleModLib/UndertaleChunks.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using UndertaleModLib.Models;
using UndertaleModLib.Util;
using static UndertaleModLib.Models.UndertaleRoom;
Expand Down Expand Up @@ -375,8 +369,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<byte[]>();
// NOTE: I do not know if 1773 is the earliest version which contains product IDs.
if (reader.undertaleData.GeneralInfo?.Major >= 2 || (reader.undertaleData.GeneralInfo?.Major == 1 && reader.undertaleData.GeneralInfo?.Build >= 1773) || (reader.undertaleData.GeneralInfo?.Major == 1 && reader.undertaleData.GeneralInfo?.Build == 1539))
if (UndertaleExtension.ProductDataEligible(reader.undertaleData))
{
for (int i = 0; i < List.Count; i++)
{
Expand Down Expand Up @@ -903,11 +896,6 @@ public class UndertaleChunkSHDR : UndertaleListChunk<UndertaleShader>
{
public override string Name => "SHDR";

internal override void SerializeChunk(UndertaleWriter writer)
{
base.SerializeChunk(writer);
}

internal override void UnserializeChunk(UndertaleReader reader)
{
long chunkEnd = reader.AbsPosition + Length;
Expand Down
72 changes: 63 additions & 9 deletions UndertaleModLib/UndertaleData.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
Expand Down Expand Up @@ -376,13 +375,16 @@ public UndertaleData()
/// <summary>
/// Get a resource from the data file by name.
/// </summary>
/// <remarks>
/// This does a linear search, and will thus be slow if used many times. It's recommended to build lookup maps if many searches are required.
/// </remarks>
/// <param name="name">The name of the desired resource.</param>
/// <param name="ignoreCase">Whether to ignore casing while searching.</param>
/// <returns>The <see cref="UndertaleResource"/>.</returns>
/// <returns>The <see cref="UndertaleNamedResource"/>.</returns>
public UndertaleNamedResource ByName(string name, bool ignoreCase = false)
{
// TODO: Check if those are all possible types
return Sounds.ByName(name, ignoreCase) ??
return
Sounds.ByName(name, ignoreCase) ??
Sprites.ByName(name, ignoreCase) ??
Backgrounds.ByName(name, ignoreCase) ??
Paths.ByName(name, ignoreCase) ??
Expand All @@ -400,11 +402,14 @@ public UndertaleNamedResource ByName(string name, bool ignoreCase = false)
}

/// <summary>
/// Reports the zero-based index of the first occurrence of the specified <see cref="UndertaleResource"/>.
/// Returns the zero-based index of the first occurrence of the specified <see cref="UndertaleResource"/>.
/// </summary>
/// <remarks>
/// This does a linear search, and will thus be slow if used many times. It's recommended to build lookup maps if many searches are required.
/// </remarks>
/// <param name="obj">The object to get the index of.</param>
/// <param name="panicIfInvalid">Whether to throw if <paramref name="obj"/> is not a valid object.</param>
/// <returns>The zero-based index position of the <paramref name="obj"/> parameter if it is found or -2 if it is not.</returns>
/// <returns>The zero-based index position of the <paramref name="obj"/> parameter if it is found, or -1 if it is not found.</returns>
/// <exception cref="InvalidOperationException"><paramref name="panicIfInvalid"/> is <see langword="true"/>
/// and <paramref name="obj"/> could not be found.</exception>
public int IndexOf(UndertaleResource obj, bool panicIfInvalid = true)
Expand All @@ -419,12 +424,61 @@ public int IndexOf(UndertaleResource obj, bool panicIfInvalid = true)

if (panicIfInvalid)
throw new InvalidOperationException();
return -2;
return -1;
}

internal int IndexOfByName(string line)

/// <summary>
/// Returns the zero-based index of the first occurrence of the specified <see cref="UndertaleNamedResource"/>.
/// </summary>
/// <remarks>
/// This does a linear search, and will thus be slow if used many times. It's recommended to build lookup maps if many searches are required.
/// </remarks>
/// <param name="name">The name of the desired resource.</param>
/// <param name="ignoreCase">Whether to ignore casing while searching.</param>
/// <returns>The zero-based index position of the <see cref="UndertaleNamedResource"/> if it is found, or -1 if it is not found.</returns>
public int IndexOfByName(string name, bool ignoreCase = false)
{
throw new NotImplementedException();
int res = Sounds.IndexOfName(name, ignoreCase);
if (res >= 0) return res;
res = Sprites.IndexOfName(name, ignoreCase);
if (res >= 0) return res;
res = Backgrounds.IndexOfName(name, ignoreCase);
if (res >= 0) return res;
res = Paths.IndexOfName(name, ignoreCase);
if (res >= 0) return res;
res = Scripts.IndexOfName(name, ignoreCase);
if (res >= 0) return res;
res = Fonts.IndexOfName(name, ignoreCase);
if (res >= 0) return res;
res = GameObjects.IndexOfName(name, ignoreCase);
if (res >= 0) return res;
res = Rooms.IndexOfName(name, ignoreCase);
if (res >= 0) return res;
res = Extensions.IndexOfName(name, ignoreCase);
if (res >= 0) return res;
res = Shaders.IndexOfName(name, ignoreCase);
if (res >= 0) return res;
res = Timelines.IndexOfName(name, ignoreCase);
if (res >= 0) return res;

if (AnimationCurves is not null)
{
res = AnimationCurves.IndexOfName(name, ignoreCase);
if (res >= 0) return res;
}
if (Sequences is not null)
{
res = Sequences.IndexOfName(name, ignoreCase);
if (res >= 0) return res;
}
if (AudioGroups is not null)
{
res = AudioGroups.IndexOfName(name, ignoreCase);
if (res >= 0) return res;
}

return -1;
}

/// <summary>
Expand Down
7 changes: 2 additions & 5 deletions UndertaleModLib/UndertaleIO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using UndertaleModLib.Compiler;
using UndertaleModLib.Models;
using UndertaleModLib.Util;
Expand Down Expand Up @@ -390,7 +387,7 @@ private readonly BindingFlags publicStaticFlags
= BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy;
private readonly Type[] readerArgType = { typeof(UndertaleReader) };
private readonly Type delegateType = typeof(Func<UndertaleReader, uint>);
private readonly Func<UndertaleReader, uint> blankCountFunc = new(_ => { return 0; });
private readonly Func<UndertaleReader, uint> blankCountFunc = _ => 0;

private bool ProcessObjectCountingErrors(uint poolSize = 0)
{
Expand All @@ -400,7 +397,7 @@ private bool ProcessObjectCountingErrors(uint poolSize = 0)
{
string fileDir = Path.GetDirectoryName(Environment.ProcessPath);
File.WriteAllText(Path.Combine(fileDir, "unserializeCountError.txt"),
countUnserializeExc.ToString() + "\n"
countUnserializeExc + "\n"
+ countUnserializeExc.Message + "\n"
+ countUnserializeExc.StackTrace);

Expand Down
21 changes: 14 additions & 7 deletions UndertaleModLib/UndertaleLists.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Linq;
using System.Reflection;
using UndertaleModLib.Models;
using UndertaleModLib.Util;

namespace UndertaleModLib
{
Expand Down Expand Up @@ -354,7 +353,7 @@ public void Serialize(UndertaleWriter writer)
// Write all pointers
foreach (T obj in this)
{
writer.WriteUndertaleObjectPointer<T>(obj);
writer.WriteUndertaleObjectPointer(obj);
}

// Write blobs, if necessary for the given type
Expand Down Expand Up @@ -439,7 +438,9 @@ public void Unserialize(UndertaleReader reader)
if (reader.undertaleData.IsGameMaker2())
{
if (!reader.undertaleData.IsVersionAtLeast(2024, 11))
{
reader.undertaleData.SetGMS2Version(2024, 11);
}
}
realCount--;
}
Expand All @@ -454,7 +455,7 @@ public void Unserialize(UndertaleReader reader)
// Advance to start of first object (particularly, if blobs exist)
if (realCount > 0)
{
T firstItem = this.First(i => i is not null);
T firstItem = this.First(obj => obj is not null);
uint pos = reader.GetAddressForUndertaleObject(firstItem);
if (reader.AbsPosition != pos)
{
Expand All @@ -478,8 +479,10 @@ public void Unserialize(UndertaleReader reader)
{
T obj = this[(int)j];
if (obj is null)
{
continue;

}

// Unserialize pre-padding, if this is a type that requires it
if (t.IsAssignableTo(typeof(PrePaddedObject)))
{
Expand Down Expand Up @@ -510,7 +513,7 @@ public void Unserialize(UndertaleReader reader)
public static uint UnserializeChildObjectCount(UndertaleReader reader)
{
// Read base object count; short-circuit if there's no objects
uint count = reader.ReadUInt32(), pointerCount = count;
uint count = reader.ReadUInt32();
if (count == 0)
{
return 0;
Expand All @@ -528,13 +531,17 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader)
if (reader.undertaleData.IsGameMaker2())
{
if (!reader.undertaleData.IsVersionAtLeast(2024, 11))
{
reader.undertaleData.SetGMS2Version(2024, 11);
}
}
else
{
reader.SubmitWarning("Null pointers found in pointer list on file built with GMS pre-2!");
reader.SubmitWarning("Null pointers found in pointer list on file built with pre-GMS2!");
}
i--; count--;

i--;
count--;
continue;
}
pointers[i] = pointer;
Expand Down
2 changes: 1 addition & 1 deletion UndertaleModTests/GameLoadingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public void DisassembleAndReassembleAllScripts()
throw new Exception("Failed to disassemble script " + code.Name.Content, e);
}

IList<UndertaleInstruction> reasm = Assembler.Assemble(disasm, data.Functions, data.Variables, data.Strings);
IList<UndertaleInstruction> reasm = Assembler.Assemble(disasm, data);
Assert.AreEqual(code.Instructions.Count, reasm.Count, "Reassembled instruction count didn't match the disassembly for script " + code.Name.Content);
uint address = 0;
for(int i = 0; i < code.Instructions.Count; i++)
Expand Down
Loading