HexManiacAdvance/src/HexManiac.Core/Models/Code/ScriptParser.cs
haven1433 f14b44eb5e decompiled scripts now have sections appearing in address order
instead of labeling sections based on what address calls them, label section based on what order they appear in memory.
2023-07-01 21:40:59 -05:00

1769 lines
84 KiB
C#

using HavenSoft.HexManiac.Core.Models.Runs;
using HavenSoft.HexManiac.Core.Models.Runs.Factory;
using HavenSoft.HexManiac.Core.ViewModels;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels.Tools;
using Microsoft.Scripting.Utils;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
// TODO consolidate script length logic:
// ScriptExtensions.GetScriptSegmentLength
// ScriptParser.CollectScripts
// ScriptParser.FindLength
namespace HavenSoft.HexManiac.Core.Models.Code {
public class ScriptParser {
public const int MaxRepeates = 20;
private readonly IReadOnlyList<IScriptLine> engine;
private readonly byte endToken;
private int gameHash;
public bool RequireCompleteAddresses { get; set; } = true;
public event EventHandler<string> CompileError;
public ScriptParser(int gameHash, IReadOnlyList<IScriptLine> engine, byte endToken) {
(this.engine, this.endToken) = (engine, endToken);
this.gameHash = gameHash;
}
public void RefreshGameHash(IDataModel model) => gameHash = model.GetShortGameCode();
public int GetScriptSegmentLength(IDataModel model, int address) => engine.GetScriptSegmentLength(gameHash, model, address, new Dictionary<int, int>());
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 List<int> CollectScripts(IDataModel model, int address) {
// do some basic validation to make sure this is actually a reasonable thing to decode
var currentRun = model.GetNextRun(address);
var scripts = new List<int>();
if (address < 0 || address >= model.Count) return scripts;
if (currentRun.Start < address) return scripts;
if (currentRun.Start == address && !(currentRun is IScriptStartRun) && !(currentRun is NoInfoRun)) return scripts;
scripts.Add(address);
var lengths = new List<int>();
for (int i = 0; i < scripts.Count; i++) {
while (i < scripts.Count && i.Range().Any(j => scripts[j] <= scripts[i] && scripts[i] < scripts[j] + lengths[j])) scripts.RemoveAt(i);
if (i == scripts.Count) break;
address = scripts[i];
int length = 0;
var destinations = new Dictionary<int, int>();
int lastCommand = -1, repeateLength = 0;
while (true) {
var line = engine.GetMatchingLine(gameHash, model, address + length);
if (line == null) break;
// normally we would just use this,
// but we want to track all the pointers that go to scripts separately.
// so just use this to get the child-sizes, so we can adjust the length after
line.CompiledByteLength(model, address + length, destinations);
length += line.LineCode.Count;
foreach (var arg in line.Args) {
if (arg.Type == ArgType.Pointer) {
var destination = model.ReadPointer(address + length);
if (destination >= 0 && destination < model.Count &&
arg.PointerType == ExpectedPointerType.Script &&
!scripts.Contains(destination) &&
i.Range().All(j => destination < scripts[j] || scripts[j] + lengths[j] <= destination)
) {
scripts.Add(destination);
}
}
length += arg.Length(model, address + length);
}
if (line.IsEndingCommand) break;
if (line.LineCode[0] != lastCommand) (lastCommand, repeateLength) = (line.LineCode[0], 1);
else repeateLength += 1;
if (line.Args.Count > 0) repeateLength = 0;
if (repeateLength > MaxRepeates) break; // same command lots of times in a row is fishy
}
// append child scripts that come directly after this script
while (true) {
// child script starts directly after this script and has only one source
if (destinations.TryGetValue(address + length, out int childLength) && childLength > 0) {
var anchor = model.GetNextAnchor(address + length);
if (anchor.Start == address + length && anchor.PointerSources.Count == 1) {
length += childLength;
continue;
}
}
// child script has a 1-byte margin (probably an end after a goto) and has only one source
if (destinations.TryGetValue(address + length + 1, out childLength)) {
// there was a skip... should we ignore it?
// If something points to that position, we can't keep going.
var anchor = model.GetNextAnchor(address + length);
if (anchor.Start == address + length && anchor.PointerSources.Count > 0) break;
anchor = model.GetNextAnchor(address + length + 1);
if (anchor.Start == address + length + 1 && anchor.PointerSources.Count == 1) {
length += childLength + 1;
continue;
}
}
break;
}
lengths.Add(length);
for (int j = lengths.Count - 1; j >= 0; j--) {
if (scripts[j] <= address || scripts[j] >= address + length) continue;
lengths.RemoveAt(j);
scripts.RemoveAt(j);
i--;
}
// look for other scripts from destinations that we should care about but don't yet
foreach (var d in destinations.Keys) {
if (scripts.Contains(d)) continue; // destination is already in queue
if (model.GetNextRun(d) is not IScriptStartRun scriptStart || scriptStart.Start != d) continue; // destination isn't formatted as a script
if (lengths.Count().Range().Any(j => scripts[j] <= d && d < scripts[j] + lengths[j])) continue; // destination is included within exsting script
scripts.Add(d);
}
}
return scripts;
}
public int FindLength(IDataModel model, int address, Dictionary<int, int> destinations = null) {
if (destinations == null) destinations = model.CurrentCacheScope.ScriptDestinations(address);
int length = 0;
int consecutiveNops = 0;
int lastCommand = -1, repeateLength = 1;
while (true) {
var line = engine.GetMatchingLine(gameHash, model, address + length);
if (line == null) break;
consecutiveNops = line.LineCommand.StartsWith("nop") ? consecutiveNops + 1 : 0;
if (consecutiveNops > 16) return 0;
length += line.CompiledByteLength(model, address + length, destinations);
if (line.IsEndingCommand) break;
if (line.LineCode[0] != lastCommand) (lastCommand, repeateLength) = (line.LineCode[0], 1);
else repeateLength += 1;
if (line.Args.Count > 0) repeateLength = 0;
if (repeateLength > ScriptParser.MaxRepeates) break; // same command lots of times in a row is fishy
}
// Include in the length any content that comes directly (or +1) after the script.
// This content should be considered part of the script.
// Only do this if the content is only referenced once, otherwise it may not be safe to include during repoints.
while (true) {
if (destinations.TryGetValue(address + length, out int additionalLength) && additionalLength > 0) {
var anchor = model.GetNextAnchor(address + length);
if (anchor.Start == address + length && anchor.PointerSources.Count == 1) {
length += additionalLength;
continue;
}
}
if (destinations.TryGetValue(address + length + 1, out additionalLength)) {
// there was a skip... should we ignore it?
// If something points to that position, we can't keep going.
var anchor = model.GetNextAnchor(address + length);
if (anchor.Start == address + length && anchor.PointerSources.Count > 0) break;
anchor = model.GetNextAnchor(address + length + 1);
if (anchor.Start == address + length + 1 && anchor.PointerSources.Count == 1) {
length += additionalLength + 1;
continue;
}
}
break;
}
return length;
}
public MacroScriptLine GetMacro(IDataModel model, int address) => engine.GetMatchingMacro(gameHash, model, address);
public ScriptLine GetLine(IDataModel model, int address) => engine.GetMatchingLine(gameHash, model, address);
public IEnumerable<IScriptLine> DependsOn(string basename) {
foreach (var line in engine) {
foreach (var arg in line.Args) {
if (arg.EnumTableName == basename) {
yield return line;
break;
}
}
}
}
private HashSet<string> constantCache, keywordCache;
public void AddKeywords(IDataModel model, CodeBody body) {
var editor = body.Editor;
editor.LineCommentHeader = "#";
editor.MultiLineCommentHeader = "/*";
editor.MultiLineCommentFooter = "*/";
if (constantCache == null || keywordCache == null) {
constantCache = new HashSet<string>();
keywordCache = new HashSet<string>();
foreach (var line in engine) {
keywordCache.Add(line.LineCommand);
foreach (var arg in line.Args) {
if (string.IsNullOrEmpty(arg.EnumTableName)) continue;
foreach (var option in model.GetOptions(arg.EnumTableName)) {
if (option == null) continue;
if ("<>!=?".Any(option.Contains)) continue;
constantCache.Add(option);
}
}
}
}
editor.Constants.Clear();
editor.Constants.AddRange(constantCache);
editor.Keywords.Clear();
editor.Keywords.AddRange(keywordCache);
editor.Keywords.Add("auto"); // for the auto-pointer feature
}
public void ClearConstantCache() {
constantCache = null;
}
// TODO refactor to rely on CollectScripts rather than duplicate code
// returns a list of scripts that were formatted, and their length
public IDictionary<int, int> FormatScript<SERun>(ModelDelta token, IDataModel model, int address) where SERun : IScriptStartRun {
Func<int, SortedSpan<int>, IScriptStartRun> constructor = (a, s) => new XSERun(a, s);
if (typeof(SERun) == typeof(BSERun)) constructor = (a, s) => new BSERun(a, s);
if (typeof(SERun) == typeof(ASERun)) constructor = (a, s) => new ASERun(a, s);
if (typeof(SERun) == typeof(TSERun)) constructor = (a, s) => new TSERun(a, s);
var processed = new Dictionary<int, int>();
var toProcess = new List<int> { address };
while (toProcess.Count > 0) {
address = toProcess.Last();
toProcess.RemoveAt(toProcess.Count - 1);
var existingRun = model.GetNextRun(address);
// We need to check for an anchor here _before_ checking if this is a duplicate address and therefore skippable.
// Because self-referential scripts won't have their anchor picked up on the initial run.
if (!(existingRun is SERun) && existingRun.Start == address) {
var anchorName = model.GetAnchorFromAddress(-1, address);
model.ObserveAnchorWritten(token, anchorName, constructor(address, existingRun.PointerSources));
}
if (processed.ContainsKey(address)) continue;
int length = 0;
while (true) {
var line = engine.GetMatchingLine(gameHash, model, address + length);
if (line == null) break;
// there may've previously been a pointer here: the code has changed!
if (model.GetNextRun(address + length) is PointerRun pRun && pRun.Start == address + length) model.ClearFormat(token, address + length, line.LineCode.Count);
length += line.LineCode.Count;
foreach (var arg in line.Args) {
if (arg.Type != ArgType.Pointer) {
// there may've previously been a pointer here: the code has changed!
// when clearing args, we actually _do_ want to clear any anchors that point to them.
var start = address + length;
var argLength = arg.Length(model, start);
if (model.GetNextRun(start) is ITableRun tableRun && tableRun.Start == start && tableRun.Length == argLength) {
// no need to clear
} else {
model.ClearFormat(token, start, argLength);
}
} else {
var destination = model.ReadPointer(address + length);
if (destination.InRange(0, model.Count) && !destination.InRange(address + length, address + length + 4)) {
var destinationRun = model.GetNextRun(destination);
if (destinationRun.Start < destination) {
// we're trying to point into already formatted data
// don't add the pointer source or destination
} else {
var pointerSource = model.GetNextRun(address + length);
if (pointerSource is PointerRun pointerRun && pointerRun.Start == address + length) {
// no need to clear/update
} else {
model.ClearFormat(token, address + length, 4);
model.ObserveRunWritten(token, new PointerRun(address + length));
}
if (arg.PointerType == ExpectedPointerType.Script) toProcess.Add(destination);
if (arg.PointerType == ExpectedPointerType.Text) {
WriteTextStream(model, token, destination, address + length);
} else if (arg.PointerType == ExpectedPointerType.Movement) {
WriteMovementStream(model, token, destination, address + length);
} else if (arg.PointerType == ExpectedPointerType.Mart) {
WriteMartStream(model, token, destination, address + length);
} else if (arg.PointerType == ExpectedPointerType.Decor) {
WriteDecorStream(model, token, destination, address + length);
} else if (arg.PointerType == ExpectedPointerType.SpriteTemplate) {
WriteSpriteTemplateStream(model, token, destination, address + length);
}
}
}
}
length += arg.Length(model, address + length);
}
if (line.IsEndingCommand) break;
}
processed.Add(address, length);
}
return processed;
}
private void WriteTextStream(IDataModel model, ModelDelta token, int destination, int source) {
if (destination < 0 || destination > model.Count) return;
var destinationLength = PCSString.ReadString(model, destination, true);
if (destinationLength > 0) {
// if there's an anchor that starts exactly here, we don't want to clear it: just update it
// if the run starts somewhere else, we better to a clear to prevent conflicting formats
var existingTextRun = model.GetNextRun(destination);
if (existingTextRun.Start != destination) {
model.ClearFormat(token, destination, destinationLength);
model.ObserveRunWritten(token, new PCSRun(model, destination, destinationLength, SortedSpan.One(source)));
} else if (existingTextRun is PCSRun && destinationLength == existingTextRun.Length) {
// length and format is correct, nothing to do
} else {
model.ClearAnchor(token, destination + existingTextRun.Length, destinationLength - existingTextRun.Length); // assuming that the old run ends before the new run, clear the difference
model.ObserveRunWritten(token, new PCSRun(model, destination, destinationLength, SortedSpan.One(source)));
model.ClearAnchor(token, destination + destinationLength, existingTextRun.Length - destinationLength); // assuming that the new run ends before the old run, clear the difference
}
}
}
private void WriteMovementStream(IDataModel model, ModelDelta token, int start, int source) {
var format = "[move.movementtypes]!FE";
WriteStream(model, token, start, source, format);
}
public void WriteMartStream(IDataModel model, ModelDelta token, int start, int source) {
var format = $"[item:{HardcodeTablesModel.ItemsTableName}]!0000";
WriteStream(model, token, start, source, format);
}
public void WriteDecorStream(IDataModel model, ModelDelta token, int start, int source) {
var format = $"[item:{HardcodeTablesModel.DecorationsTableName}]!0000";
WriteStream(model, token, start, source, format);
}
private void WriteSpriteTemplateStream(IDataModel model, ModelDelta token, int start, int source) {
var format = "[tileTag: paletteTag: oam<> anims<> images<> affineAnims<> callback<>]1";
WriteStream(model, token, start, source, format);
}
private void WriteStream(IDataModel model, ModelDelta token, int start, int source, string format) {
var sources = source >= 0 ? SortedSpan.One(source) : SortedSpan<int>.None;
TableStreamRun.TryParseTableStream(model, start, sources, string.Empty, format, null, out var tsRun);
if (tsRun != null) {
var endTokenLength = tsRun.Length - tsRun.ElementLength * tsRun.ElementCount;
if (endTokenLength > 0 && model.IsFreespace(start, endTokenLength) && token is not NoDataChangeDeltaModel) {
// freespace: write the end token
model.ClearFormat(token, tsRun.Start, endTokenLength);
tsRun = tsRun.DeserializeRunFromZero(string.Empty, token, out var _, out var _);
model.ObserveRunWritten(token, tsRun);
} else if (model.GetNextRun(tsRun.Start) is ITableRun existingRun && existingRun.Start == tsRun.Start && tsRun.DataFormatMatches(existingRun)) {
// no need to update the format, the format already matches what we want
} else {
model.ClearFormat(token, tsRun.Start, tsRun.Length);
if (tsRun.ElementCount == 0) {
// write the end token
tsRun.DeserializeRun(string.Empty, token, out var _, out var _);
}
model.ObserveRunWritten(token, tsRun);
}
}
}
private record StreamInfo(ExpectedPointerType PointerType, int Source, int Destination);
public static int InsertMissingClosers(ref string script, int caret) {
int caretMove = 0;
bool isStartOfLine = true;
var text = script.ToList();
for (int i = 0; i < text.Count; i++) {
if (text[i] == '\n' || text[i] == '\r') isStartOfLine = true;
else if (text[i] != ' ' && text[i] != '{') isStartOfLine = false;
if (text[i] != '{') continue;
if (!isStartOfLine) {
text.Insert(i, '\r');
text.Insert(i + 1, '\n');
if (i <= caret) { caret += 2; }
i += 2;
}
isStartOfLine = true;
var close = -1;
for (int j = i + 1; j < text.Count && text[j] != '{'; j++) {
if (text[j] == '\n' || text[j] == '\r') isStartOfLine = true;
else if (text[j] != ' ' && text[j] != '}') isStartOfLine = false;
if (text[j] != '}') continue;
if (!isStartOfLine) {
text.Insert(j, '\r');
text.Insert(j + 1, '\n');
if (j < caret) { caret += 2; caretMove += 2; }
j += 2;
}
close = j;
break;
}
if (close == -1) {
if (i == caret) {
text.Insert(i + 1, '\r');
text.Insert(i + 2, '\n');
}
text.Insert(i + 3, '}');
close = i + 3;
if (i == caret) caretMove -= 3;
// check for excess blank lines after the lines we just inserted
if (close < text.Count - 4 && text[close + 1] == '\r' && text[close + 2] == '\n' && text[close + 3] == '\r' && text[close + 4] == '\n') {
text.RemoveAt(close + 1);
text.RemoveAt(close + 1);
}
}
if (i + 1 < text.Count && text[i + 1] != '\r') {
text.Insert(i + 1, '\r');
text.Insert(i + 2, '\n');
if (i < caret) { caret += 2; caretMove += 2; }
close += 2;
}
if (close + 1 < text.Count && text[close + 1] != '\r') {
text.Insert(close + 1, '\r');
text.Insert(close + 2, '\n');
if (close < caret) { caret += 2; }
isStartOfLine = true;
close += 2;
}
// special case: no blank line between open and close
if (close == i + 3) {
text.Insert(i + 1, '\r');
text.Insert(i + 2, '\n');
if (i < caret) { caret += 2; caretMove -= 2; }
close += 2;
}
i = close;
}
script = new string(text.ToArray());
return caretMove;
}
public byte[] Compile(ModelDelta token, IDataModel model, int start, ref string script, out IReadOnlyList<(int originalLocation, int newLocation)> movedData, out int ignoreCharacterCount) {
int ignoreCaret = 0;
return Compile(token, model, start, ref script, ref ignoreCaret, out movedData, out ignoreCharacterCount);
}
/// <summary>
/// Potentially edits the script text and returns a set of data repoints.
/// The data is moved, but the script itself has not written by this method.
/// </summary>
/// <param name="movedData">Related data runs that moved during compilation.</param>
/// <param name="ignoreCharacterCount">Number of new characters added that should be ignored by the caret.</param>
/// <returns></returns>
public byte[] Compile(ModelDelta token, IDataModel model, int start, ref string script, ref int caret, out IReadOnlyList<(int originalLocation, int newLocation)> movedData, out int ignoreCharacterCount) {
ignoreCharacterCount = 0;
movedData = new List<(int, int)>();
var deferredContent = new List<DeferredStreamToken>();
int adjustCaret = InsertMissingClosers(ref script, caret);
caret += adjustCaret;
var lines = script.Split(new[] { '\n', '\r' }, StringSplitOptions.None)
.Select(line => line.Split('#').First())
.Where(line => !string.IsNullOrWhiteSpace(line))
.ToArray();
var result = new List<byte>();
var streamInfo = new List<StreamInfo>();
var labels = ExtractLocalLabels(model, start, lines);
bool lastCommandIsEndCommand = false;
for (var i = 0; i < lines.Length; i++) {
var line = lines[i].Trim();
if (line.EndsWith(":")) continue; // label, not code. Don't parse.
if (line == "{") {
var streamStart = i + 1;
var indentCount = 1;
i += 1;
while (indentCount > 0 && lines.Length > i) {
line = lines[i].Trim();
if (line == "{") indentCount += 1;
if (line == "}") indentCount -= 1;
i += 1;
}
i -= 1;
var streamEnd = i;
var streamLines = lines.Skip(streamStart).Take(streamEnd - streamStart).ToList();
var stream = Environment.NewLine.Join(streamLines);
// Let the stream run handle updating itself based on the stream content.
if (streamInfo.Count > 0) {
var info = streamInfo[0];
if (info.Destination == DeferredStreamToken.AutoSentinel + Pointer.NULL) {
var deferred = deferredContent[deferredContent.Count - streamInfo.Count];
deferred.UpdateContent(model, info.PointerType, stream);
} else if (model.GetNextRun(info.Destination) is IStreamRun streamRun && streamRun.Start == info.Destination) {
var newStreamRun = streamRun.DeserializeRun(stream, token, out var _, out var _); // we don't notify parents/children based on script-stream changes: we know they never have parents/children.
// alter script content and compiled byte location based on stream move
if (newStreamRun.Start != info.Destination) {
script = script.Replace(info.Destination.ToAddress(), newStreamRun.Start.ToAddress());
result[info.Source - start + 0] = (byte)(newStreamRun.Start >> 0);
result[info.Source - start + 1] = (byte)(newStreamRun.Start >> 8);
result[info.Source - start + 2] = (byte)(newStreamRun.Start >> 16);
result[info.Source - start + 3] = (byte)((newStreamRun.Start >> 24) + 0x08);
((List<(int, int)>)movedData).Add((info.Destination, newStreamRun.Start));
}
if (newStreamRun != streamRun) model.ObserveRunWritten(token, newStreamRun);
}
streamInfo.RemoveAt(0);
}
continue;
}
streamInfo.Clear();
var command = FirstMatch(line);
if (command != null) {
var currentSize = result.Count;
if (line.Contains("<??????>")) {
int newAddress = -1;
if (command.Args.Any(arg => arg.PointerType == ExpectedPointerType.Movement)) {
if (model.FreeSpaceStart == start) model.FreeSpaceStart += model.FreeSpaceBuffer;
newAddress = model.FindFreeSpace(0, 0x10);
token.ChangeData(model, newAddress, 0xFE);
WriteMovementStream(model, token, newAddress, -1);
} else if (command.Args.Any(arg => arg.PointerType == ExpectedPointerType.Text)) {
newAddress = model.FindFreeSpace(0, 0x10);
if (newAddress == -1) {
var endLength = model.Count;
model.ExpandData(token, endLength + 0x10);
newAddress = endLength + 0x10;
}
token.ChangeData(model, newAddress, 0xFF);
model.ObserveRunWritten(token, new PCSRun(model, newAddress, 1));
} else if (command.Args.Any(arg => arg.PointerType == ExpectedPointerType.Mart)) {
newAddress = model.FindFreeSpace(0, 0x10);
token.ChangeData(model, newAddress, 0x00);
token.ChangeData(model, newAddress + 1, 0x00);
WriteMartStream(model, token, newAddress, -1);
} else if (command.Args.Any(arg => arg.PointerType == ExpectedPointerType.Decor)) {
newAddress = model.FindFreeSpace(0, 0x10);
token.ChangeData(model, newAddress, 0x00);
token.ChangeData(model, newAddress + 1, 0x00);
WriteDecorStream(model, token, newAddress, -1);
} else if (command.Args.Any(arg => arg.PointerType == ExpectedPointerType.SpriteTemplate)) {
newAddress = model.FindFreeSpace(0, 0x18);
for (int j = 0; j < 0x18; j++) token.ChangeData(model, newAddress + j, 0x00);
WriteSpriteTemplateStream(model, token, newAddress, -1);
} else if (command.Args.Any(arg => arg.PointerType == ExpectedPointerType.Script)) {
newAddress = model.FindFreeSpace(0, 0x10);
token.ChangeData(model, newAddress, endToken);
// TODO write a new script template... an anchor with a format based on the current script
}
if (newAddress != -1) {
var originalLine = line;
line = line.ReplaceOne("<??????>", $"<{newAddress:X6}>");
if (command.Args.Any(arg => arg.PointerType == ExpectedPointerType.Script)) {
script = script.ReplaceOne("<??????>", $"<{newAddress:X6}>");
} else if (script.IndexOf(originalLine) != script.IndexOf($"{originalLine}{Environment.NewLine}{{{Environment.NewLine}")) {
script = script.ReplaceOne(originalLine, $"{line}{Environment.NewLine}{{{Environment.NewLine}}}");
ignoreCharacterCount += Environment.NewLine.Length * 2 + 2;
} else {
script = script.ReplaceOne("<??????>", $"<{newAddress:X6}>");
}
}
}
var error = command.Compile(model, start + currentSize, line, labels, out var code);
if (error == null) {
result.AddRange(code);
} else {
CompileError?.Invoke(this, i + ": " + error);
return null;
}
var pointerOffset = command.LineCode.Count;
foreach (var arg in command.Args) {
if (arg.Type == ArgType.Pointer && arg.PointerType != ExpectedPointerType.Unknown) {
var destination = result.ReadMultiByteValue(currentSize + pointerOffset, 4) + Pointer.NULL;
if (destination == DeferredStreamToken.AutoSentinel + Pointer.NULL) {
streamInfo.Add(new(arg.PointerType, start + currentSize + pointerOffset, destination));
(string format, byte[] defaultContent) = arg.PointerType switch {
ExpectedPointerType.Text => ("\"\"", new byte[] { 0xFF }),
ExpectedPointerType.Mart => ($"[item:{HardcodeTablesModel.ItemsTableName}]!0000", new byte[] { 0, 0 }),
ExpectedPointerType.Decor => ($"[item:{HardcodeTablesModel.DecorationsTableName}]!0000", new byte[] { 0, 0 }),
ExpectedPointerType.Movement => ($"[move.movementtypes]!FE", new byte[] { 0xFE }),
_ => ("^", new byte[0])
};
deferredContent.Add(new(currentSize + pointerOffset, format, defaultContent));
} else if (destination >= 0 && arg.PointerType != ExpectedPointerType.Script) {
streamInfo.Add(new(arg.PointerType, start + currentSize + pointerOffset, destination));
if (arg.PointerType == ExpectedPointerType.Text) {
WriteTextStream(model, token, destination, start + currentSize + pointerOffset);
} else if (arg.PointerType == ExpectedPointerType.Movement) {
WriteMovementStream(model, token, destination, start + currentSize + pointerOffset);
}
}
}
pointerOffset += arg.Length(model, currentSize + pointerOffset);
}
lastCommandIsEndCommand = command.IsEndingCommand;
}
}
if (!lastCommandIsEndCommand) result.Add(endToken);
// any labels that were used but not included, stick them on the end of the script
labels.ResolveUnresolvedLabels(start, result, endToken);
// done with script lines, now write deferred data
foreach (var deferred in deferredContent) {
deferred.WriteData(result, start);
}
return result.ToArray();
}
public IScriptLine FirstMatch(string line) {
foreach (var command in engine) {
if (!command.MatchesGame(gameHash)) continue;
if (command.CanCompile(line)) return command;
}
return null;
}
private LabelLibrary ExtractLocalLabels(IDataModel model, int start, string[] lines) {
var labels = new Dictionary<string, int>();
var length = 0;
foreach (var fullLine in lines) {
var line = fullLine.Trim();
if (line.EndsWith(":")) {
labels[line.Substring(0, line.Length - 1)] = start + length;
continue;
}
foreach (var command in engine) {
if (!command.CanCompile(line)) continue;
length += command.CompiledByteLength(model, line);
break;
}
}
return new LabelLibrary(model, labels) { RequireCompleteAddresses = RequireCompleteAddresses };
}
public string GetHelp(IDataModel model, HelpContext context) {
var currentLine = context.Line;
if (string.IsNullOrWhiteSpace(currentLine)) return null;
var tokens = ScriptLine.Tokenize(currentLine.Trim());
var candidates = engine.Where(line => line.LineCommand.Contains(tokens[0])).ToList();
var isAfterToken = context.Index > 0 &&
(context.Line.Length == context.Index || context.Line[context.Index] == ' ') &&
(char.IsLetterOrDigit(context.Line[context.Index - 1]) || context.Line[context.Index - 1].IsAny("_~'\"-.".ToCharArray()));
if (isAfterToken) {
tokens = ScriptLine.Tokenize(currentLine.Substring(0, context.Index).Trim());
// try to auto-complete whatever token is left of the cursor
// need autocomplete for command?
if (tokens.Length == 1) {
candidates = candidates.Where(line => line.LineCommand.StartsWith(tokens[0]) && line.MatchesGame(gameHash)).ToList();
foreach (var line in candidates) {
if (line.LineCommand == tokens[0] && line.CountShowArgs() == 0) return null; // perfect match with no args
}
return Environment.NewLine.Join(candidates.Take(10).Select(line => line.Usage));
}
// filter down to just perfect matches. There could be several (trainerbattle)
candidates = candidates.Where(line => line.LineCommand == tokens[0]).ToList();
var checkToken = 1;
while (candidates.Count > 1 && checkToken < tokens.Length) {
if (!tokens[checkToken].TryParseHex(out var codeValue)) break;
candidates = candidates.Where(line => line.LineCode.Count <= checkToken || line.LineCode[checkToken] == codeValue).ToList();
checkToken++;
}
var syntax = candidates.FirstOrDefault();
if (syntax != null) {
var args = syntax.Args.Where(arg => arg is ScriptArg).ToList();
var skipCount = syntax.LineCode.Count;
if (skipCount == 0) skipCount = 1; // macros
if (args.Count + skipCount >= tokens.Length && tokens.Length >= skipCount + 1) {
var arg = args[tokens.Length - 1 - skipCount];
if (!string.IsNullOrEmpty(arg.EnumTableName)) {
var options = model.GetOptions(arg.EnumTableName).Where(option => option.MatchesPartial(tokens[tokens.Length - 1])).ToList();
if (options.Count > 10) {
while (options.Count > 9) options.RemoveAt(options.Count - 1);
options.Add("...");
}
if (args.Count == tokens.Length - skipCount && options.Any(option => option == tokens[tokens.Length - 1])) return null; // perfect match on last token
return Environment.NewLine.Join(options);
}
}
}
}
if (candidates.Count > 10) return null;
if (candidates.Count == 0) return null;
if (candidates.Count == 1) {
if (candidates[0].CountShowArgs() <= tokens.Length - 1) return null;
return candidates[0].Usage + Environment.NewLine + string.Join(Environment.NewLine, candidates[0].Documentation);
}
var perfectMatch = candidates.FirstOrDefault(candidate => (currentLine + " ").StartsWith(candidate.LineCommand + " "));
if (perfectMatch != null) {
if (perfectMatch.CountShowArgs() == tokens.Length - 1) return null;
return perfectMatch.Usage + Environment.NewLine + string.Join(Environment.NewLine, perfectMatch.Documentation);
}
var bestMatch = candidates.FirstOrDefault(candidate => tokens[0].Contains(candidate.LineCommand));
if (bestMatch != null) {
if (bestMatch.CountShowArgs() == tokens.Length - 1) return null;
return bestMatch.Usage + Environment.NewLine + Environment.NewLine.Join(bestMatch.Documentation);
}
return string.Join(Environment.NewLine, candidates.Select(line => line.Usage));
}
public static int GetArgLength(IDataModel model, IScriptArg arg, int start, IDictionary<int, int> destinationLengths) {
if (arg.Type == ArgType.Pointer && arg.PointerType != ExpectedPointerType.Unknown) {
var destination = model.ReadPointer(start);
if (destination >= 0 && destination < model.Count) {
var run = model.GetNextRun(destination);
if (run is IScriptStartRun scriptStart && scriptStart.Start == destination && scriptStart.Start > start) {
return model.GetScriptLength(scriptStart, destinationLengths);
} else if (run.Start == destination) {
// we only want to add this run's length as part of the script if:
// (1) the run has no name
// (2) the run has only one source (the script)
if (run is NoInfoRun || run.PointerSources == null) return -1;
if (run is IScriptStartRun) return -1; // this script has a length, but don't track it (prevent recursion loop)
if (run.PointerSources.Count == 1 && string.IsNullOrEmpty(model.GetAnchorFromAddress(-1, destination))) {
return run.Length;
}
} else if (arg.PointerType == ExpectedPointerType.Text) {
// we didn't find a matching run, but this data claims to be simple text
var textLength = PCSString.ReadString(model, destination, true);
return textLength;
}
}
}
return -1;
}
private string[] Decompile(IDataModel data, int index, int length) {
var results = new List<string>();
var nextAnchor = data.GetNextAnchor(index);
var destinations = new Dictionary<int, int>();
ISet<int> linesWithLabelsToUpdate = new HashSet<int>();
var labels = new DecompileLabelLibrary(data, index, length);
while (length > 0) {
if (index == nextAnchor.Start) {
if (nextAnchor is IScriptStartRun) {
if (results.Count > 0) results.Add(string.Empty);
results.Add($"{labels.AddressToLabel(nextAnchor.Start, true)}: # {nextAnchor.Start:X6}");
linesWithLabelsToUpdate.Add(results.Count - 1);
} else if (nextAnchor is IStreamRun) {
if (destinations.ContainsKey(index)) {
index += nextAnchor.Length;
length -= nextAnchor.Length;
}
}
nextAnchor = data.GetNextAnchor(nextAnchor.Start + nextAnchor.Length);
continue;
} else if (index > nextAnchor.Start) {
nextAnchor = data.GetNextAnchor(nextAnchor.Start + nextAnchor.Length);
continue;
}
var line = engine.FirstOrDefault(option => option.Matches(gameHash, data, index));
if (line == null) {
results.Add($".raw {data[index]:X2}");
index += 1;
length -= 1;
} else {
results.Add(" " + line.Decompile(data, index, labels));
if (line.Args.Any(arg => arg.Type == ArgType.Pointer && arg.PointerType == ExpectedPointerType.Script)) {
linesWithLabelsToUpdate.Add(results.Count - 1);
}
var compiledByteLength = line.CompiledByteLength(data, index, destinations);
index += compiledByteLength;
length -= compiledByteLength;
// if we point to shortly after, keep going
if (destinations.ContainsKey(index) && nextAnchor is IStreamRun && nextAnchor.Start == index) continue;
if (destinations.ContainsKey(index + 1) && nextAnchor is IStreamRun && nextAnchor.Start == index + 1) continue;
if (destinations.ContainsKey(index) && nextAnchor is IScriptStartRun && nextAnchor.Start == index) continue;
if (destinations.ContainsKey(index + 1) && nextAnchor is IScriptStartRun && nextAnchor.Start == index + 1) continue;
// if we're at an end command, don't keep going
if (line.IsEndingCommand) break;
}
}
// post processing: if a line has a stream pointer to within the script,
// change that pointer to be an -auto- pointer
foreach (var label in labels.AutoLabels.ToList()) {
var autoIndex = results.Count.Range().FirstOrDefault(i => results[i].Contains($"<{label:X6}>"));
var autoRun = data.GetNextRun(label);
var runIsStream = autoRun is IStreamRun;
if (runIsStream) {
results[autoIndex] = results[autoIndex].Replace($"<{label:X6}>", "<auto>");
} else {
continue;
}
}
// post processing: change the section labels to be in address order
var sections = labels.FinalizeLabels();
foreach (int i in linesWithLabelsToUpdate) {
results[i] = labels.FinalizeLine(sections, results[i]);
}
return results.ToArray();
}
}
public interface IScriptLine {
IReadOnlyList<IScriptArg> Args { get; }
IReadOnlyList<byte> LineCode { get; }
string LineCommand { get; }
IReadOnlyList<string> Documentation { get; }
string Usage { get; }
bool IsEndingCommand { get; }
bool MatchesGame(int gameCodeHash);
int CompiledByteLength(IDataModel model, int start, IDictionary<int, int> 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<byte> data, int index);
string Decompile(IDataModel data, int start, DecompileLabelLibrary labels);
/// <summary>
/// Returns true if the command looks correct, even if the arguments are incomplete.
/// </summary>
bool CanCompile(string line);
/// <summary>
/// Returns an error if the line cannot be compiled, or a set of tokens if it can be compiled.
/// </summary>
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<byte> emptyByteList = new byte[0];
private readonly List<string> documentation = new List<string>();
private bool hasShortForm;
private readonly Dictionary<int, int> shortIndexFromLongIndex = new();
private readonly IReadOnlyList<int> matchingGames;
public IReadOnlyList<IScriptArg> Args { get; }
public IReadOnlyList<IScriptArg> 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<byte> LineCode => emptyByteList;
public IReadOnlyList<string> 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 tokens = engineLine.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
var args = new List<IScriptArg>();
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<int, int> 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<byte> 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) {
var builder = new StringBuilder(LineCommand);
var streamContent = new List<string>();
var args = new List<string>();
foreach (var arg in Args) {
if (arg is ScriptArg sarg) {
var tempBuilder = new StringBuilder();
sarg.Build(false, data, start, tempBuilder, streamContent, labels);
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);
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();
var results = new List<byte>();
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<string>();
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<int, string>();
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,
Text,
Movement,
Mart,
Decor,
SpriteTemplate,
}
public abstract class ScriptLine : IScriptLine {
private readonly List<string> documentation = new List<string>();
private readonly IReadOnlyList<int> matchingGames;
public const string Hex = "0123456789ABCDEF";
public IReadOnlyList<IScriptArg> Args { get; }
public IReadOnlyList<byte> LineCode { get; }
public string LineCommand { get; }
public IReadOnlyList<string> Documentation => documentation;
public string Usage { get; }
public virtual bool IsEndingCommand { get; }
/// <param name="destinationLengths">If this line contains pointers, calculate the pointer data's lengths and include here.</param>
public int CompiledByteLength(IDataModel model, int start, IDictionary<int, int> 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 tokens = engineLine.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
var lineCode = new List<byte>();
var args = new List<IScriptArg>();
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<int> 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 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<byte> 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 + " ")) 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] != LineCommand) 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<byte>(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) {
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<string>();
foreach (var arg in Args) {
builder.Append(" ");
if (arg is ScriptArg scriptArg) {
if (scriptArg.Build(allFillerIsZero, data, start, builder, streamContent, labels)) 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) {
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<string>();
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[] { ' ' }, 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<byte>(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<byte>(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<byte>(0x08, 0x0F, 0x11, 0x13);
}
public class TSEScriptLine : ScriptLine {
public TSEScriptLine(string engineLine) : base(engineLine) { }
public override bool IsEndingCommand => LineCode.Count == 1 && LineCode[0].IsAny<byte>(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 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).ToString($"X{length * 2}");
} else {
if (bytes == 1 && preferSign) value = (sbyte)value;
if (bytes == 2 && preferSign) value = (short)value;
return value.ToString();
}
}
return table[value - EnumOffset];
}
public int Convert(IDataModel model, string value) {
int result;
if (!string.IsNullOrEmpty(EnumTableName)) {
if (ArrayRunEnumSegment.TryParse(EnumTableName, model, value, out result)) return result + EnumOffset;
}
if (value.StartsWith("0x") && value.Substring(2).TryParseHex(out result)) return result;
if (value.StartsWith("0X") && value.Substring(2).TryParseHex(out result)) return result;
if (value.StartsWith("$") && value.Substring(1).TryParseHex(out result)) return result;
if (int.TryParse(value, out result)) return result;
return 0;
}
/// <summary>
/// Build from compiled bytes to text.
/// </summary>
public bool Build(bool allFillerIsZero, IDataModel data, int start, StringBuilder builder, List<string> streamContent, DecompileLabelLibrary labels) {
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());
}
}
}
}
return false;
}
/// <summary>
/// Build from text to compiled bytes.
/// </summary>
public string Build(IDataModel model, int address, string token, IList<byte> results, LabelLibrary labels) {
if (Type == ArgType.Byte) {
results.Add((byte)Convert(model, token));
} else if (Type == ArgType.Short) {
var value = Convert(model, token);
results.Add((byte)value);
results.Add((byte)(value >> 8));
} else if (Type == ArgType.Word) {
var value = Convert(model, token);
results.Add((byte)value);
results.Add((byte)(value >> 0x8));
results.Add((byte)(value >> 0x10));
results.Add((byte)(value >> 0x18));
} else if (Type == ArgType.Pointer) {
int value;
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 "<auto> 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<int> ConvertMany(IDataModel model, IEnumerable<string> 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<IScriptLine> self, int gameHash, IReadOnlyList<byte>data, int start) {
return (MacroScriptLine)self.FirstOrDefault(option => option is MacroScriptLine && option.Matches(gameHash, data, start));
}
/// <summary>
/// Does not consider macros. Only returns individual lines.
/// </summary>
public static ScriptLine GetMatchingLine(this IReadOnlyList<IScriptLine> self, int gameHash, IReadOnlyList<byte> data, int start) {
return (ScriptLine)self.FirstOrDefault(option => option is ScriptLine && option.Matches(gameHash, data, start));
}
public static int GetScriptSegmentLength(this IReadOnlyList<IScriptLine> self, int gameHash, IDataModel model, int address, IDictionary<int, int> destinationLengths) {
int length = 0;
int lastCommand = -1, repeateLength = 1;
while (true) {
var line = self.GetMatchingLine(gameHash, model, address + length);
if (line == null) break;
length += line.CompiledByteLength(model, address + length, destinationLengths);
if (line.IsEndingCommand) break;
if (line.LineCode[0] != lastCommand) (lastCommand, repeateLength) = (line.LineCode[0], 1);
else repeateLength += 1;
if (line.Args.Count > 0) repeateLength = 0;
if (repeateLength > ScriptParser.MaxRepeates) break; // same command lots of times in a row is fishy
}
// concatenate destinations directly after the current script
// (only if the destination is only used once)
while (true) {
if (destinationLengths.TryGetValue(address + length, out int argLength) && argLength > 0) {
var anchor = model.GetNextAnchor(address + length);
if (anchor.Start == address + length && anchor.PointerSources.Count == 1) {
length += argLength;
continue;
}
}
if (destinationLengths.TryGetValue(address + length + 1, out argLength)) {
// there was a skip... should we ignore it?
// If something points to that position, we can't keep going.
var anchor = model.GetNextAnchor(address + length);
if (anchor.Start == address + length && anchor.PointerSources.Count > 0) break;
anchor = model.GetNextAnchor(address + length + 1);
if (anchor.Start == address + length + 1 && anchor.PointerSources.Count == 1) {
length += argLength + 1;
continue;
}
}
break;
}
return length;
}
}
public class DeferredStreamToken {
public const int AutoSentinel = -0xAAAA;
private readonly int pointerOffset;
private readonly string format;
private byte[] content;
public int ContentLength => content.Length;
public DeferredStreamToken(int pointerOffset, string format, byte[] defaultContent) {
this.pointerOffset = pointerOffset;
this.format = format;
this.content = defaultContent;
}
// need the model not for insertion, but for text encoding
public void UpdateContent(IDataModel model, ExpectedPointerType type, string text) {
if (type == ExpectedPointerType.Text) {
content = model.TextConverter.Convert(text, out _).ToArray();
} else if (type == ExpectedPointerType.Mart) {
// [item:{HardcodeTablesModel.ItemsTableName}]!0000
var data = new List<byte>();
foreach (var line in text.SplitLines()) {
if (string.IsNullOrWhiteSpace(line)) continue;
if (!ArrayRunEnumSegment.TryParse(HardcodeTablesModel.ItemsTableName, model, line, out int value)) continue;
data.AddShort(value);
}
data.AddShort(0);
content = data.ToArray();
} else if (type == ExpectedPointerType.Mart) {
// [item:{HardcodeTablesModel.DecorationsTableName}]!0000
var data = new List<byte>();
foreach (var line in text.SplitLines()) {
if (string.IsNullOrWhiteSpace(line)) continue;
if (!ArrayRunEnumSegment.TryParse(HardcodeTablesModel.DecorationsTableName, model, line, out int value)) continue;
data.AddShort(value);
}
data.AddShort(0);
content = data.ToArray();
} else if (type == ExpectedPointerType.Movement) {
// $"[move.movementtypes]!FE",
var data = new List<byte>();
foreach (var line in text.SplitLines()) {
if (string.IsNullOrWhiteSpace(line)) continue;
if (!ArrayRunEnumSegment.TryParse("movementtypes", model, line, out int value)) continue;
data.Add((byte)value);
}
data.Add(0xFE);
content = data.ToArray();
} else {
throw new NotImplementedException();
}
}
public void WriteData(IDataModel model, ModelDelta token, int scriptStart, int contentOffset) {
model.ClearFormat(token, scriptStart + pointerOffset, 4);
model.WritePointer(token, scriptStart + pointerOffset, scriptStart + contentOffset);
model.ObserveRunWritten(token, new PointerRun(scriptStart + pointerOffset));
token.ChangeData(model, scriptStart + contentOffset, content);
var strategy = new FormatRunFactory(default).GetStrategy(format);
strategy.TryAddFormatAtDestination(model, token, scriptStart + pointerOffset, scriptStart + contentOffset, default, default, default);
}
public void WriteData(IList<byte> data, int scriptStart) {
var address = scriptStart + data.Count - Pointer.NULL;
data[pointerOffset + 0] = (byte)(address >> 0);
data[pointerOffset + 1] = (byte)(address >> 8);
data[pointerOffset + 2] = (byte)(address >> 16);
data[pointerOffset + 3] = (byte)(address >> 24);
data.AddRange(content);
}
}
}