using HavenSoft.HexManiac.Core.Models.Runs; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; namespace HavenSoft.HexManiac.Core.Models.Code { public class ScriptParser { private readonly IReadOnlyList engine; public ScriptParser(IReadOnlyList engine) => this.engine = engine; public int GetScriptSegmentLength(IDataModel model, int address) => engine.GetScriptSegmentLength(model, address); public string Parse(IDataModel data, int start, int length) { var builder = new StringBuilder(); foreach (var line in Decompile(data, start, length)) builder.AppendLine(line); return builder.ToString(); } public void FormatScript(ModelDelta token, IDataModel model, int address) { var processed = new List(); var toProcess = new List { address }; while (toProcess.Count > 0) { address = toProcess.Last(); toProcess.RemoveAt(toProcess.Count - 1); if (processed.Contains(address)) continue; model.ObserveRunWritten(token, new XSERun(address)); int length = 0; while (true) { var line = engine.GetMatchingLine(model, address + length); if (line == null) break; length += line.LineCode.Count; foreach (var arg in line.Args) { if (arg.Type != ArgType.Pointer) { length += arg.Length; continue; } var destination = model.ReadPointer(address + length); if (destination >= 0 && destination < model.Count) { model.ClearFormat(token, address + length, 4); model.ObserveRunWritten(token, new PointerRun(address + length)); if (line.PointsToNextScript) toProcess.Add(destination); } length += arg.Length; } if (line.IsEndingCommand) break; } processed.Add(address); } } public byte[] Compile(string script) { var lines = script.Split(new[] { Environment.NewLine }, StringSplitOptions.None) .Select(line => line.Split('#').First().Trim()) .Where(line => !string.IsNullOrEmpty(line)) .ToArray(); var result = new List(); foreach (var line in lines) { foreach (var command in engine) { if (!(line + " ").StartsWith(command.LineCommand + " ")) continue; result.AddRange(command.Compile(line)); } } return result.ToArray(); } private string[] Decompile(IDataModel data, int index, int length) { var results = new List(); while (length > 0) { var line = engine.FirstOrDefault(option => Enumerable.Range(0, option.LineCode.Count).All(i => data[index + i] == option.LineCode[i])); if (line == null) { results.Add($".raw {data[index]:X2}"); index += 1; length -= 1; } else { results.Add(line.Decompile(data, index)); index += line.CompiledByteLength; length -= line.CompiledByteLength; if (line.IsEndingCommand) break; } } return results.ToArray(); } } public class ScriptLine { public const string Hex = "0123456789ABCDEF"; public IReadOnlyList Args { get; } public IReadOnlyList LineCode { get; } public string LineCommand { get; } public int CompiledByteLength { get; } private static readonly byte[] endCodes = new byte[] { 0x02, 0x03, 0x05, 0x08, 0x0A, 0x0C, 0x0D }; public bool IsEndingCommand { get; } public bool PointsToNextScript => LineCode.Count == 1 && LineCode[0].IsAny(4, 5, 6, 7); public ScriptLine(string engineLine) { 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(Hex.Contains)) { lineCode.Add(byte.Parse(token, NumberStyles.HexNumber)); continue; } if ("<> : .".Split(' ').Any(token.Contains)) { args.Add(new ScriptArg(token)); continue; } LineCommand = token; } LineCode = lineCode; Args = args; CompiledByteLength = LineCode.Count + Args.Sum(arg => arg.Length); IsEndingCommand = LineCode.Count == 1 && endCodes.Contains(LineCode[0]); } public bool Matches(IReadOnlyList data, int index) { if (index + LineCode.Count >= data.Count) return false; return Enumerable.Range(0, LineCode.Count).All(i => data[index + i] == LineCode[i]); } public byte[] Compile(string scriptLine) { var tokens = scriptLine.Split(new[] { " " }, StringSplitOptions.None); if (tokens[0] != LineCommand) throw new ArgumentException($"Command {LineCommand} was expected, but received {tokens[0]} instead."); if (Args.Count != tokens.Length - 1) throw new ArgumentException($"Command {LineCommand} expects {Args.Count} arguments, but received {tokens.Length - 1} instead."); var results = new List(LineCode); for (int i = 0; i < Args.Count; i++) { var token = tokens[i + 1]; if (Args[i].Type == ArgType.Byte) { results.Add(byte.Parse(token, NumberStyles.HexNumber)); } else if (Args[i].Type == ArgType.Short) { var value = short.Parse(token, NumberStyles.HexNumber); results.Add((byte)value); results.Add((byte)(value >> 8)); } else if (Args[i].Type == ArgType.Word) { var value = int.Parse(token, NumberStyles.HexNumber); results.Add((byte)value); results.Add((byte)(value >> 0x8)); results.Add((byte)(value >> 0x10)); results.Add((byte)(value >> 0x18)); } else if (Args[i].Type == ArgType.Pointer) { int value; if (token.StartsWith("<") && token.EndsWith(">")) { value = int.Parse(token.Substring(1, token.Length - 2), NumberStyles.HexNumber); value += 0x8000000; } else { value = int.Parse(token, NumberStyles.HexNumber); } results.Add((byte)value); results.Add((byte)(value >> 0x8)); results.Add((byte)(value >> 0x10)); results.Add((byte)(value >> 0x18)); } else { throw new NotImplementedException(); } } return results.ToArray(); } public string Decompile(IDataModel data, int start) { 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."); } start += LineCode.Count; var builder = new StringBuilder(LineCommand); foreach (var arg in Args) { builder.Append(" "); if (arg.Type == ArgType.Byte) builder.Append($"{data[start]:X2}"); if (arg.Type == ArgType.Short) builder.Append($"{data.ReadMultiByteValue(start, 2):X4}"); if (arg.Type == ArgType.Word) builder.Append($"{data.ReadMultiByteValue(start, 4):X8}"); if (arg.Type == ArgType.Pointer) { var address = data.ReadMultiByteValue(start, 4); if (address < 0x8000000) builder.Append(address.ToString("X6")); else builder.Append($"<{address - 0x8000000:X6}>"); } start += arg.Length; } return builder.ToString(); } public static string ReadString(IReadOnlyList data, int start) { var length = PCSString.ReadString(data, start, true); return PCSString.Convert(data, start, length); } } public class ScriptArg { public ArgType Type { get; } public string Name { get; } public string EnumTableName { get; } public int Length { get; } public ScriptArg(string token) { if (token.Contains("<>")) { (Type, Length) = (ArgType.Pointer, 4); Name = token.Split(new[] { "<>" }, StringSplitOptions.None).First(); } else if (token.Contains(".")) { (Type, Length) = (ArgType.Byte, 1); Name = token.Split('.').First(); EnumTableName = token.Split('.').Last(); } else if (token.Contains("::")) { (Type, Length) = (ArgType.Word, 4); Name = token.Split(new[] { "::" }, StringSplitOptions.None).First(); } else if (token.Contains(":")) { (Type, Length) = (ArgType.Short, 2); Name = token.Split(':').First(); EnumTableName = token.Split(':').Last(); } else { // didn't find a token :( // I guess it's a byte? (Type, Length) = (ArgType.Byte, 1); Name = token; } } } public enum ArgType { Byte, Short, Word, Pointer, } public static class ScriptExtensions { public static ScriptLine GetMatchingLine(this IReadOnlyList self, IReadOnlyList data, int start) => self.FirstOrDefault(option => option.Matches(data, start)); public static int GetScriptSegmentLength(this IReadOnlyList self, IDataModel model, int address) { int length = 0; while (true) { var line = self.GetMatchingLine(model, address + length); if (line == null) break; length += line.CompiledByteLength; if (line.IsEndingCommand) break; } return length; } } }