using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Windows.Forms; using static DSPRE.RomInfo; namespace DSPRE.ROMFiles { /// /// Class to store script file data in Pokémon NDS games /// public class ScriptFile : RomFile { public List CommandSequence { get; set; } = new List(); public Dictionary OffsetToLabelMap { get; set; } = new Dictionary(); public enum ContainerTypes { Action, Script, Label }; public struct ContainerReference { public uint ID; public uint offsetInFile; } public int fileID = -1; public bool isLevelScript = new bool(); public bool HasNoScripts { get { return fileID == int.MaxValue; } } public static readonly char[] specialChars = { 'x', 'X', '#', '.', '_' }; public ScriptFile(Stream fs) { // Initialize collections CommandSequence = new List(); OffsetToLabelMap = new Dictionary(); using (BinaryReader br = new BinaryReader(fs)) { // Read header to find entry points List entryPointOffsets = new List(); isLevelScript = true; // Is Level Script until proved otherwise try { int entryPointIndex = 0; while (true) { long headerPos = br.BaseStream.Position; uint checker = br.ReadUInt16(); br.BaseStream.Position -= 0x2; uint value = br.ReadUInt32(); if (value == 0 && entryPointOffsets.Count == 0) { isLevelScript = true; break; } if (checker == 0xFD13) { br.BaseStream.Position -= 0x4; isLevelScript = false; break; } int offsetFromStart = (int)(value + headerPos + 4); entryPointOffsets.Add(offsetFromStart); // Create entry point label string entryLabel = $"script_{entryPointIndex}"; OffsetToLabelMap[offsetFromStart] = entryLabel; entryPointIndex++; } } catch (EndOfStreamException) { if (!isLevelScript) { MessageBox.Show("Script File couldn't be read correctly.", "Unexpected EOF", MessageBoxButtons.OK, MessageBoxIcon.Error); } } if (isLevelScript) { return; } // Skip the 0xFD13 marker br.ReadUInt16(); // Process commands until end of file while (br.BaseStream.Position < br.BaseStream.Length) { int currentOffset = (int)br.BaseStream.Position; // Sometimes we might hit padding bytes - try to detect and skip if (br.BaseStream.Position + 2 <= br.BaseStream.Length) { byte[] peekBytes = br.ReadBytes(2); br.BaseStream.Position -= 2; // Check if these look like padding if (peekBytes[0] == 0 && peekBytes[1] == 0) { // Skip padding byte br.ReadByte(); continue; } } ScriptCommand cmd = ReadCommand(br); if (cmd == null || cmd.id == null) { break; // End of file or error } // Check if this is an entry point or jump target bool isEntryPoint = false; int entryPointIndex = -1; string label = null; if (OffsetToLabelMap.TryGetValue(currentOffset, out string existingLabel)) { label = existingLabel; isEntryPoint = existingLabel.StartsWith("script_"); if (isEntryPoint) { entryPointIndex = int.Parse(existingLabel.Substring("script_".Length)); } } // Add to command sequence CommandSequence.Add(new ScriptCommandPosition( cmd, currentOffset, label, isEntryPoint, entryPointIndex)); } } } public ScriptFile(int fileID) : this(getFileStream(fileID)) { this.fileID = fileID; } public static FileStream getFileStream(int fileID) { string path = Filesystem.GetScriptPath(fileID); return new FileStream(path, FileMode.OpenOrCreate); } public override string ToString() { string prefix = isLevelScript ? "Level " : ""; return $"{prefix}Script File " + this.fileID; } public ScriptFile(IEnumerable lines, int fileID = -1) { CommandSequence = new List(); OffsetToLabelMap = new Dictionary(); this.fileID = fileID; int currentOffset = 0; string currentLabel = null; int entryPointIndex = 0; bool isCurrentLabelEntryPoint = false; // Track this instead // Parse each line foreach (var line in lines) { string trimmedLine = line.Trim(); // Skip empty lines if (string.IsNullOrWhiteSpace(trimmedLine)) { continue; } // Check if this is a label if (trimmedLine.EndsWith(":")) { currentLabel = trimmedLine.Substring(0, trimmedLine.Length - 1); // Track if it's an entry point isCurrentLabelEntryPoint = currentLabel.StartsWith("script_"); if (isCurrentLabelEntryPoint) { if (int.TryParse(currentLabel.Substring("script_".Length), out int index)) { entryPointIndex = index; } } continue; } // Parse the command ScriptCommand cmd = new ScriptCommand(trimmedLine); if (cmd.id == null) { continue; // Skip invalid commands } // Calculate the size of this command for offset tracking int cmdSize = 2; // Command ID (2 bytes) foreach (var param in cmd.Parameters) { cmdSize += param.RawData.Length; } // Use the tracked entry point flag int epIndex = isCurrentLabelEntryPoint ? entryPointIndex : -1; CommandSequence.Add(new ScriptCommandPosition( cmd, currentOffset, currentLabel, isCurrentLabelEntryPoint, epIndex)); // Map the offset to the label for jump targets if (currentLabel != null) { OffsetToLabelMap[currentOffset] = currentLabel; currentLabel = null; // Reset label isCurrentLabelEntryPoint = false; // Reset entry point flag } // Update offset for next command currentOffset += cmdSize; } } private ScriptCommand ReadCommand(BinaryReader br) { // Check if we've reached the end of the file if (br.BaseStream.Position >= br.BaseStream.Length) { return null; } try { ushort id = br.ReadUInt16(); List parameters = new List(); // Track the original position for jump calculations long commandStartPos = br.BaseStream.Position - 2; switch (gameFamily) { case GameFamilies.Plat: switch (id) { case 0x16: // Jump case 0x1A: // Call ProcessRelativeJumpLinear(br, parameters); break; case 0x17: // JumpIfObjID case 0x18: // JumpIfEventID case 0x19: // JumpIfPlayerDir case 0x1C: // JumpIf case 0x1D: // CallIf // First parameter (condition) parameters.Add(new ScriptParameter(new byte[] { br.ReadByte() })); // Then jump target ProcessRelativeJumpLinear(br, parameters); break; case 0x5E: // Movement parameters.Add(new ScriptParameter(BitConverter.GetBytes(br.ReadUInt16()))); ProcessRelativeJumpLinear(br, parameters); break; case 0x1CF: case 0x1D0: case 0x1D1: { byte parameter1 = br.ReadByte(); parameters.Add(new ScriptParameter(new byte[] { parameter1 })); if (parameter1 == 0x2) { parameters.Add(new ScriptParameter(br.ReadBytes(2))); //Read additional u16 if first param read is 2 } } break; case 0x21D: { ushort parameter1 = br.ReadUInt16(); parameters.Add(new ScriptParameter(BitConverter.GetBytes(parameter1))); switch (parameter1) { case 0: case 1: case 2: case 3: parameters.Add(new ScriptParameter(br.ReadBytes(2))); parameters.Add(new ScriptParameter(br.ReadBytes(2))); break; case 4: case 5: parameters.Add(new ScriptParameter(br.ReadBytes(2))); break; case 6: break; } } break; case 0x235: { short parameter1 = br.ReadInt16(); parameters.Add(new ScriptParameter(BitConverter.GetBytes(parameter1))); switch (parameter1) { case 0x1: case 0x3: parameters.Add(new ScriptParameter(br.ReadBytes(2))); parameters.Add(new ScriptParameter(br.ReadBytes(2))); parameters.Add(new ScriptParameter(br.ReadBytes(2))); break; case 0x4: parameters.Add(new ScriptParameter(br.ReadBytes(2))); parameters.Add(new ScriptParameter(br.ReadBytes(2))); break; case 0x0: case 0x6: parameters.Add(new ScriptParameter(br.ReadBytes(2))); break; default: break; } } break; case 0x23E: { short parameter1 = br.ReadInt16(); parameters.Add(new ScriptParameter(BitConverter.GetBytes(parameter1))); switch (parameter1) { case 0x1: case 0x3: parameters.Add(new ScriptParameter(br.ReadBytes(2))); break; case 0x5: case 0x6: parameters.Add(new ScriptParameter(br.ReadBytes(2))); parameters.Add(new ScriptParameter(br.ReadBytes(2))); break; default: break; } } break; case 0x2C4: { byte parameter1 = br.ReadByte(); parameters.Add(new ScriptParameter(new byte[] { parameter1 })); if (parameter1 == 0 || parameter1 == 1) { parameters.Add(new ScriptParameter(br.ReadBytes(2))); } } break; case 0x2C5: { if (RomInfo.gameVersion == GameVersions.Platinum) { parameters.Add(new ScriptParameter(br.ReadBytes(2))); parameters.Add(new ScriptParameter(br.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) { parameters.Add(new ScriptParameter(br.ReadBytes(2))); parameters.Add(new ScriptParameter(br.ReadBytes(2))); } else { goto default; } break; default: // Standard command handling if (RomInfo.ScriptCommandParametersDict.TryGetValue(id, out byte[] paramSizes)) { foreach (int size in paramSizes) { parameters.Add(new ScriptParameter(br.ReadBytes(size))); } } break; } break; case GameFamilies.HGSS: switch (id) { case 0x16: //Jump case 0x1A: //Call ProcessRelativeJumpLinear(br, parameters); break; case 0x17: //JumpIfObjID case 0x18: //JumpIfBgID case 0x19: //JumpIfPlayerDir case 0x1C: //JumpIf case 0x1D: //CallIf parameters.Add(new ScriptParameter(new byte[] { br.ReadByte() })); //in the case of JumpIf and CallIf, the first param is a comparisonOperator ProcessRelativeJumpLinear(br, parameters); break; case 0x5E: // Movement parameters.Add(new ScriptParameter(BitConverter.GetBytes(br.ReadUInt16()))); //in the case of Movement, the first param is an overworld ID ProcessRelativeJumpLinear(br, parameters); break; case 0x190: case 0x191: case 0x192: { byte parameter1 = br.ReadByte(); parameters.Add(new ScriptParameter(new byte[] { parameter1 })); if (parameter1 == 0x2) { parameters.Add(new ScriptParameter(br.ReadBytes(2))); } } break; case 0x1D1: // Number of parameters differ depending on the first parameter value { short parameter1 = br.ReadInt16(); parameters.Add(new ScriptParameter(BitConverter.GetBytes(parameter1))); switch (parameter1) { case 0x0: case 0x1: case 0x2: case 0x3: parameters.Add(new ScriptParameter(br.ReadBytes(2))); parameters.Add(new ScriptParameter(br.ReadBytes(2))); break; case 0x4: case 0x5: parameters.Add(new ScriptParameter(br.ReadBytes(2))); break; case 0x6: break; case 0x7: parameters.Add(new ScriptParameter(br.ReadBytes(2))); break; default: break; } } break; case 0x1E9: // Number of parameters differ depending on the first parameter value { short parameter1 = br.ReadInt16(); parameters.Add(new ScriptParameter(BitConverter.GetBytes(parameter1))); switch (parameter1) { case 0x0: break; case 0x1: case 0x2: case 0x3: parameters.Add(new ScriptParameter(br.ReadBytes(2))); break; case 0x4: break; case 0x5: case 0x6: parameters.Add(new ScriptParameter(br.ReadBytes(2))); parameters.Add(new ScriptParameter(br.ReadBytes(2))); break; case 0x7: case 0x8: break; default: break; } } break; default: // For standard commands, read parameters based on definition if (RomInfo.ScriptCommandParametersDict.TryGetValue(id, out byte[] paramSizes)) { foreach (int size in paramSizes) { parameters.Add(new ScriptParameter(br.ReadBytes(size))); } } break; } break; } return new ScriptCommand(id, parameters); } catch (Exception ex) { Console.WriteLine($"Error reading command at offset {br.BaseStream.Position}: {ex.Message}"); return null; } } private void ProcessRelativeJumpLinear(BinaryReader br, List parameters) { // Read the relative offset int relativeOffset = br.ReadInt32(); // Calculate absolute target int targetOffset = (int)(relativeOffset + br.BaseStream.Position); // Add to label map if not already there if (!OffsetToLabelMap.ContainsKey(targetOffset)) { string labelName = $"label_0x{targetOffset:X}"; OffsetToLabelMap[targetOffset] = labelName; } // Create jump parameter string targetLabel = OffsetToLabelMap[targetOffset]; ScriptParameter jumpParam = new ScriptParameter(relativeOffset, targetLabel) { Type = ScriptParameter.ParameterType.RelativeJump }; parameters.Add(jumpParam); } // Convert to text - outputs commands in the order they appear in the binary public string ToText() { StringBuilder sb = new StringBuilder(); foreach (var cmdPos in CommandSequence) { // If this command needs a label, output it if (!string.IsNullOrEmpty(cmdPos.Label)) { sb.AppendLine($"\n{cmdPos.Label}:"); } // Output the command (indented if not an entry point) string indent = "\t"; sb.AppendLine($"{indent}{cmdPos.Command.name}"); } return sb.ToString(); } public override byte[] ToByteArray() { MemoryStream newData = new MemoryStream(); using (BinaryWriter writer = new BinaryWriter(newData)) { // First, find all entry points and their positions in the command sequence var entryPoints = CommandSequence .Where(c => c.IsEntryPoint) .OrderBy(c => c.EntryPointIndex) .ToList(); // Allocate space for header long headerStart = writer.BaseStream.Position; writer.BaseStream.Position += entryPoints.Count * 4; writer.Write((ushort)0xFD13); // End of header marker // Keep track of command offsets in the new file Dictionary oldOffsetToNewOffset = new Dictionary(); // Write all commands sequentially foreach (var cmdPos in CommandSequence) { // Record the position of this command oldOffsetToNewOffset[cmdPos.Offset] = writer.BaseStream.Position; // Write the command writer.Write((ushort)cmdPos.Command.id); // Write parameters, handling jumps specially foreach (var param in cmdPos.Command.Parameters) { if (param.Type == ScriptParameter.ParameterType.RelativeJump) { // For jump targets, we need to recalculate the relative offset // based on the new positions of commands // Find the target offset in the original file int targetOffset = -1; foreach (var kvp in OffsetToLabelMap) { if (kvp.Value == param.TargetLabel) { targetOffset = kvp.Key; break; } } if (targetOffset != -1 && oldOffsetToNewOffset.TryGetValue(targetOffset, out long newTargetOffset)) { // Calculate new relative offset int relativeOffset = (int)(newTargetOffset - (writer.BaseStream.Position + 4)); writer.Write(relativeOffset); } else { // Fallback - write original offset writer.Write(param.TargetOffset); } } else { // Regular parameter writer.Write(param.RawData); } } } // Update header with entry point offsets writer.BaseStream.Position = headerStart; foreach (var entryPoint in entryPoints) { if (oldOffsetToNewOffset.TryGetValue(entryPoint.Offset, out long newOffset)) { uint relativeOffset = (uint)(newOffset - (headerStart + 4)); writer.Write(relativeOffset); } } } return newData.ToArray(); } public bool SaveToFileDefaultDir(int IDtoReplace, bool showSuccessMessage = true) { return SaveToFileDefaultDir(RomInfo.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) { string path = Filesystem.GetScriptPath(fileID); File.Copy(path, 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); } } } }