mirror of
https://github.com/haven1433/HexManiacAdvance.git
synced 2026-05-12 06:55:12 -05:00
243 lines
10 KiB
C#
243 lines
10 KiB
C#
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<ScriptLine> engine;
|
|
|
|
public ScriptParser(IReadOnlyList<ScriptLine> 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<int>();
|
|
var toProcess = new List<int> { 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<byte>();
|
|
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<string>();
|
|
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<ScriptArg> Args { get; }
|
|
public IReadOnlyList<byte> 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<byte>(4, 5, 6, 7);
|
|
|
|
public ScriptLine(string engineLine) {
|
|
var tokens = engineLine.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
|
var lineCode = new List<byte>();
|
|
var args = new List<ScriptArg>();
|
|
|
|
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<byte> 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<byte>(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<byte> 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<ScriptLine> self, IReadOnlyList<byte> data, int start) => self.FirstOrDefault(option => option.Matches(data, start));
|
|
|
|
public static int GetScriptSegmentLength(this IReadOnlyList<ScriptLine> 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;
|
|
}
|
|
}
|
|
}
|