diff --git a/DS_Map/Editors/ScriptEditor.cs b/DS_Map/Editors/ScriptEditor.cs index f124135..e90c92d 100644 --- a/DS_Map/Editors/ScriptEditor.cs +++ b/DS_Map/Editors/ScriptEditor.cs @@ -1,3 +1,4 @@ +using DSPRE.Editors.Utils; using DSPRE.Resources; using DSPRE.ROMFiles; using ScintillaNET; @@ -10,6 +11,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using System.Threading.Tasks; using System.Windows.Forms; namespace DSPRE.Editors { @@ -98,10 +100,200 @@ namespace DSPRE.Editors Helpers.statusLabelMessage("Setting up Script Editor..."); Update(); DSUtils.TryUnpackNarcs(new List { RomInfo.DirNames.scripts }); //12 = scripts Narc Dir + + if (Resources.ScriptDatabase.pokemonNames == null) + { + // Unpack text archives first if not unpacked yet + DSUtils.TryUnpackNarcs(new List { RomInfo.DirNames.textArchives }); + + Resources.ScriptDatabase.InitializePokemonNames(); + Resources.ScriptDatabase.InitializeItemNames(); + Resources.ScriptDatabase.InitializeMoveNames(); + Resources.ScriptDatabase.InitializeTrainerNames(); + } + + // Export scripts on first open with progress dialog, same as text archives + int scriptCount = Filesystem.GetScriptCount(); + using (var loadingForm = new LoadingForm(scriptCount, "Loading script files...")) + { + loadingForm.Shown += (s, e) => + { + Task.Run(() => + { + ROMFiles.ScriptFile.ExportAllScripts( + suppressErrors: true, + progressCallback: (current, total) => + { + if (loadingForm.IsHandleCreated) + { + loadingForm.Invoke((Action)(() => loadingForm.UpdateProgress(current))); + } + }); + + if (loadingForm.IsHandleCreated) + { + loadingForm.Invoke((Action)(() => loadingForm.Close())); + } + }); + }; + loadingForm.ShowDialog(); + + var invalidCommands = ROMFiles.ScriptFile.GetInvalidCommands(); + if (invalidCommands.Count > 0) + { + HandleInvalidScriptCommands(invalidCommands); + } + } + populate_selectScriptFileComboBox(0); UpdateScriptNumberCheckBox((NumberStyles)SettingsManager.Settings.scriptEditorFormatPreference); Helpers.statusLabelMessage(); } + + private void HandleInvalidScriptCommands(List<(int fileID, ushort commandID, long offset)> invalidCommands) + { + var uniqueCommands = invalidCommands.Select(c => c.commandID).Distinct().OrderBy(x => x).ToList(); + string commandList = string.Join(", ", uniqueCommands.Select(c => $"0x{c:X4}")); + + var affectedFiles = invalidCommands.Select(c => c.fileID).Distinct().OrderBy(x => x).ToList(); + string fileList = string.Join(", ", affectedFiles.Select(f => f.ToString("D4"))); + + var result = MessageBox.Show( + $"DSPRE could not interpret {invalidCommands.Count} script command(s) across {affectedFiles.Count} file(s).\n\n" + + $"Affected files: {fileList}\n" + + $"Unrecognized commands: {commandList}\n\n" + + $"This may happen if your project uses custom script commands. " + + $"Would you like to load a custom script command database for this project?\n\n" + + $"If you select 'Yes', DSPRE will reparse all scripts with the new database.\nIf you select 'No', the affected files will be incomplete and read-only to prevent data loss.", + "Invalid Script Commands Detected", + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning); + + if (result == DialogResult.Yes) + { + LoadCustomScriptDatabase(); + } + } + + private void LoadCustomScriptDatabase() + { + using (OpenFileDialog dialog = new OpenFileDialog()) + { + dialog.Title = "Select Custom Script Command Database"; + dialog.Filter = "JSON files (*.json)|*.json|All files (*.*)|*.*"; + dialog.InitialDirectory = Program.DatabasePath; + + if (dialog.ShowDialog() == DialogResult.OK) + { + try + { + Helpers.statusLabelMessage("Loading custom script database..."); + Update(); + + // Save database permanently to edited_databases + string editedDatabasesDir = Path.Combine(Program.DatabasePath, "edited_databases"); + Directory.CreateDirectory(editedDatabasesDir); + + string baseFileName = Path.GetFileNameWithoutExtension(RomInfo.projectName); + string romFileNameClean = baseFileName.EndsWith("_DSPRE_contents") + ? baseFileName.Substring(0, baseFileName.Length - "_DSPRE_contents".Length) + : baseFileName; + + string targetJsonPath = Path.Combine(editedDatabasesDir, $"{romFileNameClean}_scrcmd_database.json"); + + File.Copy(dialog.FileName, targetJsonPath, overwrite: true); + AppLogger.Info($"Script database saved permanently to: {targetJsonPath}"); + + ScriptDatabaseJsonLoader.InitializeFromJson(targetJsonPath, RomInfo.gameVersion); + + // Rebuild command dictionaries + RomInfo.ReloadScriptCommandDictionaries(); + + // Reinitialize name dictionaries with the new database + Resources.ScriptDatabase.InitializePokemonNames(); + Resources.ScriptDatabase.InitializeItemNames(); + Resources.ScriptDatabase.InitializeMoveNames(); + Resources.ScriptDatabase.InitializeTrainerNames(); + + // Clear the expanded scripts directory to force re-export + string expandedDir = Path.Combine(RomInfo.workDir, "expanded", "scripts"); + if (Directory.Exists(expandedDir)) + { + Directory.Delete(expandedDir, true); + } + + // Re-export with new database and progress dialog + int scriptCount = Filesystem.GetScriptCount(); + using (var loadingForm = new LoadingForm(scriptCount, "Reparsing scripts with new database...")) + { + // Start the background task after the form is shown + loadingForm.Shown += (s, e) => + { + Task.Run(() => + { + ROMFiles.ScriptFile.ExportAllScripts( + suppressErrors: false, + progressCallback: (current, total) => + { + if (loadingForm.IsHandleCreated) + { + loadingForm.Invoke((Action)(() => loadingForm.UpdateProgress(current))); + } + }); + + // Close the form when done + if (loadingForm.IsHandleCreated) + { + loadingForm.Invoke((Action)(() => loadingForm.Close())); + } + }); + }; + + // ShowDialog to keep the form modal while allowing background processing + loadingForm.ShowDialog(); + } + + // Refresh the script editor + SetupScriptEditorTextAreas(); + populate_selectScriptFileComboBox(selectScriptFileComboBox.SelectedIndex); + + Helpers.statusLabelMessage(); + + // Check if there are still errors after loading custom database + var remainingInvalidCommands = ROMFiles.ScriptFile.GetInvalidCommands(); + if (remainingInvalidCommands.Count > 0) + { + var uniqueCommands = remainingInvalidCommands.Select(c => c.commandID).Distinct().OrderBy(x => x).ToList(); + string commandList = string.Join(", ", uniqueCommands.Select(c => $"0x{c:X4}")); + + var affectedFiles = remainingInvalidCommands.Select(c => c.fileID).Distinct().OrderBy(x => x).ToList(); + string fileList = string.Join(", ", affectedFiles.Select(f => f.ToString("D4"))); + + MessageBox.Show( + $"Script database loaded, but {remainingInvalidCommands.Count} script command(s) across {affectedFiles.Count} file(s) still could not be parsed.\n\n" + + $"Affected files: {fileList}\n" + + $"Unrecognized commands: {commandList}\n\n" + + $"Affected script files were not exported to plaintext to prevent data loss. " + + $"You can load a different database or manually fix the database file to add these commands.", + "Partial Success", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + } + else + { + MessageBox.Show("Script database loaded successfully and all scripts have been reparsed.", + "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + } + catch (Exception ex) + { + Helpers.statusLabelMessage(); + MessageBox.Show($"Failed to load custom script database:\n{ex.Message}", + "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + } public void OpenScriptEditor(MainProgram parent, int scriptFileID) { SetupScriptEditor(parent); @@ -411,6 +603,19 @@ namespace DSPRE.Editors /* Create new ScriptFile object using the values in the script editor */ int fileID = currentScriptFile.fileID; + if (currentScriptFile.parseFailedDueToInvalidCommand) + { + MessageBox.Show( + "This script file could not be fully parsed due to unrecognized commands and is READ-ONLY.\n\n" + + "To fix this, load a custom script command database that includes all commands used in this script, " + + "modify your db to include changes you made, " + + "or restore the script file from a backup.", + "Cannot Save Incomplete Script", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + return; + } + ScriptTextArea.ReadOnly = true; FunctionTextArea.ReadOnly = true; ActionTextArea.ReadOnly = true; @@ -677,6 +882,28 @@ namespace DSPRE.Editors displayScriptFile(ScriptFile.ContainerTypes.Script, currentScriptFile.allScripts, scriptsNavListbox, ScriptTextArea); displayScriptFile(ScriptFile.ContainerTypes.Function, currentScriptFile.allFunctions, functionsNavListbox, FunctionTextArea); displayScriptFileActions(ScriptFile.ContainerTypes.Action, currentScriptFile.allActions, actionsNavListbox, ActionTextArea); + + if (currentScriptFile.parseFailedDueToInvalidCommand) + { + MessageBox.Show( + $"Warning: Script file {currentScriptFile.fileID:D4} could not be fully parsed due to unrecognized script commands.\n\n" + + "The script shown below is INCOMPLETE and may be missing commands at the end.\n" + + "This script is READ-ONLY to prevent data loss.\n\n" + + "To fix this, load a custom script command database that includes all commands used in this script, modify your existing one to account for changes you made or try to restore this script file if it is corrupted.", + "Incomplete Script File", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + + ScriptTextArea.ReadOnly = true; + FunctionTextArea.ReadOnly = true; + ActionTextArea.ReadOnly = true; + } + else + { + ScriptTextArea.ReadOnly = false; + FunctionTextArea.ReadOnly = false; + ActionTextArea.ReadOnly = false; + } } ScriptEditorSetClean(); @@ -701,15 +928,18 @@ namespace DSPRE.Editors /* If current command is identical to another, print UseScript instead of commands */ if (scriptCommandContainer.usedScriptID < 0) { - for (int j = 0; j < scriptCommandContainer.commands.Count; j++) + if (scriptCommandContainer.commands != null) { - ScriptCommand command = scriptCommandContainer.commands[j]; - if (!ScriptDatabase.endCodes.Contains(command.id)) + for (int j = 0; j < scriptCommandContainer.commands.Count; j++) { - buffer += '\t'; - } + ScriptCommand command = scriptCommandContainer.commands[j]; + if (command.id != null && !ScriptDatabase.endCodes.Contains((ushort)command.id)) + { + buffer += '\t'; + } - buffer += command.name + Environment.NewLine; + buffer += command.name + Environment.NewLine; + } } } else diff --git a/DS_Map/Helpers.cs b/DS_Map/Helpers.cs index 3d9fa9c..0409b2b 100644 --- a/DS_Map/Helpers.cs +++ b/DS_Map/Helpers.cs @@ -191,11 +191,6 @@ namespace DSPRE { ScriptDatabaseJsonLoader.InitializeFromJson(targetJsonPath, gameVersion); - // Initialize name dictionaries for script parameter parsing - ScriptDatabase.InitializePokemonNames(); - ScriptDatabase.InitializeItemNames(); - ScriptDatabase.InitializeMoveNames(); - ScriptDatabase.InitializeTrainerNames(); } catch (Exception ex) { diff --git a/DS_Map/Main Window.cs b/DS_Map/Main Window.cs index 3ab6b89..58db7c3 100644 --- a/DS_Map/Main Window.cs +++ b/DS_Map/Main Window.cs @@ -982,8 +982,6 @@ namespace DSPRE Helpers.statusLabelMessage(); this.Text += " - " + RomInfo.projectName; - - ScriptFile.ExportAllScripts(); } private void saveRom_Click(object sender, EventArgs e) diff --git a/DS_Map/ROMFiles/ScriptFile.cs b/DS_Map/ROMFiles/ScriptFile.cs index b2e0d4e..f0619e7 100644 --- a/DS_Map/ROMFiles/ScriptFile.cs +++ b/DS_Map/ROMFiles/ScriptFile.cs @@ -17,6 +17,9 @@ namespace DSPRE.ROMFiles // Cache for parsed plaintext scripts to avoid reparsing during search private static Dictionary plaintextCache = new Dictionary(); + private static List<(int fileID, ushort commandID, long offset)> invalidCommandsEncountered = new List<(int, ushort, long)>(); + private static bool suppressInvalidCommandErrors = false; + /// /// Clears the plaintext script cache. Useful when closing ROM or reloading. /// @@ -26,6 +29,24 @@ namespace DSPRE.ROMFiles AppLogger.Info("Script: Plaintext cache cleared."); } + public static List<(int fileID, ushort commandID, long offset)> GetInvalidCommands() + { + return new List<(int, ushort, long)>(invalidCommandsEncountered); + } + + public static void ClearInvalidCommands() + { + invalidCommandsEncountered.Clear(); + } + + /// + /// Controls whether invalid command errors should show popups (false) or be collected silently (true) + /// + public static void SetSuppressInvalidCommandErrors(bool suppress) + { + suppressInvalidCommandErrors = suppress; + } + public enum ContainerTypes { Function, @@ -44,6 +65,7 @@ namespace DSPRE.ROMFiles public List allActions = new List(); public int fileID = -1; public bool isLevelScript = new bool(); + public bool parseFailedDueToInvalidCommand = false; public bool hasNoScripts { get { return fileID == int.MaxValue; } } @@ -112,23 +134,34 @@ namespace DSPRE.ROMFiles List cmdList = new List(); bool endScript = new bool(); + bool invalidCommandFound = false; while (!endScript) { ScriptCommand cmd = ReadCommand(br, ref functionOffsets, ref movementOffsets); if (cmd.cmdParams is null) { - return; - } - - cmdList.Add(cmd); - - if (ScriptDatabase.endCodes.Contains((ushort)cmd.id)) - { + parseFailedDueToInvalidCommand = true; + invalidCommandFound = true; endScript = true; } + else + { + cmdList.Add(cmd); + + if (ScriptDatabase.endCodes.Contains((ushort)cmd.id)) + { + endScript = true; + } + } } allScripts.Add(new ScriptCommandContainer(current + 1, ContainerTypes.Script, commandList: cmdList)); + + if (invalidCommandFound) + { + AppLogger.Warn($"Script file {fileID}: Stopped parsing at Script {current + 1} due to invalid command. Remaining scripts, functions, and actions will not be loaded."); + return; + } } else { @@ -148,22 +181,33 @@ namespace DSPRE.ROMFiles { List cmdList = new List(); bool endFunction = new bool(); + bool invalidCommandFound = false; while (!endFunction) { ScriptCommand command = ReadCommand(br, ref functionOffsets, ref movementOffsets); if (command.cmdParams is null) { - return; - } - - cmdList.Add(command); - if (ScriptDatabase.endCodes.Contains((ushort)command.id)) - { + parseFailedDueToInvalidCommand = true; + invalidCommandFound = true; endFunction = true; } + else + { + cmdList.Add(command); + if (ScriptDatabase.endCodes.Contains((ushort)command.id)) + { + endFunction = true; + } + } } allFunctions.Add(new ScriptCommandContainer(current + 1, ContainerTypes.Function, commandList: cmdList)); + + if (invalidCommandFound) + { + AppLogger.Warn($"Script file {fileID}: Stopped parsing at Function {current + 1} due to invalid command. Remaining functions and actions will not be loaded."); + return; + } } else { @@ -218,6 +262,7 @@ namespace DSPRE.ROMFiles this.allFunctions = tempScript.allFunctions; this.allActions = tempScript.allActions; this.isLevelScript = tempScript.isLevelScript; + this.parseFailedDueToInvalidCommand = tempScript.parseFailedDueToInvalidCommand; } } @@ -432,6 +477,12 @@ namespace DSPRE.ROMFiles if (fileID < 0) return; + if (parseFailedDueToInvalidCommand) + { + AppLogger.Warn($"Script file {fileID:D4} was not fully parsed due to invalid commands. Skipping plaintext export to prevent data loss."); + return; + } + string txtPath = GetFilePaths(fileID).txtPath; Directory.CreateDirectory(Path.GetDirectoryName(txtPath)); @@ -479,19 +530,33 @@ namespace DSPRE.ROMFiles /// Exports all script files from the ROM to expanded/scripts/ on initial load /// This ensures the expanded directory is populated with plaintext versions /// - public static void ExportAllScripts() + /// If true, collects invalid command errors silently instead of showing popups + /// Optional callback to report progress (current index, total count) + /// True if export completed, false if cancelled or failed + public static bool ExportAllScripts(bool suppressErrors = false, Action progressCallback = null) { string expandedDir = Path.Combine(RomInfo.workDir, "expanded", "scripts"); - // Skip if the directory already has script files (already exported previously) - if (Directory.Exists(expandedDir) && Directory.GetFiles(expandedDir, "*.script").Length > 0) + if (Directory.Exists(expandedDir)) { - AppLogger.Info($"Script: expanded/scripts already exists with {Directory.GetFiles(expandedDir, "*.script").Length} files, skipping initial export."); - return; + var existingFiles = Directory.GetFiles(expandedDir, "*.script"); + int scriptCount = Filesystem.GetScriptCount(); + + // If we have all script files, skip re-export + if (existingFiles.Length >= scriptCount) + { + AppLogger.Info($"Script: expanded/scripts already exists with {existingFiles.Length} files, skipping initial export."); + return true; + } + + AppLogger.Info($"Script: expanded/scripts exists with only {existingFiles.Length}/{scriptCount} files. Re-exporting to fill in missing scripts."); } Directory.CreateDirectory(expandedDir); + ClearInvalidCommands(); + SetSuppressInvalidCommandErrors(suppressErrors); + try { // Get count of script files @@ -504,6 +569,15 @@ namespace DSPRE.ROMFiles { try { + string txtPath = GetFilePaths(i).txtPath; + + if (File.Exists(txtPath)) + { + AppLogger.Debug($"Script {i:D4} plaintext already exists, skipping to preserve any edits."); + progressCallback?.Invoke(i + 1, scriptCount); + continue; + } + // Load from binary only (don't try to read plaintext since we're creating it) using (var fs = getFileStream(i)) { @@ -525,13 +599,19 @@ namespace DSPRE.ROMFiles { AppLogger.Error($"Failed to export script {i:D4}: {ex.Message}"); } + + progressCallback?.Invoke(i + 1, scriptCount); } AppLogger.Info($"Script: Exported {exportedCount} of {scriptCount} script files to {expandedDir}"); + SetSuppressInvalidCommandErrors(false); + return true; } catch (Exception ex) { AppLogger.Error($"Failed to export scripts: {ex.Message}"); + SetSuppressInvalidCommandErrors(false); + return false; } } @@ -960,15 +1040,27 @@ namespace DSPRE.ROMFiles } 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); + long offset = dataReader.BaseStream.Position; + invalidCommandsEncountered.Add((fileID, id, offset)); + + if (!suppressInvalidCommandErrors) + { + MessageBox.Show("Script command " + id + "can't be handled for now." + + Environment.NewLine + "Reference offset 0x" + offset.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); + long offset = dataReader.BaseStream.Position; + invalidCommandsEncountered.Add((fileID, id, offset)); + + if (!suppressInvalidCommandErrors) + { + MessageBox.Show("Error: ID Read - " + id + + Environment.NewLine + "Reference offset 0x" + offset.ToString("X"), "Unrecognized script command", MessageBoxButtons.OK, MessageBoxIcon.Error); + } parameterList = null; return; } diff --git a/DS_Map/RomInfo.cs b/DS_Map/RomInfo.cs index 2281f93..33814ed 100644 --- a/DS_Map/RomInfo.cs +++ b/DS_Map/RomInfo.cs @@ -258,6 +258,17 @@ namespace DSPRE Helpers.InitializeScriptDatabase(projectName, gameFamily, gameVersion); } + public static void ReloadScriptCommandDictionaries() + { + ScriptCommandParametersDict = BuildCommandParametersDatabase(gameFamily); + ScriptCommandNamesDict = BuildCommandNamesDatabase(gameFamily); + ScriptActionNamesDict = BuildActionNamesDatabase(gameFamily); + ScriptComparisonOperatorsDict = BuildComparisonOperatorsDatabase(gameFamily); + ScriptCommandNamesReverseDict = ScriptCommandNamesDict.Reverse(); + ScriptActionNamesReverseDict = ScriptActionNamesDict.Reverse(); + ScriptComparisonOperatorsReverseDict = ScriptComparisonOperatorsDict.Reverse(); + } + public static Dictionary GetScriptCommandInfoDict() { switch (gameFamily)