DS-Pokemon-Rom-Editor/DS_Map/ROMFiles/ScriptFile.cs
Miguel Terol Espino 12da61692f Started merging changes
Co-authored-by: Cuddlyogre <cuddlyogre@users.noreply.github.com>
2024-03-06 00:36:18 +01:00

932 lines
48 KiB
C#

using DSPRE.Resources;
using ScintillaNET;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using static DSPRE.ROMFiles.ScriptFile;
using static DSPRE.RomInfo;
namespace DSPRE.ROMFiles {
/// <summary>
/// Class to store script file data in Pokémon NDS games
/// </summary>
public class ScriptFile : RomFile {
#region Constants
//this enum doesn't really make much sense now but it will, once scripts can be called and jumped to
public enum containerTypes { Function, Action, Script };
public struct ContainerReference {
public uint ID;
public uint offsetInFile;
}
#endregion
#region Fields (3)
public List<CommandContainer> allScripts = new List<CommandContainer>();
public List<CommandContainer> allFunctions = new List<CommandContainer>();
public List<ActionContainer> allActions = new List<ActionContainer>();
public int? fileID = null;
public bool isLevelScript = new bool();
public static readonly char[] specialChars = { 'x', 'X', '#', '.', '_' };
#endregion
#region Constructors (1)
public ScriptFile(Stream fs, bool readFunctions = true, bool readActions = true) {
List<int> scriptOffsets = new List<int>();
List<int> functionOffsets = new List<int>();
List<int> movementOffsets = new List<int>();
using (BinaryReader scrReader = new BinaryReader(fs)) {
/* Read script offsets from the header */
isLevelScript = true; // Is Level Script as long as magic number FD13 doesn't exist
try {
while (true) {
uint checker = scrReader.ReadUInt16();
scrReader.BaseStream.Position -= 0x2;
uint value = scrReader.ReadUInt32();
if (value == 0 && scriptOffsets.Count == 0) {
isLevelScript = true;
break;
} else if (checker == 0xFD13) {
scrReader.BaseStream.Position -= 0x4;
isLevelScript = false;
break;
} else {
int offsetFromStart = (int)(value + scrReader.BaseStream.Position); // Don't change order of addition
scriptOffsets.Add(offsetFromStart);
}
}
} catch (EndOfStreamException) {
if (!isLevelScript) {
MessageBox.Show("Script File couldn't be read correctly.", "Unexpected EOF", MessageBoxButtons.OK, MessageBoxIcon.Error); // Now this may appear in a few level scripts that don't have a 4-byte aligned "00 00 00 00"
}
}
if (isLevelScript) {
return;
}
/* Read scripts */
for (uint current = 0; current < scriptOffsets.Count; current++) {
int index = scriptOffsets.FindIndex(x => x == scriptOffsets[(int)current]); // Check for UseScript
if (index == current) {
scrReader.BaseStream.Position = scriptOffsets[(int)current];
List<ScriptCommand> cmdList = new List<ScriptCommand>();
bool endScript = new bool();
while (!endScript) {
ScriptCommand cmd = ReadCommand(scrReader, ref functionOffsets, ref movementOffsets);
if (cmd.cmdParams is null) {
return;
}
cmdList.Add(cmd);
if (ScriptDatabase.endCodes.Contains((ushort)cmd.id)) {
endScript = true;
}
}
allScripts.Add(new CommandContainer(current + 1, containerTypes.Script, commandList: cmdList));
} else {
allScripts.Add(new CommandContainer(current + 1, containerTypes.Script, useScript: index + 1));
}
}
/* Read functions */
if (readFunctions) {
for (uint current = 0; current < functionOffsets.Count; current++) {
scrReader.BaseStream.Position = functionOffsets[(int)current];
int posInList = scriptOffsets.IndexOf(functionOffsets[(int)current]); // Check for UseScript
if (posInList == -1) {
List<ScriptCommand> cmdList = new List<ScriptCommand>();
bool endFunction = new bool();
while (!endFunction) {
ScriptCommand command = ReadCommand(scrReader, ref functionOffsets, ref movementOffsets);
if (command.cmdParams is null) {
return;
}
cmdList.Add(command);
if (ScriptDatabase.endCodes.Contains((ushort)command.id)) {
endFunction = true;
}
}
allFunctions.Add(new CommandContainer(current + 1, containerTypes.Function, commandList: cmdList));
} else {
allFunctions.Add(new CommandContainer(current + 1, containerTypes.Function, useScript: posInList + 1));
}
}
}
if (readActions) {
/* Read movements */
for (uint current = 0; current < movementOffsets.Count; current++) {
scrReader.BaseStream.Position = movementOffsets[(int)current];
List<ScriptAction> cmdList = new List<ScriptAction>();
bool endMovement = new bool();
while (!endMovement) {
ushort id = scrReader.ReadUInt16();
if (id == 0xFE) {
endMovement = true;
cmdList.Add(new ScriptAction(id, 0));
} else {
cmdList.Add(new ScriptAction(id, scrReader.ReadUInt16()));
}
}
allActions.Add(new ActionContainer(current + 1, actionCommandsList: cmdList));
}
}
}
}
public ScriptFile(int fileID, bool readFunctions = true, bool readActions = true) :
this(new FileStream(RomInfo.gameDirs[DirNames.scripts].unpackedDir + "\\" + fileID.ToString("D4"), FileMode.Open), readFunctions, readActions) {
this.fileID = fileID;
}
public ScriptFile(List<CommandContainer> scripts, List<CommandContainer> functions, List<ActionContainer> movements, int fileID = -1) {
allScripts = scripts;
allFunctions = functions;
allActions = movements;
isLevelScript = false;
}
public ScriptFile(IEnumerable<string> scriptLines, IEnumerable<string> functionLines, IEnumerable<string> actionLines, int fileID = -1) {
//TODO: give user the possibility to jump to/call a script
//once it's done, this Predicate below will be the only one needed, since there will be no distinction between
//a script and a function
bool functionEndCondition(List<(int linenum, string text)> source, int x, ushort? id) {
return source[x].text.TrimEnd().IgnoreCaseEquals(RomInfo.ScriptCommandNamesDict[0x0002]) //End
|| source[x].text.IndexOf(RomInfo.ScriptCommandNamesDict[0x0016] + ' ' + containerTypes.Function.ToString(), StringComparison.InvariantCultureIgnoreCase) >= 0 //Jump Function_#
|| source[x].text.TrimEnd().IgnoreCaseEquals(RomInfo.ScriptCommandNamesDict[0x001B])
|| ScriptDatabase.endCodes.Contains(id);
} //Return
bool scriptEndCondition(List<(int linenum, string text)> source, int x, ushort? id) {
return source[x].text.TrimEnd().IgnoreCaseEquals(RomInfo.ScriptCommandNamesDict[0x0002]) //End
|| source[x].text.IndexOf(RomInfo.ScriptCommandNamesDict[0x0016] + ' ' + containerTypes.Function.ToString()) >= 0 //Jump Function_#
|| ScriptDatabase.endCodes.Contains(id);
}
allScripts = ReadCommandsFromLines(scriptLines.ToList(), containerTypes.Script, scriptEndCondition); //Jump + whitespace
if (allScripts is null) {
return;
}
if (allScripts.Count <= 0) {
this.fileID = int.MaxValue;
return;
}
if (functionLines != null) {
allFunctions = ReadCommandsFromLines(functionLines.ToList(), containerTypes.Function, functionEndCondition); //Jump + whitespace
if (allFunctions is null) {
return;
}
}
if (actionLines != null) {
allActions = ReadActionsFromLines(actionLines.ToList());
if (allActions is null) {
return;
}
}
this.fileID = fileID;
}
#endregion
#region Methods (1)
private ScriptCommand ReadCommand(BinaryReader dataReader, ref List<int> functionOffsets, ref List<int> actionOffsets) {
ushort id = dataReader.ReadUInt16();
List<byte[]> parameterList = new List<byte[]>();
/* How to read parameters for different commands for DPPt*/
switch (RomInfo.gameFamily) {
case GameFamilies.DP:
case GameFamilies.Plat:
switch (id) {
case 0x16: //Jump
case 0x1A: //Call
ProcessRelativeJump(dataReader, ref parameterList, ref functionOffsets);
break;
case 0x17: //JumpIfObjID
case 0x18: //JumpIfBgID
case 0x19: //JumpIfPlayerDir
case 0x1C: //JumpIf
case 0x1D: //CallIf
//in the case of JumpIf and CallIf, the first param is a comparisonOperator
//for JumpIfPlayerDir it's a directionID
//for JumpIfObjID, it's an EventID
parameterList.Add(new byte[] { dataReader.ReadByte() });
ProcessRelativeJump(dataReader, ref parameterList, ref functionOffsets);
break;
case 0x5E: // Movement
case 0x2A1: // Movement2
//in the case of Movement, the first param is an overworld ID
parameterList.Add(BitConverter.GetBytes(dataReader.ReadUInt16()));
ProcessRelativeJump(dataReader, ref parameterList, ref actionOffsets);
break;
case 0x1CF:
case 0x1D0:
case 0x1D1: {
byte parameter1 = dataReader.ReadByte();
parameterList.Add(new byte[] { parameter1 });
if (parameter1 == 0x2) {
parameterList.Add(dataReader.ReadBytes(2)); //Read additional u16 if first param read is 2
}
}
break;
case 0x21D: {
ushort parameter1 = dataReader.ReadUInt16();
parameterList.Add(BitConverter.GetBytes(parameter1));
switch (parameter1) {
case 0:
case 1:
case 2:
case 3:
parameterList.Add(dataReader.ReadBytes(2));
parameterList.Add(dataReader.ReadBytes(2));
break;
case 4:
case 5:
parameterList.Add(dataReader.ReadBytes(2));
break;
case 6:
break;
}
}
break;
case 0x235: {
short parameter1 = dataReader.ReadInt16();
parameterList.Add(BitConverter.GetBytes(parameter1));
switch (parameter1) {
case 0x1:
case 0x3:
parameterList.Add(dataReader.ReadBytes(2));
parameterList.Add(dataReader.ReadBytes(2));
parameterList.Add(dataReader.ReadBytes(2));
break;
case 0x4:
parameterList.Add(dataReader.ReadBytes(2));
parameterList.Add(dataReader.ReadBytes(2));
break;
case 0x0:
case 0x6:
parameterList.Add(dataReader.ReadBytes(2));
break;
default:
break;
}
}
break;
case 0x23E: {
short parameter1 = dataReader.ReadInt16();
parameterList.Add(BitConverter.GetBytes(parameter1));
switch (parameter1) {
case 0x1:
case 0x3:
parameterList.Add(dataReader.ReadBytes(2));
break;
case 0x5:
case 0x6:
parameterList.Add(dataReader.ReadBytes(2));
parameterList.Add(dataReader.ReadBytes(2));
break;
default:
break;
}
}
break;
case 0x2C4: {
byte parameter1 = dataReader.ReadByte();
parameterList.Add(new byte[] { parameter1 });
if (parameter1 == 0 || parameter1 == 1) {
parameterList.Add(dataReader.ReadBytes(2));
}
}
break;
case 0x2C5: {
if (RomInfo.gameVersion == GameVersions.Platinum) {
parameterList.Add(dataReader.ReadBytes(2));
parameterList.Add(dataReader.ReadBytes(2));
} else {
goto default;
}
}
break;
case 0x2C6:
case 0x2C9:
case 0x2CA:
case 0x2CD:
if (RomInfo.gameVersion == GameVersions.Platinum) {
break;
} else {
goto default;
}
case 0x2CF:
if (RomInfo.gameVersion == GameVersions.Platinum) {
parameterList.Add(dataReader.ReadBytes(2));
parameterList.Add(dataReader.ReadBytes(2));
} else {
goto default;
}
break;
default:
addParametersToList(ref parameterList, id, dataReader);
break;
}
break;
case GameFamilies.HGSS:
switch (id) {
case 0x16: //Jump
case 0x1A: //Call
ProcessRelativeJump(dataReader, ref parameterList, ref functionOffsets);
break;
case 0x17: //JumpIfObjID
case 0x18: //JumpIfBgID
case 0x19: //JumpIfPlayerDir
case 0x1C: //JumpIf
case 0x1D: //CallIf
parameterList.Add(new byte[] { dataReader.ReadByte() }); //in the case of JumpIf and CallIf, the first param is a comparisonOperator
ProcessRelativeJump(dataReader, ref parameterList, ref functionOffsets);
break;
case 0x5E: // Movement
parameterList.Add(BitConverter.GetBytes(dataReader.ReadUInt16())); //in the case of Movement, the first param is an overworld ID
ProcessRelativeJump(dataReader, ref parameterList, ref actionOffsets);
break;
case 0x190:
case 0x191:
case 0x192: {
byte parameter1 = dataReader.ReadByte();
parameterList.Add(new byte[] { parameter1 });
if (parameter1 == 0x2) {
parameterList.Add(dataReader.ReadBytes(2));
}
}
break;
case 0x1D1: // Number of parameters differ depending on the first parameter value
{
short parameter1 = dataReader.ReadInt16();
parameterList.Add(BitConverter.GetBytes(parameter1));
switch (parameter1) {
case 0x0:
case 0x1:
case 0x2:
case 0x3:
parameterList.Add(dataReader.ReadBytes(2));
parameterList.Add(dataReader.ReadBytes(2));
break;
case 0x4:
case 0x5:
parameterList.Add(dataReader.ReadBytes(2));
break;
case 0x6:
break;
case 0x7:
parameterList.Add(dataReader.ReadBytes(2));
break;
default:
break;
}
}
break;
case 0x1E9: // Number of parameters differ depending on the first parameter value
{
short parameter1 = dataReader.ReadInt16();
parameterList.Add(BitConverter.GetBytes(parameter1));
switch (parameter1) {
case 0x0:
break;
case 0x1:
case 0x2:
case 0x3:
parameterList.Add(dataReader.ReadBytes(2));
break;
case 0x4:
break;
case 0x5:
case 0x6:
parameterList.Add(dataReader.ReadBytes(2));
parameterList.Add(dataReader.ReadBytes(2));
break;
case 0x7:
case 0x8:
break;
default:
break;
}
}
break;
default:
addParametersToList(ref parameterList, id, dataReader);
break;
}
break;
}
return new ScriptCommand(id, parameterList);
}
private void ProcessRelativeJump(BinaryReader dataReader, ref List<byte[]> parameterList, ref List<int> offsetsList) {
int relativeOffset = dataReader.ReadInt32();
int offsetFromScriptFileStart = (int)(relativeOffset + dataReader.BaseStream.Position);
if (!offsetsList.Contains(offsetFromScriptFileStart)) {
offsetsList.Add(offsetFromScriptFileStart);
}
int functionNumber = offsetsList.IndexOf(offsetFromScriptFileStart);
if (functionNumber < 0) {
throw new InvalidOperationException();
}
parameterList.Add(BitConverter.GetBytes(functionNumber + 1));
}
private void addParametersToList(ref List<byte[]> parameterList, ushort id, BinaryReader dataReader) {
Console.WriteLine("Loaded command id: " + id.ToString("X4"));
try {
foreach (int bytesToRead in RomInfo.ScriptCommandParametersDict[id]) {
parameterList.Add(dataReader.ReadBytes(bytesToRead));
}
} catch (NullReferenceException) {
MessageBox.Show("Script command " + id + "can't be handled for now." +
Environment.NewLine + "Reference offset 0x" + dataReader.BaseStream.Position.ToString("X"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
parameterList = null;
return;
} catch {
MessageBox.Show("Error: ID Read - " + id +
Environment.NewLine + "Reference offset 0x" + dataReader.BaseStream.Position.ToString("X"), "Unrecognized script command", MessageBoxButtons.OK, MessageBoxIcon.Error);
parameterList = null;
return;
}
}
private void AddReference(ref List<ScriptReference> references, ushort commandID, List<byte[]> parameterList, int pos, CommandContainer cont) {
if (ScriptDatabase.commandsWithRelativeJump.TryGetValue(commandID, out int parameterWithRelativeJump)) {
uint invokedID = BitConverter.ToUInt32(parameterList[parameterWithRelativeJump], 0); // Jump, Call
if (commandID == 0x005E)
references.Add(new ScriptReference(cont.containerType, cont.manualUserID, containerTypes.Action, invokedID, pos - 4));
else {
references.Add(new ScriptReference(cont.containerType, cont.manualUserID, containerTypes.Function, invokedID, pos - 4));
}
}
}
private List<CommandContainer> ReadCommandsFromLines(List<string> linelist, containerTypes containerType, Func<List<(int linenum, string text)>, int, ushort?, bool> endConditions) {
List<(int linenum, string text)> lineSource = new List<(int linenum, string text)>();
for (int l = 0; l < linelist.Count; l++) {
string cur = linelist[l];
if (!string.IsNullOrWhiteSpace(cur)) {
lineSource.Add((l, cur));
}
}
List<CommandContainer> ls = new List<CommandContainer>();
int i = 0;
try {
uint scriptNumber = 0;
while (i < lineSource.Count) {
if (scriptNumber == 0) {
int positionOfScriptNumber;
int positionOfScriptKeyword = lineSource[i].text.IndexOf(containerType.ToString(), StringComparison.InvariantCultureIgnoreCase);
if (positionOfScriptKeyword > 0) {
MessageBox.Show("Unrecognized container keyword: \"" + lineSource[i] + '"', "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
} else if (positionOfScriptKeyword < 0) {
i++;
continue;
} else {
if ((positionOfScriptNumber = lineSource[i].text.IndexOfFirstNumber()) < positionOfScriptKeyword) {
MessageBox.Show("Unspecified Script/Function label.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
}
}
scriptNumber = uint.Parse(lineSource[i++].text.Substring(positionOfScriptNumber).Split()[0].Replace(":", ""));
}
if (lineSource[i].text.IndexOf("UseScript", StringComparison.InvariantCultureIgnoreCase) >= 0) {
int useScriptNumber = short.Parse(lineSource[i].text.Substring(1 + lineSource[i].text.IndexOf('#')));
ls.Add(new CommandContainer(scriptNumber, containerType, useScriptNumber));
i++;
} else {
/* Read script commands */
List<ScriptCommand> cmdList = new List<ScriptCommand>();
ScriptCommand lastRead;
do {
lastRead = new ScriptCommand(lineSource[i].text, lineSource[i].linenum + 1);
if (lastRead.id is null) {
return null;
}
cmdList.Add(lastRead);
} while (!endConditions(lineSource, i++, lastRead.id));
ls.Add(new CommandContainer(scriptNumber, containerType, commandList: cmdList));
}
scriptNumber = 0;
}
} catch (ArgumentOutOfRangeException) {
MessageBox.Show($"Unexpectedly reached end of lines.\n\n" +
$"Last line index: {lineSource[i].linenum}.\n" +
$"Managed to parse {ls.Count} Command Containers.", "Fatal Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
}
return ls;
}
private List<ActionContainer> ReadActionsFromLines(List<string> linelist) {
List<(int linenum, string text)> lineSource = new List<(int linenum, string text)>();
for (int l = 0; l < linelist.Count; l++) {
string cur = linelist[l];
if (!string.IsNullOrWhiteSpace(cur)) {
lineSource.Add((l, cur));
}
}
List<ActionContainer> ls = new List<ActionContainer>();
int i = 0;
try {
uint actionNumber = 0;
while (i < lineSource.Count) {
if (actionNumber == 0) {
int positionOfActionNumber;
int positionOfActionKeyword = lineSource[i].text.IndexOf(containerTypes.Action.ToString(), StringComparison.InvariantCultureIgnoreCase);
if (positionOfActionKeyword > 0) {
MessageBox.Show("Unrecognized container keyword: \"" + lineSource[i] + '"', "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
} else if (positionOfActionKeyword < 0) {
i++;
continue;
} else {
if ((positionOfActionNumber = lineSource[i].text.IndexOfFirstNumber()) < positionOfActionKeyword) {
MessageBox.Show("Unspecified Action label.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
}
}
actionNumber = uint.Parse(lineSource[i].text.Substring(positionOfActionNumber).Split()[0].Replace(":", ""));
i++;
}
List<ScriptAction> cmdList = new List<ScriptAction>();
/* Read script actions */
do {
ScriptAction toAdd = new ScriptAction(lineSource[i].text, lineSource[i].linenum + 1);
if (toAdd.id is null) {
return null;
}
cmdList.Add(toAdd);
} while (!lineSource[i++].text.IgnoreCaseEquals(RomInfo.ScriptActionNamesDict[0x00FE]));
ls.Add(new ActionContainer(actionNumber, actionCommandsList: cmdList));
actionNumber = 0;
}
} catch (ArgumentOutOfRangeException) {
MessageBox.Show($"Unexpectedly reached end of lines.\n\n" +
$"Last line index: {i}.\n" +
$"Managed to parse {ls.Count} Command Containers.", "Fatal Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
}
return ls;
}
#region Output
public override byte[] ToByteArray() {
MemoryStream newData = new MemoryStream();
using (BinaryWriter writer = new BinaryWriter(newData)) {
List<ContainerReference> scriptOffsets = new List<ContainerReference>(); //uint OFFSET, int Function/Script/Action ID
List<ContainerReference> functionOffsets = new List<ContainerReference>();
List<ContainerReference> actionOffsets = new List<ContainerReference>();
List<ScriptReference> refList = new List<ScriptReference>();
/* Allocate enough space for script pointers, which we do not know yet */
try {
writer.BaseStream.Position += allScripts.Count * 0x4;
writer.Write((ushort)0xFD13); // Signal the end of header section
List<CommandContainer> useScriptCallers = new List<CommandContainer>();
/* Write scripts */
foreach (CommandContainer currentScript in allScripts) {
if (currentScript.usedScript == -1) {
scriptOffsets.Add(new ContainerReference() {
ID = currentScript.manualUserID,
offsetInFile = (uint)writer.BaseStream.Position
}
);
foreach (ScriptCommand currentCmd in currentScript.commands) {
writer.Write((ushort)currentCmd.id);
//System.Diagnostics.Debug.Write(BitConverter.ToString(BitConverter.GetBytes(commandID)) + " ");
List<byte[]> parameterList = currentCmd.cmdParams;
foreach (byte[] b in parameterList) {
writer.Write(b);
//System.Diagnostics.Debug.WriteLine(BitConverter.ToString(parameterList[k]) + " ");
}
/* If command calls a function/movement, store reference position */
AddReference(ref refList, (ushort)currentCmd.id, parameterList, (int)writer.BaseStream.Position, currentScript);
}
} else {
useScriptCallers.Add(currentScript);
}
}
int scriptsCount = scriptOffsets.Count;
foreach (CommandContainer caller in useScriptCallers) {
for (int i = 0; i < scriptsCount; i++) {
ContainerReference scriptReference = scriptOffsets[i];
if (scriptReference.ID == caller.usedScript) {
scriptOffsets.Add(new ContainerReference() {
ID = caller.manualUserID,
offsetInFile = scriptReference.offsetInFile
});// If script has UseScript, copy offset
}
}
}
/* Write functions */
foreach (CommandContainer currentFunction in allFunctions) {
if (currentFunction.usedScript == -1) {
functionOffsets.Add(new ContainerReference() {
ID = currentFunction.manualUserID,
offsetInFile = (uint)writer.BaseStream.Position
}
);
foreach (ScriptCommand currentCmd in currentFunction.commands) {
writer.Write((ushort)currentCmd.id);
//System.Diagnostics.Debug.Write(BitConverter.ToString(BitConverter.GetBytes(commandID)) + " ");
List<byte[]> parameterList = currentCmd.cmdParams;
foreach (byte[] b in parameterList) {
writer.Write(b);
//System.Diagnostics.Debug.Write(BitConverter.ToString(parameterList[k]) + " ");
}
/* If command calls a function/movement, store reference position */
AddReference(ref refList, (ushort)currentCmd.id, parameterList, (int)writer.BaseStream.Position, currentFunction);
}
} else {
int functionUsescript = currentFunction.usedScript - 1;
//Find script with same ID as the function's referenced UseScript
int scriptUsedByFunction = scriptOffsets.FindIndex(ind => ind.ID == currentFunction.usedScript);
if (functionUsescript >= scriptOffsets.Count || scriptUsedByFunction < 0) {
MessageBox.Show($"Function #{currentFunction.manualUserID} refers to Script {currentFunction.usedScript}, which does not exist.\n" +
$"This Script File can't be saved.", "Can't resolve UseScript reference", MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
}
functionOffsets.Add(new ContainerReference() {
ID = currentFunction.manualUserID,
offsetInFile = scriptOffsets[scriptUsedByFunction].offsetInFile
});
}
}
// Movements must be halfword-aligned
if (writer.BaseStream.Position % 2 == 1) { //Check if the writer's head is on an odd byte
writer.Write((byte)0x00); //Add padding
}
/* Write movements */
foreach (ActionContainer currentAction in allActions) {
actionOffsets.Add(new ContainerReference() {
ID = currentAction.manualUserID,
offsetInFile = (uint)writer.BaseStream.Position
});
foreach (ScriptAction currentCmd in currentAction.actionCommandsList) {
writer.Write((ushort)currentCmd.id);
writer.Write((ushort)currentCmd.repetitionCount);
}
}
/* Write script offsets to header */
writer.BaseStream.Position = 0x0;
scriptOffsets = scriptOffsets.OrderBy(x => x.ID).ToList(); //Write script offsets to header in the correct order
for (int i = 0; i < scriptOffsets.Count; i++) {
writer.Write(scriptOffsets[i].offsetInFile - (uint)writer.BaseStream.Position - 0x4);
}
SortedSet<uint> undeclaredFuncs = new SortedSet<uint>();
SortedSet<uint> undeclaredActions = new SortedSet<uint>();
SortedSet<uint> uninvokedFuncs = new SortedSet<uint>(allFunctions.Select(x => x.manualUserID).ToArray());
SortedSet<uint> unreferencedActions = new SortedSet<uint>(allActions.Select(x => x.manualUserID).ToArray());
//refList = refList.OrderBy(x => x.invokedID).ToList(); //Sorting is not necessary, after all...
for (int i = 0; i < refList.Count; i++) {
writer.BaseStream.Position = refList[i].invokedAt; //place seek head on parameter that is supposed to store the jump address
ContainerReference result;
if (refList[i].typeOfInvoked is containerTypes.Action) { //isApplyMovement
result = actionOffsets.Find(entry => entry.ID == refList[i].invokedID);
if (result.Equals(default(ContainerReference))) {
undeclaredActions.Add(refList[i].invokedID);
} else {
int relativeOffset = (int)(result.offsetInFile - refList[i].invokedAt - 4);
writer.Write(relativeOffset);
unreferencedActions.Remove(refList[i].invokedID);
}
} else {
result = functionOffsets.Find(entry => entry.ID == refList[i].invokedID);
if (result.Equals(default(ContainerReference))) {
undeclaredFuncs.Add(refList[i].invokedID);
} else {
int relativeOffset = (int)(result.offsetInFile - refList[i].invokedAt - 4);
writer.Write(relativeOffset);
if (FunctionIsInvoked(refList, uninvokedFuncs, refList[i].invokedID, 0)) {
uninvokedFuncs.Remove(refList[i].invokedID);
}
//if (refList[i].callerType != containerTypes.Function ||
// (refList[i].callerType == refList[i].invokedType && refList[i].callerID == refList[i].invokedID) ||
// !uninvokedFuncs.Contains(refList[i].callerID)) { //remove reference if caller is a script, or if caller calls itself, or if caller is a function that's been invoked already
// uninvokedFuncs.Remove(refList[i].invokedID);
//}
}
}
}
//Error check
string errorMsg = "";
if (undeclaredFuncs.Count > 0) {
string[] errorFunctionsUndeclared = undeclaredFuncs.ToArray().Select(x => x.ToString()).ToArray();
errorMsg += "These Functions have been invoked but not declared: " + Environment.NewLine + string.Join(separator: ",", errorFunctionsUndeclared);
errorMsg += Environment.NewLine;
}
if (undeclaredActions.Count > 0) {
string[] errorActionsUndeclared = undeclaredActions.ToArray().Select(x => x.ToString()).ToArray();
errorMsg += "These Actions have been referenced but not declared: " + Environment.NewLine + string.Join(separator: ",", errorActionsUndeclared);
errorMsg += Environment.NewLine;
}
if (!string.IsNullOrEmpty(errorMsg)) {
MessageBox.Show(errorMsg + Environment.NewLine + "This Script File has not been overwritten since it can not be saved.", "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error);
errorMsg = "";
return null;
}
if (uninvokedFuncs.Count > 0) {
string[] orphanedFunctions = uninvokedFuncs.ToArray().Select(x => x.ToString()).ToArray();
errorMsg += "Unused Function IDs detected: " + Environment.NewLine + string.Join(", ", orphanedFunctions);
errorMsg += Environment.NewLine;
errorMsg += "\nIn order for a Function to be saved, it must be invoked by a Script or by another used Function.";
errorMsg += Environment.NewLine;
errorMsg += Environment.NewLine;
}
if (unreferencedActions.Count > 0) {
string[] orphanedActions = unreferencedActions.ToArray().Select(x => x.ToString()).ToArray();
errorMsg += "Unused Action IDs detected: " + Environment.NewLine + string.Join(", ", orphanedActions);
errorMsg += Environment.NewLine;
errorMsg += "\nIn order for an Action to be saved, it must be called by a Script or by a used Function.";
errorMsg += Environment.NewLine;
errorMsg += Environment.NewLine;
}
if (!string.IsNullOrEmpty(errorMsg)) {
MessageBox.Show(errorMsg + Environment.NewLine + "Remember that every unused Function or Action is always lost upon reloading the Script File.", "Warning!", MessageBoxButtons.OK, MessageBoxIcon.Information);
errorMsg = "";
}
} catch (NullReferenceException nre) {
Console.WriteLine(nre);
return null;
}
}
return newData.ToArray();
}
private bool FunctionIsInvoked(List<ScriptReference> refList, SortedSet<uint> uninvokedFuncsSet, uint funcID, int callCount = 0, uint? excludedCaller = null) {
if (callCount >= 30) {
MessageBox.Show("Something went very wrong saving this Script File!" +
"\nIt is recommended that you backup its code somewhere, to avoid losing progress.",
"Fatal error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
Console.WriteLine("Checking calls of function " + funcID + (excludedCaller == null ? "" : " excluding Function " + excludedCaller + " as the caller."));
if (!uninvokedFuncsSet.Contains(funcID)) {
Console.WriteLine("Function " + funcID + " has already been invoked before. Nothing to check.");
return true; //Abort
}
if (refList is null || refList.Count <= 0) {
return false;
}
//Find the first instance of funcID being called, excluding calls coming from an excludedCaller
//if excludedCaller is null, there's nothing to exclude: a normal search is performed.
ScriptReference sr = refList.Find(x => x.invokedID == funcID && (excludedCaller == null || x.callerID != excludedCaller));
if (sr is null) {
Console.WriteLine("No reference found!!!");
return false;
}
if (sr.typeOfCaller is containerTypes.Script) {
Console.WriteLine("Function " + funcID + " is directly called by Script " + sr.callerID);
return true;
}
if (sr.typeOfCaller is containerTypes.Function) {
if (FunctionIsInvoked(refList, uninvokedFuncsSet, sr.callerID, ++callCount, excludedCaller: sr.invokedID)) { //check if caller function is invoked as well
Console.WriteLine("Function " + funcID + " is called by Function " + sr.callerID);
return true;
}
}
Console.WriteLine("Function " + funcID + " is unused");
return false;
}
public bool SaveToFileDefaultDir(int IDtoReplace, bool showSuccessMessage = true) {
return SaveToFileDefaultDir(DirNames.scripts, IDtoReplace, showSuccessMessage);
}
public void SaveToFileExplorePath(string suggestedFileName, bool blindmode) {
SaveFileDialog sf = new SaveFileDialog {
Filter = "Gen IV Script File (*.scr)|*.scr"
};
if (!string.IsNullOrEmpty(suggestedFileName)) {
sf.FileName = suggestedFileName;
}
if (sf.ShowDialog() != DialogResult.OK) {
return;
}
if (blindmode) {
File.Copy(RomInfo.gameDirs[DirNames.scripts].unpackedDir + "\\" + ((int)fileID).ToString("D4"), sf.FileName, overwrite: true);
string msg = "";
if (!isLevelScript) {
msg += "The last saved version of this ";
}
MessageBox.Show(msg + GetType().Name + " has been exported successfully.", "", MessageBoxButtons.OK, MessageBoxIcon.Information);
} else {
this.SaveToFile(sf.FileName, showSuccessMessage: true);
}
}
#endregion
#endregion
}
internal class ScriptReference {
public containerTypes typeOfCaller { get; private set; }
public uint callerID { get; private set; }
public containerTypes typeOfInvoked { get; private set; }
public uint invokedID { get; private set; }
public int invokedAt { get; private set; }
public ScriptReference(containerTypes typeOfCaller, uint callerID, containerTypes invokedType, uint invokedID, int invokedAt) {
this.typeOfCaller = typeOfCaller;
this.callerID = callerID;
this.typeOfInvoked = invokedType;
this.invokedID = invokedID;
this.invokedAt = invokedAt;
}
public override string ToString() {
return typeOfCaller + " " + callerID + " invokes " + typeOfInvoked + " " + invokedID + " at " + invokedAt;
}
}
}