From 5dc7eaecda14e4b0efe2de164aa0541342cc07c4 Mon Sep 17 00:00:00 2001 From: haven1433 Date: Sun, 1 Oct 2023 12:12:10 -0500 Subject: [PATCH] refactor ScriptParser.cs is getting too long * move ScriptLine and related types to their own file * move ScriptArg and related types to their own file --- src/HexManiac.Core/Models/Code/ScriptArg.cs | 320 +++++++ src/HexManiac.Core/Models/Code/ScriptLine.cs | 544 +++++++++++ .../Models/Code/ScriptParser.cs | 845 ------------------ 3 files changed, 864 insertions(+), 845 deletions(-) create mode 100644 src/HexManiac.Core/Models/Code/ScriptArg.cs create mode 100644 src/HexManiac.Core/Models/Code/ScriptLine.cs 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));