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; private set; } 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; ShortFormArgs ??= Args.Where(arg => arg is not SilentMatchArg).ToList(); } /// /// Once the short form is exatracted, the long form is left behind. /// So either way, engineLine is now just the long form. /// 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 // entries in long might not have any exact matches if the short-argument is multiple bytes and is split into multiple single-byte arguments in the long arguments. for (int i = 0; i < longTokens.Length; i++) { var index = shortTokens.IndexOf(longTokens[i]); if (index == -1) continue; shortIndexFromLongIndex.Add(i, index); } // NOTE: short form args do not permit array args, such as with the animation scripts ShortFormArgs = shortTokens.Select(token => new ScriptArg(token)).ToList(); 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; var expectedVariableValues = new Dictionary(); 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) { // if the argument is duplicated through multiple spots, // make sure all the spots match the same value. var matchingShortArg = ShortFormArgs.FirstOrDefault(shortArg => shortArg.Name == arg.Name); var argLength = arg.Length(default, -1); if ((matchingShortArg?.Length(default, -1) ?? 0) == argLength) { var value = data.ReadMultiByteValue(index, argLength); if (!expectedVariableValues.TryGetValue(sarg.Name, out var expectedValue)) expectedVariableValues[sarg.Name] = value; else if (expectedValue != value) return false; // only match the macro if all the variables match } } 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(); var shiftNames = new Dictionary(); var carryNames = new Dictionary(); int i = 0; foreach (var arg in Args) { if (arg is ScriptArg sarg) { var tempBuilder = new StringBuilder(); int shift = 0, carry = 0; var argLength = sarg.Length(data, start); if (!shortIndexFromLongIndex.TryGetValue(i, out var shortIndex)) { var shortArg = ShortFormArgs.FirstOrDefault(arg => arg.Name == sarg.Name); if (shortArg != null && shortArg.Length(data, start) != argLength) { if (!carryNames.TryGetValue(sarg.Name, out carry)) carry = 0; if (!shiftNames.TryGetValue(sarg.Name, out shift)) shift = 0; shiftNames[sarg.Name] = shift + 8 * argLength; carryNames[sarg.Name] = carry + (data.ReadMultiByteValue(start, argLength) << shift); } } sarg.Build(false, data, start, tempBuilder, streamContent, shift, carry, labels, streamTypes); args.Add(tempBuilder.ToString()); i += 1; } 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; var shiftNames = new Dictionary(); for (int i = 0; i < Args.Count; i++) { if (Args[i] is ScriptArg scriptArg) { int shift = 0; if (!shortIndexFromLongIndex.ContainsKey(i)) { // the arguments length doesn't match the short version. // pull the current shift if (!shiftNames.TryGetValue(scriptArg.Name, out shift)) shift = 0; shiftNames[scriptArg.Name] = shift + scriptArg.Length(model, start + results.Count) * 8; } var token = args[specifiedArgIndex]; var message = scriptArg.Build(model, start + results.Count, token, shift, 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 || args.Length == 0) 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; if (shortIndexFromLongIndex.TryGetValue(i, out var shortIndex)) { if (shortIndex < args.Length) longForm.Add(args[shortIndex]); } else { var shortformIndex = ShortFormArgs.Count.Range().FirstOrDefault(i => ShortFormArgs[i].Name == Args[i].Name); longForm.Add(args[shortformIndex]); } } return longForm.ToArray(); } private string[] ConvertLongFormToShortForm(string[] args) { if (!hasShortForm) return args; var shortForm = new Dictionary(); int j = 0; for (int i = 0; i < Args.Count; i++) { if (Args[i] is SilentMatchArg) continue; if (shortIndexFromLongIndex.TryGetValue(i, out var shortIndex)) { shortForm[shortIndex] = args[j]; // this prefers to use the last instance of the argument, if the argument appears multiple times. Needs to work this way for split-byte args. } else { var shortformIndex = ShortFormArgs.Count.Range().FirstOrDefault(i => ShortFormArgs[i].Name == Args[i].Name); shortForm[shortformIndex] = args[j]; } j += 1; } 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 + 1)).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, 0xef, 0xf6, 0xf7); } 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); } }