diff --git a/src/HexManiac.Core/Models/Code/ScriptArg.cs b/src/HexManiac.Core/Models/Code/ScriptArg.cs
new file mode 100644
index 00000000..30cceb73
--- /dev/null
+++ b/src/HexManiac.Core/Models/Code/ScriptArg.cs
@@ -0,0 +1,320 @@
+using HavenSoft.HexManiac.Core.Models.Runs;
+using HavenSoft.HexManiac.Core.ViewModels;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
+
+namespace HavenSoft.HexManiac.Core.Models.Code {
+
+ public interface IScriptArg {
+ ArgType Type { get; }
+ ExpectedPointerType PointerType { get; }
+ string Name { get; }
+ string EnumTableName { get; }
+
+ int Length(IDataModel model, int start);
+ }
+
+ public class ScriptArg : IScriptArg {
+ private int length;
+
+ public ArgType Type { get; }
+ public ExpectedPointerType PointerType { get; }
+ public string Name { get; }
+ public string EnumTableName { get; }
+ public int EnumOffset { get; }
+
+ public int Length(IDataModel model, int start) => length;
+
+ public ScriptArg(string token) {
+ (Type, PointerType, Name, EnumTableName, length) = Construct(token);
+ if (EnumTableName == null) return;
+ if (EnumTableName.Contains("+")) {
+ var parts = EnumTableName.Split(new[] { '+' }, 2);
+ EnumTableName = parts[0];
+ if (parts[1].TryParseInt(out var result)) EnumOffset = result;
+ } else if (EnumTableName.Contains("-")) {
+ var parts = EnumTableName.Split(new[] { '-' }, 2);
+ EnumTableName = parts[0];
+ if (parts[1].TryParseInt(out var result)) EnumOffset = -result;
+ }
+ }
+
+ public static (ArgType type, ExpectedPointerType pointerType, string name, string enumTableName, int length) Construct(string token) {
+ if (token.Contains("<>")) {
+ var (type, length) = (ArgType.Pointer, 4);
+ var name = token.Split(new[] { "<>" }, StringSplitOptions.None).First();
+ return (type, ExpectedPointerType.Unknown, name, default, length);
+ } else if (token.Contains("<\"\">")) {
+ var (type, length) = (ArgType.Pointer, 4);
+ var name = token.Split(new[] { "<\"\">" }, StringSplitOptions.None).First();
+ return (type, ExpectedPointerType.Text, name, default, length);
+ } else if (token.Contains("<`mart`>")) {
+ var (type, length) = (ArgType.Pointer, 4);
+ var name = token.Split(new[] { "<`mart`>" }, StringSplitOptions.None).First();
+ return (type, ExpectedPointerType.Mart, name, default, length);
+ } else if (token.Contains("<`decor`>")) {
+ var (type, length) = (ArgType.Pointer, 4);
+ var name = token.Split(new[] { "<`decor`>" }, StringSplitOptions.None).First();
+ return (type, ExpectedPointerType.Decor, name, default, length);
+ } else if (token.Contains("<`move`>")) {
+ var (type, length) = (ArgType.Pointer, 4);
+ var name = token.Split(new[] { "<`move`>" }, StringSplitOptions.None).First();
+ return (type, ExpectedPointerType.Movement, name, default, length);
+ } else if (token.Contains("<`oam`>")) {
+ var (type, length) = (ArgType.Pointer, 4);
+ var name = token.Split(new[] { "<`oam`>" }, StringSplitOptions.None).First();
+ return (type, ExpectedPointerType.SpriteTemplate, name, default, length);
+
+ } else if (token.Contains("<`xse`>")) {
+ var (type, length) = (ArgType.Pointer, 4);
+ var name = token.Split(new[] { "<`xse`>" }, StringSplitOptions.None).First();
+ return (type, ExpectedPointerType.Script, name, default, length);
+ } else if (token.Contains("<`bse`>")) {
+ var (type, length) = (ArgType.Pointer, 4);
+ var name = token.Split(new[] { "<`bse`>" }, StringSplitOptions.None).First();
+ return (type, ExpectedPointerType.Script, name, default, length);
+ } else if (token.Contains("<`ase`>")) {
+ var (type, length) = (ArgType.Pointer, 4);
+ var name = token.Split(new[] { "<`ase`>" }, StringSplitOptions.None).First();
+ return (type, ExpectedPointerType.Script, name, default, length);
+ } else if (token.Contains("<`tse`>")) {
+ var (type, length) = (ArgType.Pointer, 4);
+ var name = token.Split(new[] { "<`tse`>" }, StringSplitOptions.None).First();
+ return (type, ExpectedPointerType.Script, name, default, length);
+
+ } else if (token.Contains("::")) {
+ var (type, length) = (ArgType.Word, 4);
+ var name = token.Split(new[] { "::" }, StringSplitOptions.None).First();
+ var enumTableName = token.Split("::").Last();
+ return (type, default, name, enumTableName, length);
+ } else if (token.Contains(':')) {
+ var (type, length) = (ArgType.Short, 2);
+ var name = token.Split(':').First();
+ var enumTableName = token.Split(':').Last();
+ return (type, default, name, enumTableName, length);
+ } else if (token.Contains('.')) {
+ var (type, length) = (ArgType.Byte, 1);
+ var parts = token.Split(new[] { '.' }, 2);
+ var name = parts[0];
+ var enumTableName = parts[1];
+ return (type, default, name, enumTableName, length);
+ } else {
+ // didn't find a token :(
+ // I guess it's a byte?
+ var (type, length) = (ArgType.Byte, 1);
+ var name = token;
+ return (type, default, name, default, length);
+ }
+ }
+
+ public static bool IsValidToken(string token) {
+ return "<> <`xse`> <`bse`> <`ase`> <`tse`> <\"\"> <`mart`> <`decor`> <`move`> <`oam`> : .".Split(' ').Any(token.Contains);
+ }
+
+ public bool FitsInRange(IDataModel model, int address) {
+ if (string.IsNullOrEmpty(EnumTableName) || EnumTableName.StartsWith("|")) return true;
+ return model.ReadMultiByteValue(address, length) < model.GetOptions(EnumTableName).Count;
+ }
+
+ private string Convert(IDataModel model, int value, int bytes) {
+ var preferHex = EnumTableName?.EndsWith("|h") ?? false;
+ var preferSign = EnumTableName?.EndsWith("|z") ?? false;
+ var enumName = EnumTableName?.Split('|')[0];
+ var table = string.IsNullOrEmpty(enumName) ? null : model.GetOptions(enumName);
+ if (table == null || value - EnumOffset < 0 || table.Count <= value - EnumOffset || string.IsNullOrEmpty(table[value])) {
+ if (preferHex || value == int.MinValue || Math.Abs(value) >= 0x4000) {
+ return "0x" + ((uint)(value - EnumOffset)).ToString($"X{length * 2}");
+ } else {
+ if (bytes == 1 && preferSign) value = (sbyte)value;
+ if (bytes == 2 && preferSign) value = (short)value;
+ return (value - EnumOffset).ToString();
+ }
+ }
+ return table[value - EnumOffset];
+ }
+
+ private string Convert(IDataModel model, string value, out int result) {
+ result = 0;
+ var parseType = "as a number";
+ if (!string.IsNullOrEmpty(EnumTableName)) {
+ if (!EnumTableName.StartsWith("|")) parseType = "from " + EnumTableName;
+ if (ArrayRunEnumSegment.TryParse(EnumTableName, model, value, out result)) {
+ result += EnumOffset;
+ return null;
+ }
+ }
+ if (
+ value.StartsWith("0x") && value.Substring(2).TryParseHex(out result) ||
+ value.StartsWith("0X") && value.Substring(2).TryParseHex(out result) ||
+ value.StartsWith("$") && value.Substring(1).TryParseHex(out result) ||
+ int.TryParse(value, out result)
+ ) {
+ result += EnumOffset;
+ return null;
+ }
+ return $"Could not parse '{value}' {parseType}.";
+ }
+
+ ///
+ /// Build from compiled bytes to text.
+ ///
+ public bool Build(bool allFillerIsZero, IDataModel data, int start, StringBuilder builder, List streamContent, DecompileLabelLibrary labels, IList streamTypes) {
+ if (allFillerIsZero && Name == "filler") return true;
+ if (Type == ArgType.Byte) builder.Append(Convert(data, data[start], 1));
+ if (Type == ArgType.Short) builder.Append(Convert(data, data.ReadMultiByteValue(start, 2), 2));
+ if (Type == ArgType.Word) builder.Append(Convert(data, data.ReadMultiByteValue(start, 4), 4));
+ if (Type == ArgType.Pointer) {
+ var address = data.ReadMultiByteValue(start, 4);
+ if (address < 0x8000000) {
+ builder.Append(labels.AddressToLabel(address, Type == ArgType.Pointer && PointerType == ExpectedPointerType.Script));
+ } else {
+ address -= 0x8000000;
+ builder.Append($"<{labels.AddressToLabel(address, Type == ArgType.Pointer && PointerType == ExpectedPointerType.Script)}>");
+ if (PointerType != ExpectedPointerType.Unknown) {
+ if (data.GetNextRun(address) is IStreamRun stream && stream.Start == address) {
+ streamContent.Add(stream.SerializeRun());
+ streamTypes.Add(PointerType);
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// Build from text to compiled bytes.
+ ///
+ public string Build(IDataModel model, int address, string token, IList results, LabelLibrary labels) {
+ int value;
+ if (Type == ArgType.Byte) {
+ var error = Convert(model, token, out value);
+ if (error != null) return error;
+ results.Add((byte)value);
+ } else if (Type == ArgType.Short) {
+ var error = Convert(model, token, out value);
+ if (error != null) return error;
+ results.Add((byte)value);
+ results.Add((byte)(value >> 8));
+ } else if (Type == ArgType.Word) {
+ var error = Convert(model, token, out value);
+ if (error != null) return error;
+ results.Add((byte)value);
+ results.Add((byte)(value >> 0x8));
+ results.Add((byte)(value >> 0x10));
+ results.Add((byte)(value >> 0x18));
+ } else if (Type == ArgType.Pointer) {
+ if (token.StartsWith("<")) {
+ if (!token.EndsWith(">")) return "Unmatched <>";
+ token = token.Substring(1, token.Length - 2);
+ }
+ if (token.StartsWith("0x")) {
+ token = token.Substring(2);
+ }
+ if (token == "auto") {
+ if (PointerType == ExpectedPointerType.Script || PointerType == ExpectedPointerType.Unknown) {
+ return " only supported for text/data.";
+ }
+ value = Pointer.NULL + DeferredStreamToken.AutoSentinel;
+ } else if (labels.TryResolveLabel(token, out value)) {
+ // resolved to an address
+ } else if (token.TryParseHex(out value)) {
+ // pointer *is* an address: nothing else to do
+ if (value > -Pointer.NULL) value += Pointer.NULL;
+ // public bool RequireCompleteAddresses { get; set; } = true;
+ if (labels.RequireCompleteAddresses && (token.Length < 6 || token.Length > 7)) {
+ return "Script addresses must be 6 or 7 characters long.";
+ }
+ } else if (PointerType != ExpectedPointerType.Script) {
+ return $"'{token}' is not a valid pointer.";
+ } else {
+ labels.AddUnresolvedLabel(token, address);
+ value = Pointer.NULL;
+ }
+ value -= Pointer.NULL;
+ results.Add((byte)value);
+ results.Add((byte)(value >> 0x8));
+ results.Add((byte)(value >> 0x10));
+ results.Add((byte)(value >> 0x18));
+ } else {
+ throw new NotImplementedException();
+ }
+ return null;
+ }
+ }
+
+ public class SilentMatchArg : IScriptArg {
+ public ArgType Type => ArgType.Byte;
+ public ExpectedPointerType PointerType => ExpectedPointerType.Unknown;
+ public string Name => null;
+ public string EnumTableName => null;
+ public int EnumOffset => 0;
+
+ public int Length(IDataModel model, int start) => 1;
+
+ public byte ExpectedValue { get; }
+ public SilentMatchArg(byte value) => ExpectedValue = value;
+ }
+
+ public class ArrayArg : IScriptArg {
+ public ArgType Type { get; }
+ public string Name { get; }
+ public string EnumTableName { get; }
+ public int TokenLength { get; }
+ public ExpectedPointerType PointerType => ExpectedPointerType.Unknown;
+
+ public int Length(IDataModel model, int start) {
+ return model[start] * TokenLength + 1;
+ }
+
+ public ArrayArg(string token) {
+ (Type, _, Name, EnumTableName, TokenLength) = ScriptArg.Construct(token);
+ }
+
+ public string ConvertMany(IDataModel model, int start) {
+ var result = new StringBuilder();
+ var count = model[start];
+ start++;
+ for (int i = 0; i < count; i++) {
+ var value = model.ReadMultiByteValue(start, TokenLength);
+ start += TokenLength;
+ var tokenText = "0x" + value.ToString($"X{TokenLength * 2}");
+ if (!string.IsNullOrEmpty(EnumTableName)) {
+ var table = model.GetOptions(EnumTableName);
+ if ((table?.Count ?? 0) > value) {
+ tokenText = table[value];
+ }
+ }
+ result.Append(tokenText);
+ if (i < count - 1) result.Append(' ');
+ }
+ return result.ToString();
+ }
+
+ public IEnumerable ConvertMany(IDataModel model, IEnumerable info) {
+ foreach (var token in info) {
+ if (string.IsNullOrEmpty(EnumTableName)) {
+ if (token.StartsWith("0x") && token.Substring(2).TryParseHex(out var result)) yield return result;
+ else if (token.StartsWith("0X") && token.Substring(2).TryParseHex(out result)) yield return result;
+ else if (token.StartsWith("$") && token.Substring(1).TryParseHex(out result)) yield return result;
+ else if (int.TryParse(token, out result)) yield return result;
+ else yield return 0;
+ } else if (ArrayRunEnumSegment.TryParse(EnumTableName, model, token, out var enumValue)) {
+ yield return enumValue;
+ } else {
+ yield return 0;
+ }
+ }
+ }
+ }
+
+ public enum ArgType {
+ Byte,
+ Short,
+ Word,
+ Pointer,
+ }
+
+}
diff --git a/src/HexManiac.Core/Models/Code/ScriptLine.cs b/src/HexManiac.Core/Models/Code/ScriptLine.cs
new file mode 100644
index 00000000..99c56400
--- /dev/null
+++ b/src/HexManiac.Core/Models/Code/ScriptLine.cs
@@ -0,0 +1,544 @@
+using HavenSoft.HexManiac.Core.ViewModels;
+using HavenSoft.HexManiac.Core;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+namespace HavenSoft.HexManiac.Core.Models.Code {
+ public interface IScriptLine {
+ IReadOnlyList Args { get; }
+ IReadOnlyList LineCode { get; }
+ string LineCommand { get; }
+ IReadOnlyList Documentation { get; }
+ string Usage { get; }
+
+ bool IsEndingCommand { get; }
+
+ bool MatchesGame(int gameCodeHash);
+ int CompiledByteLength(IDataModel model, int start, IDictionary destinationLengths); // compile from the bytes in the model, at that start location
+ int CompiledByteLength(IDataModel model, string line); // compile from the line of code passed in
+ bool Matches(int gameCodeHash, IReadOnlyList data, int index);
+ string Decompile(IDataModel data, int start, DecompileLabelLibrary labels, IList streamTypes);
+
+ ///
+ /// Returns true if the command looks correct, even if the arguments are incomplete.
+ ///
+ bool CanCompile(string line);
+
+ ///
+ /// Returns an error if the line cannot be compiled, or a set of tokens if it can be compiled.
+ ///
+ string ErrorCheck(string scriptLine, out string[] tokens);
+
+ string Compile(IDataModel model, int start, string scriptLine, LabelLibrary labels, out byte[] result);
+
+ void AddDocumentation(string content);
+
+ public int CountShowArgs() {
+ return Args.Sum(arg => {
+ if (arg is ScriptArg) return 1;
+ return 0;
+ // something with array args?
+ });
+ }
+ }
+
+ public class MacroScriptLine : IScriptLine {
+ private static readonly IReadOnlyList emptyByteList = new byte[0];
+ private readonly List documentation = new List();
+
+ private bool hasShortForm;
+ private readonly Dictionary shortIndexFromLongIndex = new();
+ private readonly IReadOnlyList matchingGames;
+
+ public IReadOnlyList Args { get; }
+ public IReadOnlyList ShortFormArgs {
+ get {
+ if (shortIndexFromLongIndex.Count == 0) {
+ return Args.Where(arg => arg is not SilentMatchArg).ToList();
+ }
+ var args = new IScriptArg[shortIndexFromLongIndex.Count()];
+ foreach (var pair in shortIndexFromLongIndex) {
+ args[pair.Value] = Args[pair.Key];
+ }
+ return args;
+ }
+ }
+ public IReadOnlyList LineCode => emptyByteList;
+ public IReadOnlyList Documentation => documentation;
+ public string LineCommand { get; }
+ public bool IsEndingCommand => false;
+ public bool IsValid { get; } = true;
+ public string Usage { get; private set; }
+
+ public static bool IsMacroLine(string engineLine) {
+ engineLine = engineLine.Trim();
+ var tokens = engineLine.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ if (tokens.Length == 0) return false;
+ var token = tokens[0];
+ if (token.StartsWith("[") && tokens.Length > 1) token = tokens[1];
+ if (token.StartsWith("#")) return false;
+ if (token.Length == 2 && token.TryParseHex(out _)) return false;
+ return true;
+ }
+
+ public MacroScriptLine(string engineLine) {
+ var docSplit = engineLine.Split(new[] { '#' }, 2);
+ if (docSplit.Length > 1) documentation.Add('#' + docSplit[1]);
+ engineLine = docSplit[0].Trim();
+ matchingGames = ScriptLine.ExtractMatchingGames(ref engineLine);
+ ExtractShortformInfo(ref engineLine);
+ if (!hasShortForm) {
+ Usage = " ".Join(engineLine.Split(' ').Where(token => token.Length != 2 || !token.TryParseHex(out _)));
+ }
+ var usageTokens = Usage.Split(" ", StringSplitOptions.RemoveEmptyEntries);
+ Usage = usageTokens[0] + " " + " ".Join(usageTokens.Skip(1).Select(t => t.Split(".:|<".ToCharArray())[0]));
+
+ var tokens = engineLine.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ var args = new List();
+ LineCommand = tokens[0];
+
+ for (int i = 1; i < tokens.Length; i++) {
+ var token = tokens[i];
+ if (token.Length == 2 && token.TryParseHex(out int number)) {
+ args.Add(new SilentMatchArg((byte)number));
+ } else if (ScriptArg.IsValidToken(token)) {
+ args.Add(new ScriptArg(token));
+ } else {
+ IsValid = false;
+ }
+ }
+
+ Args = args;
+ }
+
+ private void ExtractShortformInfo(ref string engineLine) {
+ if (!engineLine.Contains("->")) return;
+ var parts = engineLine.Split("->");
+ if (parts.Length != 2) return;
+ engineLine = parts[1];
+ var shortTokens = parts[0].Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ var longTokens = parts[1].Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ if (shortTokens[0] != longTokens[0]) return;
+ shortTokens = shortTokens.Skip(1).ToArray();
+ longTokens = longTokens.Skip(1).ToArray();
+
+ // for each entry in long, it shows up somewhere in short
+ // entries in long can appear multiple times
+ for (int i = 0; i < longTokens.Length; i++) {
+ var index = shortTokens.IndexOf(longTokens[i]);
+ if (index == -1) continue;
+ shortIndexFromLongIndex.Add(i, index);
+ }
+
+ hasShortForm = true;
+ Usage = parts[0];
+ }
+
+ public bool MatchesGame(int game) => matchingGames?.Contains(game) ?? true;
+
+ public int CompiledByteLength(IDataModel model, int start, IDictionary destinationLengths) {
+ var length = LineCode.Count;
+ foreach (var arg in Args) {
+ if (destinationLengths != null) {
+ var argLength = ScriptParser.GetArgLength(model, arg, start + length, destinationLengths);
+ if (argLength > 0) destinationLengths[model.ReadPointer(start + length)] = argLength;
+ }
+ length += arg.Length(default, -1);
+ }
+ return length;
+ }
+
+ public int CompiledByteLength(IDataModel model, string line) {
+ if (!CanCompile(line)) return 0;
+ var length = LineCode.Count;
+ foreach (var arg in Args) {
+ length += arg.Length(default, -1);
+ }
+ return length;
+ }
+
+ public bool Matches(int gameCodeHash, IReadOnlyList data, int index) {
+ if (Args.Count == 0) return false;
+ if (!MatchesGame(gameCodeHash)) return false;
+ for (int i = 0; i < Args.Count; i++) {
+ var arg = Args[i];
+ if (arg is SilentMatchArg smarg) {
+ if (data[index] != smarg.ExpectedValue) return false;
+ } else if (arg is ScriptArg sarg) {
+ // don't validate, this part is variable
+ } else {
+ throw new NotImplementedException();
+ }
+ index += arg.Length(default, -1);
+ }
+ return true;
+ }
+
+ public string Decompile(IDataModel data, int start, DecompileLabelLibrary labels, IList streamTypes) {
+ var builder = new StringBuilder(LineCommand);
+ var streamContent = new List();
+ var args = new List();
+ foreach (var arg in Args) {
+ if (arg is ScriptArg sarg) {
+ var tempBuilder = new StringBuilder();
+ sarg.Build(false, data, start, tempBuilder, streamContent, labels, streamTypes);
+ args.Add(tempBuilder.ToString());
+ }
+ start += arg.Length(data, start);
+ }
+ if (args.Count > 0) {
+ builder.Append(" ");
+ builder.Append(" ".Join(ConvertLongFormToShortForm(args.ToArray())));
+ }
+ foreach (var content in streamContent) {
+ builder.AppendLine();
+ builder.AppendLine("{");
+ builder.AppendLine(content);
+ builder.Append("}");
+ }
+ return builder.ToString();
+ }
+
+ public bool CanCompile(string line) {
+ var tokens = ScriptLine.Tokenize(line);
+ if (tokens.Length == 0) return false;
+ if (tokens[0] != LineCommand) return false;
+ return true;
+ }
+
+ public string ErrorCheck(string scriptLine, out string[] tokens) {
+ tokens = ScriptLine.Tokenize(scriptLine);
+ for (int i = 1; i < scriptLine.Length - 1; i++) {
+ if (scriptLine[i] != '"') continue;
+ if (scriptLine[i - 1] != ' ' && scriptLine[i + 1] != ' ') return "Cannot have \"quotes\" in the middle of a name.";
+ }
+ if (tokens[0] != LineCommand) throw new ArgumentException($"Command {LineCommand} was expected, but received {tokens[0]} instead.");
+ var args = tokens.Skip(1).ToArray();
+ var shortArgs = args;
+ args = ConvertShortFormToLongForm(args);
+ var commandText = LineCommand;
+ var specifiedArgs = Args.Where(arg => arg is ScriptArg).Count();
+ if (specifiedArgs != args.Length) {
+ return $"Command {commandText} expects {specifiedArgs} arguments, but received {shortArgs.Length} instead.";
+ }
+ return null;
+ }
+
+ public string Compile(IDataModel model, int start, string scriptLine, LabelLibrary labels, out byte[] result) {
+ result = null;
+ var error = ErrorCheck(scriptLine, out var tokens);
+ if (error != null) return error;
+ var args = tokens.Skip(1).ToArray();
+ args = ConvertShortFormToLongForm(args);
+ var results = new List();
+ var specifiedArgIndex = 0;
+ for (int i = 0; i < Args.Count; i++) {
+ if (Args[i] is ScriptArg scriptArg) {
+ var token = args[specifiedArgIndex];
+ var message = scriptArg.Build(model, start + results.Count, token, results, labels);
+ if (message != null) return message;
+ specifiedArgIndex += 1;
+ } else if (Args[i] is SilentMatchArg silentArg) {
+ results.Add(silentArg.ExpectedValue);
+ }
+ }
+ result = results.ToArray();
+ return null;
+ }
+
+ public void AddDocumentation(string doc) => documentation.Add(doc);
+
+ private string[] ConvertShortFormToLongForm(string[] args) {
+ if (!hasShortForm) return args;
+ // build long-form args from this short form
+ var longForm = new List();
+ for (int i = 0; i < Args.Count; i++) {
+ if (Args[i] is SilentMatchArg) continue;
+ var shortIndex = shortIndexFromLongIndex[i];
+ if (shortIndex < args.Length) longForm.Add(args[shortIndex]);
+ }
+ return longForm.ToArray();
+ }
+
+ private string[] ConvertLongFormToShortForm(string[] args) {
+ if (!hasShortForm) return args;
+ var shortForm = new Dictionary();
+ for (int i = 0; i < Args.Count; i++) {
+ if (Args[i] is SilentMatchArg) continue;
+ var shortIndex = shortIndexFromLongIndex[i];
+ shortForm[shortIndex] = args[shortForm.Count];
+ }
+ return shortForm.Count.Range(i => shortForm[i]).ToArray();
+ }
+ }
+
+ public abstract class ScriptLine : IScriptLine {
+ private readonly List documentation = new List();
+ private readonly IReadOnlyList matchingGames;
+
+ public const string Hex = "0123456789ABCDEF";
+ public IReadOnlyList Args { get; }
+ public IReadOnlyList LineCode { get; }
+ public string LineCommand { get; }
+ public IReadOnlyList Documentation => documentation;
+ public string Usage { get; }
+
+ public virtual bool IsEndingCommand { get; }
+
+ /// If this line contains pointers, calculate the pointer data's lengths and include here.
+ public int CompiledByteLength(IDataModel model, int start, IDictionary destinationLengths) {
+ var length = LineCode.Count;
+ foreach (var arg in Args) {
+ if (arg.Type == ArgType.Pointer) {
+ var destination = model.ReadPointer(start + length);
+ if (destinationLengths != null && !destinationLengths.ContainsKey(destination)) {
+ var argLength = ScriptParser.GetArgLength(model, arg, start + length, destinationLengths);
+ if (argLength > 0) destinationLengths[destination] = argLength;
+ }
+ }
+ length += arg.Length(model, start + length);
+ }
+ return length;
+ }
+ public int CompiledByteLength(IDataModel model, string line) {
+ var length = LineCode.Count;
+ var tokens = line.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ for (var i = 0; i < Args.Count; i++) {
+ if (Args[i] is ScriptArg sarg) length += sarg.Length(default, -1);
+ if (Args[i] is ArrayArg aarg) length += aarg.ConvertMany(model, tokens.Skip(i)).Count() * aarg.TokenLength + 1;
+ }
+ return length;
+ }
+
+ public ScriptLine(string engineLine) {
+ var docSplit = engineLine.Split(new[] { '#' }, 2);
+ if (docSplit.Length > 1) documentation.Add('#' + docSplit[1]);
+ engineLine = docSplit[0].Trim();
+ matchingGames = ExtractMatchingGames(ref engineLine);
+ Usage = engineLine.Split(new[] { ' ' }, 2).Last();
+ var usageTokens = Usage.Split(" ", StringSplitOptions.RemoveEmptyEntries);
+ Usage = usageTokens[0] + " " + " ".Join(usageTokens.Skip(1).Select(t => t.Split(".:|<".ToCharArray())[0]));
+
+ var tokens = engineLine.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ var lineCode = new List();
+ var args = new List();
+
+ foreach (var token in tokens) {
+ if (token.Length == 2 && token.All(ViewPort.AllHexCharacters.Contains)) {
+ lineCode.Add(byte.Parse(token, NumberStyles.HexNumber));
+ } else if (token.StartsWith("[") && token.EndsWith("]")) {
+ var content = token.Substring(1, token.Length - 2);
+ args.Add(new ArrayArg(content));
+ } else if (ScriptArg.IsValidToken(token)) {
+ args.Add(new ScriptArg(token));
+ } else {
+ LineCommand = token;
+ }
+ }
+
+ LineCode = lineCode;
+ Args = args;
+ }
+
+ public static IReadOnlyList ExtractMatchingGames(ref string line) {
+ if (!line.StartsWith("[")) return null;
+ var gamesEnd = line.IndexOf("]");
+ if (gamesEnd == -1) return null;
+ var games = line.Substring(1, gamesEnd - 1);
+ line = line.Substring(gamesEnd + 1).TrimStart();
+ return games.Split("_").Select(ConvertAscii).ToList();
+ }
+
+ public static IReadOnlyList GetMatchingGames(IScriptLine line) {
+ var names = new[] { "AXVE", "AXPE", "BPRE", "BPGE", "BPEE" };
+ return names.Where(name => line.MatchesGame(ConvertAscii(name))).ToList();
+ }
+
+ public static int ConvertAscii(string letters) {
+ return letters.Reverse().Aggregate(0, (current, letter) => (current << 8) | (byte)letter);
+ }
+
+ public bool MatchesGame(int game) => matchingGames?.Contains(game) ?? true;
+
+ public void AddDocumentation(string doc) => documentation.Add(doc);
+
+ public bool PartialMatchLine(string line) => LineCommand.MatchesPartial(line.Split(' ')[0]);
+
+ public bool Matches(int gameCodeHash, IReadOnlyList data, int index) {
+ if (index + LineCode.Count >= data.Count) return false;
+ if (MatchesGame(gameCodeHash)) {
+ var result = true;
+ for (int i = 0; result && i < LineCode.Count; i++) result = data[index + i] == LineCode[i]; // avoid making lambda for performance
+ return result;
+ }
+ return false;
+ }
+
+ public bool CanCompile(string line) {
+ if (!(line + " ").StartsWith(LineCommand + " ", StringComparison.CurrentCultureIgnoreCase)) return false;
+ if (LineCode.Count == 1) return true;
+ var tokens = Tokenize(line).ToList();
+ if (tokens.Count < LineCode.Count) return false;
+ tokens.RemoveAt(0);
+ for (int i = 1; i < LineCode.Count; i++) {
+ if (!byte.TryParse(tokens[0], NumberStyles.HexNumber, CultureInfo.CurrentCulture, out var value)) return false;
+ if (value != LineCode[i]) return false;
+ tokens.RemoveAt(0);
+ }
+ return true;
+ }
+
+ public string ErrorCheck(string scriptLine, out string[] tokens) {
+ tokens = Tokenize(scriptLine);
+ if (!tokens[0].Equals(LineCommand, StringComparison.CurrentCultureIgnoreCase)) throw new ArgumentException($"Command {LineCommand} was expected, but received {tokens[0]} instead.");
+ var commandText = LineCommand;
+ for (int i = 1; i < LineCode.Count; i++) commandText += " " + LineCode[i].ToString("X2");
+ var fillerCount = Args.Count(arg => arg.Name == "filler");
+ for (int i = 0; i < fillerCount; i++) {
+ if (tokens.Length < Args.Count + LineCode.Count) tokens = tokens.Append("0").ToArray();
+ }
+ if (Args.Count > 0 && Args.Last() is ArrayArg) {
+ if (Args.Count > tokens.Length) {
+ return $"Command {commandText} expects {Args.Count} arguments, but received {tokens.Length - LineCode.Count} instead.";
+ }
+ } else if (Args.Count != tokens.Length - LineCode.Count) {
+ return $"Command {commandText} expects {Args.Count} arguments, but received {tokens.Length - LineCode.Count} instead.";
+ }
+ return null;
+ }
+
+ public string Compile(IDataModel model, int start, string scriptLine, LabelLibrary labels, out byte[] result) {
+ result = null;
+ var error = ErrorCheck(scriptLine, out var tokens);
+ if (error != null) return error;
+ var results = new List(LineCode);
+ start += LineCode.Count;
+ for (int i = 0; i < Args.Count; i++) {
+ if (Args[i] is ScriptArg scriptArg) {
+ var token = tokens[i + LineCode.Count];
+ var message = scriptArg.Build(model, start, token, results, labels);
+ if (message != null) return message;
+ start += scriptArg.Length(model, start);
+ } else if (Args[i] is ArrayArg arrayArg) {
+ var values = arrayArg.ConvertMany(model, tokens.Skip(i + 1)).ToList();
+ results.Add((byte)values.Count);
+ start += 1;
+ foreach (var value in values) {
+ if (Args[i].Type == ArgType.Byte) {
+ results.Add((byte)value);
+ start += 1;
+ } else if (Args[i].Type == ArgType.Short) {
+ results.Add((byte)value);
+ results.Add((byte)(value >> 8));
+ start += 2;
+ } else if (Args[i].Type == ArgType.Word) {
+ results.Add((byte)value);
+ results.Add((byte)(value >> 0x8));
+ results.Add((byte)(value >> 0x10));
+ results.Add((byte)(value >> 0x18));
+ start += 4;
+ } else {
+ throw new NotImplementedException();
+ }
+ }
+ }
+ }
+ result = results.ToArray();
+ return null;
+ }
+
+ public string Decompile(IDataModel data, int start, DecompileLabelLibrary labels, IList streamTypes) {
+ for (int i = 0; i < LineCode.Count; i++) {
+ if (LineCode[i] != data[start + i]) throw new ArgumentException($"Data at {start:X6} does not match the {LineCommand} command.");
+ }
+ var allFillerIsZero = IsAllFillerZero(data, start);
+ start += LineCode.Count;
+ var builder = new StringBuilder(LineCommand);
+ for (int i = 1; i < LineCode.Count; i++) {
+ builder.Append(" " + LineCode[i].ToHexString());
+ }
+
+ var streamContent = new List();
+ foreach (var arg in Args) {
+ builder.Append(" ");
+ if (arg is ScriptArg scriptArg) {
+ if (scriptArg.Build(allFillerIsZero, data, start, builder, streamContent, labels, streamTypes)) continue;
+ } else if (arg is ArrayArg arrayArg) {
+ builder.Append(arrayArg.ConvertMany(data, start));
+ } else {
+ throw new NotImplementedException();
+ }
+ start += arg.Length(data, start);
+ }
+ foreach (var content in streamContent) {
+ builder.AppendLine();
+ builder.AppendLine("{");
+ builder.AppendLine(content);
+ builder.Append("}");
+ }
+ return builder.ToString();
+ }
+
+ private bool IsAllFillerZero(IDataModel data, int start) {
+ start += LineCode.Count;
+ foreach (var arg in Args) {
+ if (arg.Name == "filler") {
+ var value = data.ReadMultiByteValue(start, arg.Length(data, start));
+ if (value != 0) return false;
+ }
+ start += arg.Length(data, start);
+ }
+ return true;
+ }
+
+ public static string ReadString(IDataModel data, int start) {
+ var length = PCSString.ReadString(data, start, true);
+ return data.TextConverter.Convert(data, start, length);
+ }
+
+ public static string[] Tokenize(string scriptLine) {
+ var result = new List();
+ var quoteCut = scriptLine.Split('"');
+
+ for (int i = 0; i < quoteCut.Length; i++) {
+ if (i % 2 == 0 && quoteCut[i].Length == 0) continue;
+
+ if (i % 2 == 1) result.Add($"\"{quoteCut[i]}\"");
+ else result.AddRange(quoteCut[i].Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries));
+ }
+
+ return result.ToArray();
+ }
+
+ public override string ToString() {
+ return string.Join(" ", LineCode.Select(code => code.ToHexString()).Concat(Args.Select(arg => arg.Name)).ToArray());
+ }
+ }
+
+ public class XSEScriptLine : ScriptLine {
+ public XSEScriptLine(string engineLine) : base(engineLine) { }
+
+ public override bool IsEndingCommand => LineCode.Count == 1 && LineCode[0].IsAny(0x02, 0x03, 0x05, 0x08, 0x0A, 0x0C, 0x0D);
+ }
+
+ public class BSEScriptLine : ScriptLine {
+ public BSEScriptLine(string engineLine) : base(engineLine) { }
+
+ public override bool IsEndingCommand => LineCode.Count == 1 && LineCode[0].IsAny(0x28, 0x3c, 0x3d, 0x3e, 0x3f);
+ }
+
+ public class ASEScriptLine : ScriptLine {
+ public ASEScriptLine(string engineLine) : base(engineLine) { }
+
+ public override bool IsEndingCommand => LineCode.Count == 1 && LineCode[0].IsAny(0x08, 0x0F, 0x11, 0x13);
+ }
+
+ public class TSEScriptLine : ScriptLine {
+ public TSEScriptLine(string engineLine) : base(engineLine) { }
+ public override bool IsEndingCommand => LineCode.Count == 1 && LineCode[0].IsAny(0x45, 0x47, 0x59, 0x5A);
+ }
+
+}
diff --git a/src/HexManiac.Core/Models/Code/ScriptParser.cs b/src/HexManiac.Core/Models/Code/ScriptParser.cs
index 19c9fcce..5df5b81f 100644
--- a/src/HexManiac.Core/Models/Code/ScriptParser.cs
+++ b/src/HexManiac.Core/Models/Code/ScriptParser.cs
@@ -286,7 +286,6 @@ namespace HavenSoft.HexManiac.Core.Models.Code {
// TODO refactor to rely on CollectScripts rather than duplicate code
// returns a list of scripts that were formatted, and their length
- public IDictionary FormatScript(ModelDelta token, IDataModel model, int address) where SERun : IScriptStartRun {
if (!address.InRange(0, model.Count)) return null;
Func, IScriptStartRun> constructor = (a, s) => new XSERun(a, s);
if (typeof(SERun) == typeof(BSERun)) constructor = (a, s) => new BSERun(a, s);
@@ -356,7 +355,6 @@ namespace HavenSoft.HexManiac.Core.Models.Code {
}
length += arg.Length(model, address + length);
}
- if (line.IsEndingCommand) break;
}
processed.Add(address, length);
}
@@ -971,274 +969,6 @@ namespace HavenSoft.HexManiac.Core.Models.Code {
}
}
- public interface IScriptLine {
- IReadOnlyList Args { get; }
- IReadOnlyList LineCode { get; }
- string LineCommand { get; }
- IReadOnlyList Documentation { get; }
- string Usage { get; }
-
- bool IsEndingCommand { get; }
-
- bool MatchesGame(int gameCodeHash);
- int CompiledByteLength(IDataModel model, int start, IDictionary destinationLengths); // compile from the bytes in the model, at that start location
- int CompiledByteLength(IDataModel model, string line); // compile from the line of code passed in
- bool Matches(int gameCodeHash, IReadOnlyList data, int index);
- string Decompile(IDataModel data, int start, DecompileLabelLibrary labels, IList streamTypes);
-
- ///
- /// Returns true if the command looks correct, even if the arguments are incomplete.
- ///
- bool CanCompile(string line);
-
- ///
- /// Returns an error if the line cannot be compiled, or a set of tokens if it can be compiled.
- ///
- string ErrorCheck(string scriptLine, out string[] tokens);
-
- string Compile(IDataModel model, int start, string scriptLine, LabelLibrary labels, out byte[] result);
-
- void AddDocumentation(string content);
-
- public int CountShowArgs() {
- return Args.Sum(arg => {
- if (arg is ScriptArg) return 1;
- return 0;
- // something with array args?
- });
- }
- }
-
- public class MacroScriptLine : IScriptLine {
- private static readonly IReadOnlyList emptyByteList = new byte[0];
- private readonly List documentation = new List();
-
- private bool hasShortForm;
- private readonly Dictionary shortIndexFromLongIndex = new();
- private readonly IReadOnlyList matchingGames;
-
- public IReadOnlyList Args { get; }
- public IReadOnlyList ShortFormArgs {
- get {
- if (shortIndexFromLongIndex.Count == 0) {
- return Args.Where(arg => arg is not SilentMatchArg).ToList();
- }
- var args = new IScriptArg[shortIndexFromLongIndex.Count()];
- foreach (var pair in shortIndexFromLongIndex) {
- args[pair.Value] = Args[pair.Key];
- }
- return args;
- }
- }
- public IReadOnlyList LineCode => emptyByteList;
- public IReadOnlyList Documentation => documentation;
- public string LineCommand { get; }
- public bool IsEndingCommand => false;
- public bool IsValid { get; } = true;
- public string Usage { get; private set; }
-
- public static bool IsMacroLine(string engineLine) {
- engineLine = engineLine.Trim();
- var tokens = engineLine.Split(' ', StringSplitOptions.RemoveEmptyEntries);
- if (tokens.Length == 0) return false;
- var token = tokens[0];
- if (token.StartsWith("[") && tokens.Length > 1) token = tokens[1];
- if (token.StartsWith("#")) return false;
- if (token.Length == 2 && token.TryParseHex(out _)) return false;
- return true;
- }
-
- public MacroScriptLine(string engineLine) {
- var docSplit = engineLine.Split(new[] { '#' }, 2);
- if (docSplit.Length > 1) documentation.Add('#' + docSplit[1]);
- engineLine = docSplit[0].Trim();
- matchingGames = ScriptLine.ExtractMatchingGames(ref engineLine);
- ExtractShortformInfo(ref engineLine);
- if (!hasShortForm) {
- Usage = " ".Join(engineLine.Split(' ').Where(token => token.Length != 2 || !token.TryParseHex(out _)));
- }
- var usageTokens = Usage.Split(" ", StringSplitOptions.RemoveEmptyEntries);
- Usage = usageTokens[0] + " " + " ".Join(usageTokens.Skip(1).Select(t => t.Split(".:|<".ToCharArray())[0]));
-
- var tokens = engineLine.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
- var args = new List();
- LineCommand = tokens[0];
-
- for (int i = 1; i < tokens.Length; i++) {
- var token = tokens[i];
- if (token.Length == 2 && token.TryParseHex(out int number)) {
- args.Add(new SilentMatchArg((byte)number));
- } else if (ScriptArg.IsValidToken(token)) {
- args.Add(new ScriptArg(token));
- } else {
- IsValid = false;
- }
- }
-
- Args = args;
- }
-
- private void ExtractShortformInfo(ref string engineLine) {
- if (!engineLine.Contains("->")) return;
- var parts = engineLine.Split("->");
- if (parts.Length != 2) return;
- engineLine = parts[1];
- var shortTokens = parts[0].Split(' ', StringSplitOptions.RemoveEmptyEntries);
- var longTokens = parts[1].Split(' ', StringSplitOptions.RemoveEmptyEntries);
- if (shortTokens[0] != longTokens[0]) return;
- shortTokens = shortTokens.Skip(1).ToArray();
- longTokens = longTokens.Skip(1).ToArray();
-
- // for each entry in long, it shows up somewhere in short
- // entries in long can appear multiple times
- for (int i = 0; i < longTokens.Length; i++) {
- var index = shortTokens.IndexOf(longTokens[i]);
- if (index == -1) continue;
- shortIndexFromLongIndex.Add(i, index);
- }
-
- hasShortForm = true;
- Usage = parts[0];
- }
-
- public bool MatchesGame(int game) => matchingGames?.Contains(game) ?? true;
-
- public int CompiledByteLength(IDataModel model, int start, IDictionary destinationLengths) {
- var length = LineCode.Count;
- foreach (var arg in Args) {
- if (destinationLengths != null) {
- var argLength = ScriptParser.GetArgLength(model, arg, start + length, destinationLengths);
- if (argLength > 0) destinationLengths[model.ReadPointer(start + length)] = argLength;
- }
- length += arg.Length(default, -1);
- }
- return length;
- }
-
- public int CompiledByteLength(IDataModel model, string line) {
- if (!CanCompile(line)) return 0;
- var length = LineCode.Count;
- foreach (var arg in Args) {
- length += arg.Length(default, -1);
- }
- return length;
- }
-
- public bool Matches(int gameCodeHash, IReadOnlyList data, int index) {
- if (Args.Count == 0) return false;
- if (!MatchesGame(gameCodeHash)) return false;
- for (int i = 0; i < Args.Count; i++) {
- var arg = Args[i];
- if (arg is SilentMatchArg smarg) {
- if (data[index] != smarg.ExpectedValue) return false;
- } else if (arg is ScriptArg sarg) {
- // don't validate, this part is variable
- } else {
- throw new NotImplementedException();
- }
- index += arg.Length(default, -1);
- }
- return true;
- }
-
- public string Decompile(IDataModel data, int start, DecompileLabelLibrary labels, IList streamTypes) {
- var builder = new StringBuilder(LineCommand);
- var streamContent = new List();
- var args = new List();
- foreach (var arg in Args) {
- if (arg is ScriptArg sarg) {
- var tempBuilder = new StringBuilder();
- sarg.Build(false, data, start, tempBuilder, streamContent, labels, streamTypes);
- args.Add(tempBuilder.ToString());
- }
- start += arg.Length(data, start);
- }
- if (args.Count > 0) {
- builder.Append(" ");
- builder.Append(" ".Join(ConvertLongFormToShortForm(args.ToArray())));
- }
- foreach (var content in streamContent) {
- builder.AppendLine();
- builder.AppendLine("{");
- builder.AppendLine(content);
- builder.Append("}");
- }
- return builder.ToString();
- }
-
- public bool CanCompile(string line) {
- var tokens = ScriptLine.Tokenize(line);
- if (tokens.Length == 0) return false;
- if (tokens[0] != LineCommand) return false;
- return true;
- }
-
- public string ErrorCheck(string scriptLine, out string[] tokens) {
- tokens = ScriptLine.Tokenize(scriptLine);
- for (int i = 1; i < scriptLine.Length - 1; i++) {
- if (scriptLine[i] != '"') continue;
- if (scriptLine[i - 1] != ' ' && scriptLine[i + 1] != ' ') return "Cannot have \"quotes\" in the middle of a name.";
- }
- if (tokens[0] != LineCommand) throw new ArgumentException($"Command {LineCommand} was expected, but received {tokens[0]} instead.");
- var args = tokens.Skip(1).ToArray();
- var shortArgs = args;
- args = ConvertShortFormToLongForm(args);
- var commandText = LineCommand;
- var specifiedArgs = Args.Where(arg => arg is ScriptArg).Count();
- if (specifiedArgs != args.Length) {
- return $"Command {commandText} expects {specifiedArgs} arguments, but received {shortArgs.Length} instead.";
- }
- return null;
- }
-
- public string Compile(IDataModel model, int start, string scriptLine, LabelLibrary labels, out byte[] result) {
- result = null;
- var error = ErrorCheck(scriptLine, out var tokens);
- if (error != null) return error;
- var args = tokens.Skip(1).ToArray();
- args = ConvertShortFormToLongForm(args);
- var results = new List();
- var specifiedArgIndex = 0;
- for (int i = 0; i < Args.Count; i++) {
- if (Args[i] is ScriptArg scriptArg) {
- var token = args[specifiedArgIndex];
- var message = scriptArg.Build(model, start + results.Count, token, results, labels);
- if (message != null) return message;
- specifiedArgIndex += 1;
- } else if (Args[i] is SilentMatchArg silentArg) {
- results.Add(silentArg.ExpectedValue);
- }
- }
- result = results.ToArray();
- return null;
- }
-
- public void AddDocumentation(string doc) => documentation.Add(doc);
-
- private string[] ConvertShortFormToLongForm(string[] args) {
- if (!hasShortForm) return args;
- // build long-form args from this short form
- var longForm = new List();
- for (int i = 0; i < Args.Count; i++) {
- if (Args[i] is SilentMatchArg) continue;
- var shortIndex = shortIndexFromLongIndex[i];
- if (shortIndex < args.Length) longForm.Add(args[shortIndex]);
- }
- return longForm.ToArray();
- }
-
- private string[] ConvertLongFormToShortForm(string[] args) {
- if (!hasShortForm) return args;
- var shortForm = new Dictionary();
- for (int i = 0; i < Args.Count; i++) {
- if (Args[i] is SilentMatchArg) continue;
- var shortIndex = shortIndexFromLongIndex[i];
- shortForm[shortIndex] = args[shortForm.Count];
- }
- return shortForm.Count.Range(i => shortForm[i]).ToArray();
- }
- }
-
public enum ExpectedPointerType {
Unknown,
Script,
@@ -1249,581 +979,6 @@ namespace HavenSoft.HexManiac.Core.Models.Code {
SpriteTemplate,
}
- public abstract class ScriptLine : IScriptLine {
- private readonly List documentation = new List();
- private readonly IReadOnlyList matchingGames;
-
- public const string Hex = "0123456789ABCDEF";
- public IReadOnlyList Args { get; }
- public IReadOnlyList LineCode { get; }
- public string LineCommand { get; }
- public IReadOnlyList Documentation => documentation;
- public string Usage { get; }
-
- public virtual bool IsEndingCommand { get; }
-
- /// If this line contains pointers, calculate the pointer data's lengths and include here.
- public int CompiledByteLength(IDataModel model, int start, IDictionary destinationLengths) {
- var length = LineCode.Count;
- foreach (var arg in Args) {
- if (arg.Type == ArgType.Pointer) {
- var destination = model.ReadPointer(start + length);
- if (destinationLengths != null && !destinationLengths.ContainsKey(destination)) {
- var argLength = ScriptParser.GetArgLength(model, arg, start + length, destinationLengths);
- if (argLength > 0) destinationLengths[destination] = argLength;
- }
- }
- length += arg.Length(model, start + length);
- }
- return length;
- }
- public int CompiledByteLength(IDataModel model, string line) {
- var length = LineCode.Count;
- var tokens = line.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
- for (var i = 0; i < Args.Count; i++) {
- if (Args[i] is ScriptArg sarg) length += sarg.Length(default, -1);
- if (Args[i] is ArrayArg aarg) length += aarg.ConvertMany(model, tokens.Skip(i)).Count() * aarg.TokenLength + 1;
- }
- return length;
- }
-
- public ScriptLine(string engineLine) {
- var docSplit = engineLine.Split(new[] { '#' }, 2);
- if (docSplit.Length > 1) documentation.Add('#' + docSplit[1]);
- engineLine = docSplit[0].Trim();
- matchingGames = ExtractMatchingGames(ref engineLine);
- Usage = engineLine.Split(new[] { ' ' }, 2).Last();
- var usageTokens = Usage.Split(" ", StringSplitOptions.RemoveEmptyEntries);
- Usage = usageTokens[0] + " " + " ".Join(usageTokens.Skip(1).Select(t => t.Split(".:|<".ToCharArray())[0]));
-
- var tokens = engineLine.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
- var lineCode = new List();
- var args = new List();
-
- foreach (var token in tokens) {
- if (token.Length == 2 && token.All(ViewPort.AllHexCharacters.Contains)) {
- lineCode.Add(byte.Parse(token, NumberStyles.HexNumber));
- } else if (token.StartsWith("[") && token.EndsWith("]")) {
- var content = token.Substring(1, token.Length - 2);
- args.Add(new ArrayArg(content));
- } else if (ScriptArg.IsValidToken(token)) {
- args.Add(new ScriptArg(token));
- } else {
- LineCommand = token;
- }
- }
-
- LineCode = lineCode;
- Args = args;
- }
-
- public static IReadOnlyList ExtractMatchingGames(ref string line) {
- if (!line.StartsWith("[")) return null;
- var gamesEnd = line.IndexOf("]");
- if (gamesEnd == -1) return null;
- var games = line.Substring(1, gamesEnd - 1);
- line = line.Substring(gamesEnd + 1).TrimStart();
- return games.Split("_").Select(ConvertAscii).ToList();
- }
-
- public static IReadOnlyList GetMatchingGames(IScriptLine line) {
- var names = new[] { "AXVE", "AXPE", "BPRE", "BPGE", "BPEE" };
- return names.Where(name => line.MatchesGame(ConvertAscii(name))).ToList();
- }
-
- public static int ConvertAscii(string letters) {
- return letters.Reverse().Aggregate(0, (current, letter) => (current << 8) | (byte)letter);
- }
-
- public bool MatchesGame(int game) => matchingGames?.Contains(game) ?? true;
-
- public void AddDocumentation(string doc) => documentation.Add(doc);
-
- public bool PartialMatchLine(string line) => LineCommand.MatchesPartial(line.Split(' ')[0]);
-
- public bool Matches(int gameCodeHash, IReadOnlyList data, int index) {
- if (index + LineCode.Count >= data.Count) return false;
- if (MatchesGame(gameCodeHash)) {
- var result = true;
- for (int i = 0; result && i < LineCode.Count; i++) result = data[index + i] == LineCode[i]; // avoid making lambda for performance
- return result;
- }
- return false;
- }
-
- public bool CanCompile(string line) {
- if (!(line + " ").StartsWith(LineCommand + " ", StringComparison.CurrentCultureIgnoreCase)) return false;
- if (LineCode.Count == 1) return true;
- var tokens = Tokenize(line).ToList();
- if (tokens.Count < LineCode.Count) return false;
- tokens.RemoveAt(0);
- for (int i = 1; i < LineCode.Count; i++) {
- if (!byte.TryParse(tokens[0], NumberStyles.HexNumber, CultureInfo.CurrentCulture, out var value)) return false;
- if (value != LineCode[i]) return false;
- tokens.RemoveAt(0);
- }
- return true;
- }
-
- public string ErrorCheck(string scriptLine, out string[] tokens) {
- tokens = Tokenize(scriptLine);
- if (!tokens[0].Equals(LineCommand, StringComparison.CurrentCultureIgnoreCase)) throw new ArgumentException($"Command {LineCommand} was expected, but received {tokens[0]} instead.");
- var commandText = LineCommand;
- for (int i = 1; i < LineCode.Count; i++) commandText += " " + LineCode[i].ToString("X2");
- var fillerCount = Args.Count(arg => arg.Name == "filler");
- for (int i = 0; i < fillerCount; i++) {
- if (tokens.Length < Args.Count + LineCode.Count) tokens = tokens.Append("0").ToArray();
- }
- if (Args.Count > 0 && Args.Last() is ArrayArg) {
- if (Args.Count > tokens.Length) {
- return $"Command {commandText} expects {Args.Count} arguments, but received {tokens.Length - LineCode.Count} instead.";
- }
- } else if (Args.Count != tokens.Length - LineCode.Count) {
- return $"Command {commandText} expects {Args.Count} arguments, but received {tokens.Length - LineCode.Count} instead.";
- }
- return null;
- }
-
- public string Compile(IDataModel model, int start, string scriptLine, LabelLibrary labels, out byte[] result) {
- result = null;
- var error = ErrorCheck(scriptLine, out var tokens);
- if (error != null) return error;
- var results = new List(LineCode);
- start += LineCode.Count;
- for (int i = 0; i < Args.Count; i++) {
- if (Args[i] is ScriptArg scriptArg) {
- var token = tokens[i + LineCode.Count];
- var message = scriptArg.Build(model, start, token, results, labels);
- if (message != null) return message;
- start += scriptArg.Length(model, start);
- } else if (Args[i] is ArrayArg arrayArg) {
- var values = arrayArg.ConvertMany(model, tokens.Skip(i + 1)).ToList();
- results.Add((byte)values.Count);
- start += 1;
- foreach (var value in values) {
- if (Args[i].Type == ArgType.Byte) {
- results.Add((byte)value);
- start += 1;
- } else if (Args[i].Type == ArgType.Short) {
- results.Add((byte)value);
- results.Add((byte)(value >> 8));
- start += 2;
- } else if (Args[i].Type == ArgType.Word) {
- results.Add((byte)value);
- results.Add((byte)(value >> 0x8));
- results.Add((byte)(value >> 0x10));
- results.Add((byte)(value >> 0x18));
- start += 4;
- } else {
- throw new NotImplementedException();
- }
- }
- }
- }
- result = results.ToArray();
- return null;
- }
-
- public string Decompile(IDataModel data, int start, DecompileLabelLibrary labels, IList streamTypes) {
- for (int i = 0; i < LineCode.Count; i++) {
- if (LineCode[i] != data[start + i]) throw new ArgumentException($"Data at {start:X6} does not match the {LineCommand} command.");
- }
- var allFillerIsZero = IsAllFillerZero(data, start);
- start += LineCode.Count;
- var builder = new StringBuilder(LineCommand);
- for (int i = 1; i < LineCode.Count; i++) {
- builder.Append(" " + LineCode[i].ToHexString());
- }
-
- var streamContent = new List();
- foreach (var arg in Args) {
- builder.Append(" ");
- if (arg is ScriptArg scriptArg) {
- if (scriptArg.Build(allFillerIsZero, data, start, builder, streamContent, labels, streamTypes)) continue;
- } else if (arg is ArrayArg arrayArg) {
- builder.Append(arrayArg.ConvertMany(data, start));
- } else {
- throw new NotImplementedException();
- }
- start += arg.Length(data, start);
- }
- foreach (var content in streamContent) {
- builder.AppendLine();
- builder.AppendLine("{");
- builder.AppendLine(content);
- builder.Append("}");
- }
- return builder.ToString();
- }
-
- private bool IsAllFillerZero(IDataModel data, int start) {
- start += LineCode.Count;
- foreach (var arg in Args) {
- if (arg.Name == "filler") {
- var value = data.ReadMultiByteValue(start, arg.Length(data, start));
- if (value != 0) return false;
- }
- start += arg.Length(data, start);
- }
- return true;
- }
-
- public static string ReadString(IDataModel data, int start) {
- var length = PCSString.ReadString(data, start, true);
- return data.TextConverter.Convert(data, start, length);
- }
-
- public static string[] Tokenize(string scriptLine) {
- var result = new List();
- var quoteCut = scriptLine.Split('"');
-
- for (int i = 0; i < quoteCut.Length; i++) {
- if (i % 2 == 0 && quoteCut[i].Length == 0) continue;
-
- if (i % 2 == 1) result.Add($"\"{quoteCut[i]}\"");
- else result.AddRange(quoteCut[i].Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries));
- }
-
- return result.ToArray();
- }
-
- public override string ToString() {
- return string.Join(" ", LineCode.Select(code => code.ToHexString()).Concat(Args.Select(arg => arg.Name)).ToArray());
- }
- }
-
- public class XSEScriptLine : ScriptLine {
- public XSEScriptLine(string engineLine) : base(engineLine) { }
-
- public override bool IsEndingCommand => LineCode.Count == 1 && LineCode[0].IsAny(0x02, 0x03, 0x05, 0x08, 0x0A, 0x0C, 0x0D);
- }
-
- public class BSEScriptLine : ScriptLine {
- public BSEScriptLine(string engineLine) : base(engineLine) { }
-
- public override bool IsEndingCommand => LineCode.Count == 1 && LineCode[0].IsAny(0x28, 0x3c, 0x3d, 0x3e, 0x3f);
- }
-
- public class ASEScriptLine : ScriptLine {
- public ASEScriptLine(string engineLine) : base(engineLine) { }
-
- public override bool IsEndingCommand => LineCode.Count == 1 && LineCode[0].IsAny(0x08, 0x0F, 0x11, 0x13);
- }
-
- public class TSEScriptLine : ScriptLine {
- public TSEScriptLine(string engineLine) : base(engineLine) { }
- public override bool IsEndingCommand => LineCode.Count == 1 && LineCode[0].IsAny(0x45, 0x47, 0x59, 0x5A);
- }
-
- public interface IScriptArg {
- ArgType Type { get; }
- ExpectedPointerType PointerType { get; }
- string Name { get; }
- string EnumTableName { get; }
-
- int Length(IDataModel model, int start);
- }
-
- public class ScriptArg : IScriptArg {
- private int length;
-
- public ArgType Type { get; }
- public ExpectedPointerType PointerType { get; }
- public string Name { get; }
- public string EnumTableName { get; }
- public int EnumOffset { get; }
-
- public int Length(IDataModel model, int start) => length;
-
- public ScriptArg(string token) {
- (Type, PointerType, Name, EnumTableName, length) = Construct(token);
- if (EnumTableName == null) return;
- if (EnumTableName.Contains("+")) {
- var parts = EnumTableName.Split(new[] { '+' }, 2);
- EnumTableName = parts[0];
- if (parts[1].TryParseInt(out var result)) EnumOffset = result;
- } else if (EnumTableName.Contains("-")) {
- var parts = EnumTableName.Split(new[] { '-' }, 2);
- EnumTableName = parts[0];
- if (parts[1].TryParseInt(out var result)) EnumOffset = -result;
- }
- }
-
- public static (ArgType type, ExpectedPointerType pointerType, string name, string enumTableName, int length) Construct(string token) {
- if (token.Contains("<>")) {
- var (type, length) = (ArgType.Pointer, 4);
- var name = token.Split(new[] { "<>" }, StringSplitOptions.None).First();
- return (type, ExpectedPointerType.Unknown, name, default, length);
- } else if (token.Contains("<\"\">")) {
- var (type, length) = (ArgType.Pointer, 4);
- var name = token.Split(new[] { "<\"\">" }, StringSplitOptions.None).First();
- return (type, ExpectedPointerType.Text, name, default, length);
- } else if (token.Contains("<`mart`>")) {
- var (type, length) = (ArgType.Pointer, 4);
- var name = token.Split(new[] { "<`mart`>" }, StringSplitOptions.None).First();
- return (type, ExpectedPointerType.Mart, name, default, length);
- } else if (token.Contains("<`decor`>")) {
- var (type, length) = (ArgType.Pointer, 4);
- var name = token.Split(new[] { "<`decor`>" }, StringSplitOptions.None).First();
- return (type, ExpectedPointerType.Decor, name, default, length);
- } else if (token.Contains("<`move`>")) {
- var (type, length) = (ArgType.Pointer, 4);
- var name = token.Split(new[] { "<`move`>" }, StringSplitOptions.None).First();
- return (type, ExpectedPointerType.Movement, name, default, length);
- } else if (token.Contains("<`oam`>")) {
- var (type, length) = (ArgType.Pointer, 4);
- var name = token.Split(new[] { "<`oam`>" }, StringSplitOptions.None).First();
- return (type, ExpectedPointerType.SpriteTemplate, name, default, length);
-
- } else if (token.Contains("<`xse`>")) {
- var (type, length) = (ArgType.Pointer, 4);
- var name = token.Split(new[] { "<`xse`>" }, StringSplitOptions.None).First();
- return (type, ExpectedPointerType.Script, name, default, length);
- } else if (token.Contains("<`bse`>")) {
- var (type, length) = (ArgType.Pointer, 4);
- var name = token.Split(new[] { "<`bse`>" }, StringSplitOptions.None).First();
- return (type, ExpectedPointerType.Script, name, default, length);
- } else if (token.Contains("<`ase`>")) {
- var (type, length) = (ArgType.Pointer, 4);
- var name = token.Split(new[] { "<`ase`>" }, StringSplitOptions.None).First();
- return (type, ExpectedPointerType.Script, name, default, length);
- } else if (token.Contains("<`tse`>")) {
- var (type, length) = (ArgType.Pointer, 4);
- var name = token.Split(new[] { "<`tse`>" }, StringSplitOptions.None).First();
- return (type, ExpectedPointerType.Script, name, default, length);
-
- } else if (token.Contains("::")) {
- var (type, length) = (ArgType.Word, 4);
- var name = token.Split(new[] { "::" }, StringSplitOptions.None).First();
- var enumTableName = token.Split("::").Last();
- return (type, default, name, enumTableName, length);
- } else if (token.Contains(':')) {
- var (type, length) = (ArgType.Short, 2);
- var name = token.Split(':').First();
- var enumTableName = token.Split(':').Last();
- return (type, default, name, enumTableName, length);
- } else if (token.Contains('.')) {
- var (type, length) = (ArgType.Byte, 1);
- var parts = token.Split(new[] { '.' }, 2);
- var name = parts[0];
- var enumTableName = parts[1];
- return (type, default, name, enumTableName, length);
- } else {
- // didn't find a token :(
- // I guess it's a byte?
- var (type, length) = (ArgType.Byte, 1);
- var name = token;
- return (type, default, name, default, length);
- }
- }
-
- public static bool IsValidToken(string token) {
- return "<> <`xse`> <`bse`> <`ase`> <`tse`> <\"\"> <`mart`> <`decor`> <`move`> <`oam`> : .".Split(' ').Any(token.Contains);
- }
-
- public bool FitsInRange(IDataModel model, int address) {
- if (string.IsNullOrEmpty(EnumTableName) || EnumTableName.StartsWith("|")) return true;
- return model.ReadMultiByteValue(address, length) < model.GetOptions(EnumTableName).Count;
- }
-
- private string Convert(IDataModel model, int value, int bytes) {
- var preferHex = EnumTableName?.EndsWith("|h") ?? false;
- var preferSign = EnumTableName?.EndsWith("|z") ?? false;
- var enumName = EnumTableName?.Split('|')[0];
- var table = string.IsNullOrEmpty(enumName) ? null : model.GetOptions(enumName);
- if (table == null || value - EnumOffset < 0 || table.Count <= value - EnumOffset || string.IsNullOrEmpty(table[value])) {
- if (preferHex || value == int.MinValue || Math.Abs(value) >= 0x4000) {
- return "0x" + ((uint)(value - EnumOffset)).ToString($"X{length * 2}");
- } else {
- if (bytes == 1 && preferSign) value = (sbyte)value;
- if (bytes == 2 && preferSign) value = (short)value;
- return (value - EnumOffset).ToString();
- }
- }
- return table[value - EnumOffset];
- }
-
- private string Convert(IDataModel model, string value, out int result) {
- result = 0;
- var parseType = "as a number";
- if (!string.IsNullOrEmpty(EnumTableName)) {
- if (!EnumTableName.StartsWith("|")) parseType = "from " + EnumTableName;
- if (ArrayRunEnumSegment.TryParse(EnumTableName, model, value, out result)) {
- result += EnumOffset;
- return null;
- }
- }
- if (
- value.StartsWith("0x") && value.Substring(2).TryParseHex(out result) ||
- value.StartsWith("0X") && value.Substring(2).TryParseHex(out result) ||
- value.StartsWith("$") && value.Substring(1).TryParseHex(out result) ||
- int.TryParse(value, out result)
- ) {
- result += EnumOffset;
- return null;
- }
- return $"Could not parse '{value}' {parseType}.";
- }
-
- ///
- /// Build from compiled bytes to text.
- ///
- public bool Build(bool allFillerIsZero, IDataModel data, int start, StringBuilder builder, List streamContent, DecompileLabelLibrary labels, IList streamTypes) {
- if (allFillerIsZero && Name == "filler") return true;
- if (Type == ArgType.Byte) builder.Append(Convert(data, data[start], 1));
- if (Type == ArgType.Short) builder.Append(Convert(data, data.ReadMultiByteValue(start, 2), 2));
- if (Type == ArgType.Word) builder.Append(Convert(data, data.ReadMultiByteValue(start, 4), 4));
- if (Type == ArgType.Pointer) {
- var address = data.ReadMultiByteValue(start, 4);
- if (address < 0x8000000) {
- builder.Append(labels.AddressToLabel(address, Type == ArgType.Pointer && PointerType == ExpectedPointerType.Script));
- } else {
- address -= 0x8000000;
- builder.Append($"<{labels.AddressToLabel(address, Type == ArgType.Pointer && PointerType == ExpectedPointerType.Script)}>");
- if (PointerType != ExpectedPointerType.Unknown) {
- if (data.GetNextRun(address) is IStreamRun stream && stream.Start == address) {
- streamContent.Add(stream.SerializeRun());
- streamTypes.Add(PointerType);
- }
- }
- }
- }
- return false;
- }
-
- ///
- /// Build from text to compiled bytes.
- ///
- public string Build(IDataModel model, int address, string token, IList results, LabelLibrary labels) {
- int value;
- if (Type == ArgType.Byte) {
- var error = Convert(model, token, out value);
- if (error != null) return error;
- results.Add((byte)value);
- } else if (Type == ArgType.Short) {
- var error = Convert(model, token, out value);
- if (error != null) return error;
- results.Add((byte)value);
- results.Add((byte)(value >> 8));
- } else if (Type == ArgType.Word) {
- var error = Convert(model, token, out value);
- if (error != null) return error;
- results.Add((byte)value);
- results.Add((byte)(value >> 0x8));
- results.Add((byte)(value >> 0x10));
- results.Add((byte)(value >> 0x18));
- } else if (Type == ArgType.Pointer) {
- if (token.StartsWith("<")) {
- if (!token.EndsWith(">")) return "Unmatched <>";
- token = token.Substring(1, token.Length - 2);
- }
- if (token.StartsWith("0x")) {
- token = token.Substring(2);
- }
- if (token == "auto") {
- if (PointerType == ExpectedPointerType.Script || PointerType == ExpectedPointerType.Unknown) {
- return " only supported for text/data.";
- }
- value = Pointer.NULL + DeferredStreamToken.AutoSentinel;
- } else if (labels.TryResolveLabel(token, out value)) {
- // resolved to an address
- } else if (token.TryParseHex(out value)) {
- // pointer *is* an address: nothing else to do
- if (value > -Pointer.NULL) value += Pointer.NULL;
- // public bool RequireCompleteAddresses { get; set; } = true;
- if (labels.RequireCompleteAddresses && (token.Length < 6 || token.Length > 7)) {
- return "Script addresses must be 6 or 7 characters long.";
- }
- } else if (PointerType != ExpectedPointerType.Script) {
- return $"'{token}' is not a valid pointer.";
- } else {
- labels.AddUnresolvedLabel(token, address);
- value = Pointer.NULL;
- }
- value -= Pointer.NULL;
- results.Add((byte)value);
- results.Add((byte)(value >> 0x8));
- results.Add((byte)(value >> 0x10));
- results.Add((byte)(value >> 0x18));
- } else {
- throw new NotImplementedException();
- }
- return null;
- }
- }
-
- public class SilentMatchArg : IScriptArg {
- public ArgType Type => ArgType.Byte;
- public ExpectedPointerType PointerType => ExpectedPointerType.Unknown;
- public string Name => null;
- public string EnumTableName => null;
- public int EnumOffset => 0;
-
- public int Length(IDataModel model, int start) => 1;
-
- public byte ExpectedValue { get; }
- public SilentMatchArg(byte value) => ExpectedValue = value;
- }
-
- public class ArrayArg : IScriptArg {
- public ArgType Type { get; }
- public string Name { get; }
- public string EnumTableName { get; }
- public int TokenLength { get; }
- public ExpectedPointerType PointerType => ExpectedPointerType.Unknown;
-
- public int Length(IDataModel model, int start) {
- return model[start] * TokenLength + 1;
- }
-
- public ArrayArg(string token) {
- (Type, _, Name, EnumTableName, TokenLength) = ScriptArg.Construct(token);
- }
-
- public string ConvertMany(IDataModel model, int start) {
- var result = new StringBuilder();
- var count = model[start];
- start++;
- for (int i = 0; i < count; i++) {
- var value = model.ReadMultiByteValue(start, TokenLength);
- start += TokenLength;
- var tokenText = "0x" + value.ToString($"X{TokenLength * 2}");
- if (!string.IsNullOrEmpty(EnumTableName)) {
- var table = model.GetOptions(EnumTableName);
- if ((table?.Count ?? 0) > value) {
- tokenText = table[value];
- }
- }
- result.Append(tokenText);
- if (i < count - 1) result.Append(' ');
- }
- return result.ToString();
- }
-
- public IEnumerable ConvertMany(IDataModel model, IEnumerable info) {
- foreach (var token in info) {
- if (string.IsNullOrEmpty(EnumTableName)) {
- if (token.StartsWith("0x") && token.Substring(2).TryParseHex(out var result)) yield return result;
- else if (token.StartsWith("0X") && token.Substring(2).TryParseHex(out result)) yield return result;
- else if (token.StartsWith("$") && token.Substring(1).TryParseHex(out result)) yield return result;
- else if (int.TryParse(token, out result)) yield return result;
- else yield return 0;
- } else if (ArrayRunEnumSegment.TryParse(EnumTableName, model, token, out var enumValue)) {
- yield return enumValue;
- } else {
- yield return 0;
- }
- }
- }
- }
-
- public enum ArgType {
- Byte,
- Short,
- Word,
- Pointer,
- }
-
public static class ScriptExtensions {
public static MacroScriptLine GetMatchingMacro(this IReadOnlyList self, int gameHash, IReadOnlyListdata, int start) {
return (MacroScriptLine)self.FirstOrDefault(option => option is MacroScriptLine && option.Matches(gameHash, data, start));