mirror of
https://github.com/AdAstra-LD/DS-Pokemon-Rom-Editor.git
synced 2026-09-07 18:55:57 -05:00
scripts plaintext parsing + simple caching for consecutive searches
This commit is contained in:
@@ -129,6 +129,13 @@ Game-specific paths are defined in `RomInfo.gameDirs`.
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Code Style and Formatting
|
||||
|
||||
**IMPORTANT: Avoid Useless Comments**
|
||||
- Do NOT write comments that simply restate what the code does
|
||||
- Comments should only explain "why" the code exists, not "what" it does
|
||||
- Only add comments when there's a non-obvious reason, tricky logic, or important context
|
||||
|
||||
### ROM File Editing Pattern
|
||||
When editing ROM data:
|
||||
1. Load ROM project (unpacks to working directory)
|
||||
|
||||
@@ -618,8 +618,7 @@ namespace DSPRE.Editors
|
||||
Helpers.DisableHandlers();
|
||||
|
||||
ScriptFile lastScriptFile = currentScriptFile;
|
||||
// currentScriptFile = (ScriptFile)selectScriptFileComboBox.SelectedItem;
|
||||
currentScriptFile = new ScriptFile(selectScriptFileComboBox.SelectedIndex); // Load script file
|
||||
int fileID = selectScriptFileComboBox.SelectedIndex;
|
||||
|
||||
ScriptTextArea.ClearAll();
|
||||
FunctionTextArea.ClearAll();
|
||||
@@ -629,6 +628,18 @@ namespace DSPRE.Editors
|
||||
functionsNavListbox.Items.Clear();
|
||||
actionsNavListbox.Items.Clear();
|
||||
|
||||
if (TryLoadPlaintextDirect(fileID, out bool isLevelScript))
|
||||
{
|
||||
// Create a minimal ScriptFile just to hold the fileID for saving later
|
||||
currentScriptFile = new ScriptFile(new List<ScriptCommandContainer>(), new List<ScriptCommandContainer>(), new List<ScriptActionContainer>(), fileID);
|
||||
currentScriptFile.isLevelScript = isLevelScript;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: load and parse normally (binary or older plaintext)
|
||||
currentScriptFile = new ScriptFile(fileID);
|
||||
}
|
||||
|
||||
//prevent buttons from flickering when the combobox selection changes
|
||||
bool typeChanged = true;
|
||||
if (lastScriptFile != null)
|
||||
@@ -888,40 +899,12 @@ namespace DSPRE.Editors
|
||||
{
|
||||
string content = File.ReadAllText(of.FileName);
|
||||
|
||||
// Split content into sections
|
||||
const string SCRIPTS_HEADER = "//===== SCRIPTS =====//";
|
||||
const string FUNCTIONS_HEADER = "//===== FUNCTIONS =====//";
|
||||
const string ACTIONS_HEADER = "//===== ACTIONS =====//";
|
||||
|
||||
int scriptsStart = content.IndexOf(SCRIPTS_HEADER);
|
||||
int functionsStart = content.IndexOf(FUNCTIONS_HEADER);
|
||||
int actionsStart = content.IndexOf(ACTIONS_HEADER);
|
||||
|
||||
if (scriptsStart == -1 || functionsStart == -1 || actionsStart == -1)
|
||||
// Use shared loading logic (don't populate nav lists - user will save/reload)
|
||||
if (!LoadPlaintextIntoEditor(content, false, out _))
|
||||
{
|
||||
throw new FormatException("Invalid script file format. Missing required section headers.");
|
||||
}
|
||||
|
||||
// Extract each section's content
|
||||
string scripts = content.Substring(
|
||||
scriptsStart + SCRIPTS_HEADER.Length,
|
||||
functionsStart - (scriptsStart + SCRIPTS_HEADER.Length)
|
||||
).Trim();
|
||||
|
||||
string functions = content.Substring(
|
||||
functionsStart + FUNCTIONS_HEADER.Length,
|
||||
actionsStart - (functionsStart + FUNCTIONS_HEADER.Length)
|
||||
).Trim();
|
||||
|
||||
string actions = content.Substring(
|
||||
actionsStart + ACTIONS_HEADER.Length
|
||||
).Trim();
|
||||
|
||||
// Update text areas
|
||||
ScriptTextArea.Text = scripts;
|
||||
FunctionTextArea.Text = functions;
|
||||
ActionTextArea.Text = actions;
|
||||
|
||||
MessageBox.Show("Script file imported successfully!", "", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -1127,33 +1110,158 @@ namespace DSPRE.Editors
|
||||
searchInScriptsButton_Click(null, null);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Tries to load plaintext directly without parsing for fast display.
|
||||
/// Only works if plaintext exists and is newer than binary.
|
||||
/// Returns true if successful, false if parsing is needed.
|
||||
/// </summary>
|
||||
private bool TryLoadPlaintextDirect(int fileID, out bool isLevelScript)
|
||||
{
|
||||
isLevelScript = false;
|
||||
|
||||
var (binPath, txtPath) = ScriptFile.GetFilePaths(fileID);
|
||||
|
||||
if (!File.Exists(txtPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (File.Exists(binPath) && File.GetLastWriteTimeUtc(txtPath) < File.GetLastWriteTimeUtc(binPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string content = File.ReadAllText(txtPath);
|
||||
if (!LoadPlaintextIntoEditor(content, true, out isLevelScript))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AppLogger.Info($"Script file {fileID:D4} loaded directly from plaintext (fast path)");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Error($"Fast plaintext loading failed for {fileID:D4}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared logic to load plaintext content into editor text areas.
|
||||
/// Used by both auto-loading and manual import.
|
||||
/// </summary>
|
||||
/// <param name="content">The plaintext file content</param>
|
||||
/// <param name="populateNavLists">Whether to populate navigation lists (true for load, false for import that's followed by reload)</param>
|
||||
/// <param name="isLevelScript">Output: whether this is a level script</param>
|
||||
/// <returns>True if successful, false if format is invalid</returns>
|
||||
private bool LoadPlaintextIntoEditor(string content, bool populateNavLists, out bool isLevelScript)
|
||||
{
|
||||
isLevelScript = false;
|
||||
|
||||
// Extract sections
|
||||
const string SCRIPTS_HEADER = "//===== SCRIPTS =====//";
|
||||
const string FUNCTIONS_HEADER = "//===== FUNCTIONS =====//";
|
||||
const string ACTIONS_HEADER = "//===== ACTIONS =====//";
|
||||
|
||||
int scriptsStart = content.IndexOf(SCRIPTS_HEADER);
|
||||
int functionsStart = content.IndexOf(FUNCTIONS_HEADER);
|
||||
int actionsStart = content.IndexOf(ACTIONS_HEADER);
|
||||
|
||||
if (scriptsStart == -1 || functionsStart == -1 || actionsStart == -1)
|
||||
{
|
||||
return false; // Invalid format
|
||||
}
|
||||
|
||||
string scriptsSection = content.Substring(
|
||||
scriptsStart + SCRIPTS_HEADER.Length,
|
||||
functionsStart - scriptsStart - SCRIPTS_HEADER.Length
|
||||
).Trim();
|
||||
|
||||
string functionsSection = content.Substring(
|
||||
functionsStart + FUNCTIONS_HEADER.Length,
|
||||
actionsStart - functionsStart - FUNCTIONS_HEADER.Length
|
||||
).Trim();
|
||||
|
||||
string actionsSection = content.Substring(
|
||||
actionsStart + ACTIONS_HEADER.Length
|
||||
).Trim();
|
||||
|
||||
// Directly dump text into text areas (no parsing!)
|
||||
ScriptTextArea.Text = scriptsSection;
|
||||
FunctionTextArea.Text = functionsSection;
|
||||
ActionTextArea.Text = actionsSection;
|
||||
|
||||
// Optionally populate navigation lists
|
||||
if (populateNavLists)
|
||||
{
|
||||
PopulateNavListFromText(scriptsSection, scriptsNavListbox, "Script");
|
||||
PopulateNavListFromText(functionsSection, functionsNavListbox, "Function");
|
||||
PopulateNavListFromText(actionsSection, actionsNavListbox, "Action");
|
||||
}
|
||||
|
||||
// Check if it's a level script (no scripts, only functions)
|
||||
isLevelScript = string.IsNullOrWhiteSpace(scriptsSection) || !scriptsSection.Contains("Script ");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populates navigation listbox by counting "Type X:" headers in text
|
||||
/// </summary>
|
||||
private void PopulateNavListFromText(string text, ListBox navListBox, string containerType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return;
|
||||
|
||||
var lines = text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
string trimmed = line.Trim();
|
||||
if (trimmed.StartsWith(containerType + " ", StringComparison.InvariantCultureIgnoreCase) && trimmed.Contains(':'))
|
||||
{
|
||||
// Extract "Script 1:" or "Function 2:" etc.
|
||||
int colonIndex = trimmed.IndexOf(':');
|
||||
if (colonIndex > 0)
|
||||
{
|
||||
string header = trimmed.Substring(0, colonIndex);
|
||||
navListBox.Items.Add(header);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<ScriptFile> getScriptsToSearch()
|
||||
{
|
||||
List<ScriptFile> scriptsToSearch = new List<ScriptFile>();
|
||||
if (searchOnlyCurrentScriptCheckBox.Checked)
|
||||
{
|
||||
this.UIThread(() => {
|
||||
this.UIThread(() =>
|
||||
{
|
||||
searchProgressBar.Maximum = 1;
|
||||
});
|
||||
int i = selectScriptFileComboBox.SelectedIndex;
|
||||
ScriptFile scriptFile = new ScriptFile(i);
|
||||
AppLogger.Debug("Attempting to load script " + scriptFile.fileID);
|
||||
scriptsToSearch.Add(scriptFile);
|
||||
this.UIThread(() => {
|
||||
this.UIThread(() =>
|
||||
{
|
||||
searchProgressBar.IncrementNoAnimation();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
this.UIThread(() => {
|
||||
this.UIThread(() =>
|
||||
{
|
||||
searchProgressBar.Maximum = selectScriptFileComboBox.Items.Count;
|
||||
});
|
||||
for (int i = 0; i < selectScriptFileComboBox.Items.Count; i++)
|
||||
{
|
||||
ScriptFile scriptFile = new ScriptFile(i);
|
||||
AppLogger.Debug("Attempting to load script " + scriptFile.fileID);
|
||||
scriptsToSearch.Add(scriptFile);
|
||||
this.UIThread(() => {
|
||||
this.UIThread(() =>
|
||||
{
|
||||
searchProgressBar.IncrementNoAnimation();
|
||||
});
|
||||
}
|
||||
@@ -1167,29 +1275,32 @@ namespace DSPRE.Editors
|
||||
return;
|
||||
}
|
||||
BackgroundWorker bw = new BackgroundWorker();
|
||||
bw.DoWork += (_sender, args) => {
|
||||
this.UIThread(() => {
|
||||
searchInScriptsResultListBox.Items.Clear();
|
||||
searchProgressBar.Value = 0;
|
||||
});
|
||||
List<ScriptFile> scriptsToSearch = getScriptsToSearch();
|
||||
|
||||
string searchString = searchInScriptsTextBox.Text;
|
||||
Func<string, bool> searchCriteriaCS = (string s) => s.IndexOf(searchString, StringComparison.InvariantCulture) >= 0;
|
||||
Func<string, bool> searchCriteriaCI = (string s) => s.IndexOf(searchString, StringComparison.InvariantCultureIgnoreCase) >= 0;
|
||||
Func<string, bool> searchCriteria = scriptSearchCaseSensitiveCheckBox.Checked ? searchCriteriaCS : searchCriteriaCI;
|
||||
|
||||
List<ScriptEditorSearchResult> results = new List<ScriptEditorSearchResult>();
|
||||
foreach (ScriptFile scriptFile in scriptsToSearch)
|
||||
bw.DoWork += (_sender, args) =>
|
||||
{
|
||||
List<ScriptEditorSearchResult> scriptResults = SearchInScripts(scriptFile, scriptFile.allScripts, searchCriteria);
|
||||
List<ScriptEditorSearchResult> functionResults = SearchInScripts(scriptFile, scriptFile.allFunctions, searchCriteria);
|
||||
// List<ScriptEditorSearchResult> actionResults = SearchInScripts(scriptFile, scriptFile.allActions, searchCriteria);
|
||||
results.AddRange(scriptResults);
|
||||
results.AddRange(functionResults);
|
||||
// results.AddRange(actionResults);
|
||||
}
|
||||
this.UIThread(() => {
|
||||
this.UIThread(() =>
|
||||
{
|
||||
searchInScriptsResultListBox.Items.Clear();
|
||||
searchProgressBar.Value = 0;
|
||||
});
|
||||
List<ScriptFile> scriptsToSearch = getScriptsToSearch();
|
||||
|
||||
string searchString = searchInScriptsTextBox.Text;
|
||||
Func<string, bool> searchCriteriaCS = (string s) => s.IndexOf(searchString, StringComparison.InvariantCulture) >= 0;
|
||||
Func<string, bool> searchCriteriaCI = (string s) => s.IndexOf(searchString, StringComparison.InvariantCultureIgnoreCase) >= 0;
|
||||
Func<string, bool> searchCriteria = scriptSearchCaseSensitiveCheckBox.Checked ? searchCriteriaCS : searchCriteriaCI;
|
||||
|
||||
List<ScriptEditorSearchResult> results = new List<ScriptEditorSearchResult>();
|
||||
foreach (ScriptFile scriptFile in scriptsToSearch)
|
||||
{
|
||||
List<ScriptEditorSearchResult> scriptResults = SearchInScripts(scriptFile, scriptFile.allScripts, searchCriteria);
|
||||
List<ScriptEditorSearchResult> functionResults = SearchInScripts(scriptFile, scriptFile.allFunctions, searchCriteria);
|
||||
// List<ScriptEditorSearchResult> actionResults = SearchInScripts(scriptFile, scriptFile.allActions, searchCriteria);
|
||||
results.AddRange(scriptResults);
|
||||
results.AddRange(functionResults);
|
||||
// results.AddRange(actionResults);
|
||||
}
|
||||
this.UIThread(() =>
|
||||
{
|
||||
searchInScriptsResultListBox.Items.AddRange(results.ToArray());
|
||||
searchProgressBar.Value = 0;
|
||||
});
|
||||
@@ -1202,6 +1313,12 @@ namespace DSPRE.Editors
|
||||
{
|
||||
List<ScriptEditorSearchResult> results = new List<ScriptEditorSearchResult>();
|
||||
|
||||
// Check if plaintext parsing failed
|
||||
if (commandContainers == null)
|
||||
{
|
||||
return results;
|
||||
}
|
||||
|
||||
for (int j = 0; j < commandContainers.Count; j++)
|
||||
{
|
||||
if (commandContainers[j].commands is null)
|
||||
@@ -1278,7 +1395,8 @@ namespace DSPRE.Editors
|
||||
|
||||
}
|
||||
|
||||
public class ScriptEditorSearchResult {
|
||||
public class ScriptEditorSearchResult
|
||||
{
|
||||
public readonly ScriptFile scriptFile;
|
||||
public readonly ScriptFile.ContainerTypes containerType;
|
||||
public readonly int commandNumber;
|
||||
@@ -1286,7 +1404,8 @@ namespace DSPRE.Editors
|
||||
|
||||
public const int ResultsPadding = 1;
|
||||
|
||||
public ScriptEditorSearchResult(ScriptFile scriptFile, ScriptFile.ContainerTypes containerType, int commandNumber, ScriptCommand scriptCommand) {
|
||||
public ScriptEditorSearchResult(ScriptFile scriptFile, ScriptFile.ContainerTypes containerType, int commandNumber, ScriptCommand scriptCommand)
|
||||
{
|
||||
this.scriptFile = scriptFile;
|
||||
this.containerType = containerType;
|
||||
this.commandNumber = commandNumber;
|
||||
@@ -1295,7 +1414,8 @@ namespace DSPRE.Editors
|
||||
|
||||
public string CommandBlockOpen { get { return $"{containerType} {commandNumber}:"; } }
|
||||
|
||||
public override string ToString() {
|
||||
public override string ToString()
|
||||
{
|
||||
return $"File {scriptFile.fileID} - {CommandBlockOpen} {scriptCommand.name}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using DSPRE.Editors;
|
||||
using DSPRE.Editors;
|
||||
using DSPRE.Editors.BtxEditor;
|
||||
using DSPRE.Resources;
|
||||
using DSPRE.ROMFiles;
|
||||
@@ -982,6 +982,8 @@ namespace DSPRE
|
||||
|
||||
Helpers.statusLabelMessage();
|
||||
this.Text += " - " + RomInfo.projectName;
|
||||
|
||||
ScriptFile.ExportAllScripts();
|
||||
}
|
||||
|
||||
private void saveRom_Click(object sender, EventArgs e)
|
||||
@@ -1012,6 +1014,12 @@ namespace DSPRE
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ScriptFile.BuildRequiredBins())
|
||||
{
|
||||
MessageBox.Show("An error occurred while rebuilding script files. Save aborted.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Helpers.statusLabelMessage("Repacking NARCS...");
|
||||
Update();
|
||||
|
||||
@@ -1270,12 +1278,12 @@ namespace DSPRE
|
||||
case GameFamilies.Plat:
|
||||
WildEditorDPPt wildEditorDppt = new WildEditorDPPt(wildPokeUnpackedPath, RomInfo.GetPokemonNames(),
|
||||
encToOpen, EditorPanels.headerEditor.internalNames.Count);
|
||||
wildEditorDppt.Show();
|
||||
wildEditorDppt.Show();
|
||||
break;
|
||||
default:
|
||||
WildEditorHGSS wildEditorHgss = new WildEditorHGSS(wildPokeUnpackedPath, RomInfo.GetPokemonNames(),
|
||||
encToOpen, EditorPanels.headerEditor.internalNames.Count);
|
||||
wildEditorHgss.Show();
|
||||
wildEditorHgss.Show();
|
||||
break;
|
||||
}
|
||||
Helpers.statusLabelMessage();
|
||||
@@ -1730,7 +1738,7 @@ namespace DSPRE
|
||||
private void settingsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
SettingsWindow editor = new SettingsWindow();
|
||||
editor.Show();
|
||||
editor.Show();
|
||||
}
|
||||
|
||||
private void pokemonDataEditorToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using DSPRE.Resources;
|
||||
using DSPRE.Resources;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
@@ -6,8 +6,10 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace DSPRE.ROMFiles {
|
||||
public class ScriptCommand {
|
||||
namespace DSPRE.ROMFiles
|
||||
{
|
||||
public class ScriptCommand
|
||||
{
|
||||
|
||||
public ushort? id;
|
||||
public List<byte[]> cmdParams;
|
||||
@@ -52,7 +54,8 @@ namespace DSPRE.ROMFiles {
|
||||
}
|
||||
}
|
||||
|
||||
public ScriptCommand(string wholeLine, int lineNumber = 0) {
|
||||
public ScriptCommand(string wholeLine, int lineNumber = 0)
|
||||
{
|
||||
name = wholeLine;
|
||||
cmdParams = new List<byte[]>();
|
||||
|
||||
@@ -60,17 +63,26 @@ namespace DSPRE.ROMFiles {
|
||||
string[] nameParts = processedLine.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); // Separate command code from parameters
|
||||
/* Get command id, which is always first in the description */
|
||||
|
||||
if (RomInfo.ScriptCommandNamesReverseDict.TryGetValue(nameParts[0].ToLower(), out ushort cmdID)) {
|
||||
if (RomInfo.ScriptCommandNamesReverseDict.TryGetValue(nameParts[0].ToLower(), out ushort cmdID))
|
||||
{
|
||||
id = cmdID;
|
||||
} else {
|
||||
try {
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
id = ushort.Parse(nameParts[0].PurgeSpecial(ScriptFile.specialChars), nameParts[0].GetNumberStyle());
|
||||
} catch {
|
||||
}
|
||||
catch
|
||||
{
|
||||
string details;
|
||||
if (wholeLine.Contains(':') && wholeLine.ContainsNumber()) {
|
||||
if (wholeLine.Contains(':') && wholeLine.ContainsNumber())
|
||||
{
|
||||
details = "This probably means you forgot to \"End\" the Script or Function above it.";
|
||||
details += Environment.NewLine + "Please, also note that only Functions can be terminated\nwith \"Return\".";
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
details = "Are you sure it's a proper Script Command?";
|
||||
}
|
||||
|
||||
@@ -86,7 +98,8 @@ namespace DSPRE.ROMFiles {
|
||||
int paramLength = 0;
|
||||
int paramsProcessed = 0;
|
||||
|
||||
if (parametersSizeArr.Length > 0 && parametersSizeArr.First() == 0xFF) {
|
||||
if (parametersSizeArr.Length > 0 && parametersSizeArr.First() == 0xFF)
|
||||
{
|
||||
int firstParamValue = int.Parse(nameParts[1].PurgeSpecial(ScriptFile.specialChars), nameParts[1].GetNumberStyle());
|
||||
byte firstParamSize = parametersSizeArr[1];
|
||||
|
||||
@@ -97,25 +110,27 @@ namespace DSPRE.ROMFiles {
|
||||
int optionsCount = 0;
|
||||
|
||||
bool found = false;
|
||||
while (i < parametersSizeArr.Length) {
|
||||
while (i < parametersSizeArr.Length)
|
||||
{
|
||||
paramLength = parametersSizeArr[i + 1];
|
||||
|
||||
if (parametersSizeArr[i] == firstParamValue) {
|
||||
if (parametersSizeArr[i] == firstParamValue)
|
||||
{
|
||||
//Firstly, build subarray of parameter sizes, starting from the chosen option [firstParamValue]
|
||||
//FOR EXAMPLE: CMD 0x235 and firstParamValue = 5
|
||||
|
||||
// { 0xFF, 2,
|
||||
// 0, 1, 2,
|
||||
// 1, 3, 2, 2, 2,
|
||||
// 2, 0,
|
||||
// 3, 3, 2, 2, 2,
|
||||
// 4, 2, 2, 2,
|
||||
// 5, 3, (2, 2, 2) => this will be the parameters subarray
|
||||
// { 0xFF, 2,
|
||||
// 0, 1, 2,
|
||||
// 1, 3, 2, 2, 2,
|
||||
// 2, 0,
|
||||
// 3, 3, 2, 2, 2,
|
||||
// 4, 2, 2, 2,
|
||||
// 5, 3, (2, 2, 2) => this will be the parameters subarray
|
||||
// 6, 1, 2
|
||||
// },
|
||||
// },
|
||||
byte[] subParametersSize = parametersSizeArr.SubArray(i + 2, paramLength++);
|
||||
|
||||
//Create a slightly bigger temp array
|
||||
//Create a slightly bigger temp array
|
||||
byte[] temp = new byte[1 + subParametersSize.Length];
|
||||
|
||||
//Store the size of the firstParamValue there
|
||||
@@ -134,33 +149,44 @@ namespace DSPRE.ROMFiles {
|
||||
optionsCount++;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
if (!found)
|
||||
{
|
||||
MessageBox.Show($"Command {nameParts[0]} is a special Script Command.\n" +
|
||||
$"The value of the first parameter must be a number in the range [0 - {optionsCount}].\n\n" +
|
||||
$"Line {lineNumber}: {wholeLine}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
id = null;
|
||||
return;
|
||||
}
|
||||
} else if (parametersSizeArr.Length == 1 && parametersSizeArr.First() == 0) {
|
||||
}
|
||||
else if (parametersSizeArr.Length == 1 && parametersSizeArr.First() == 0)
|
||||
{
|
||||
paramLength = 0;
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
paramLength = parametersSizeArr.Length;
|
||||
}
|
||||
|
||||
if (nameParts.Length - 1 == paramLength) {
|
||||
for (int i = paramsProcessed; i < paramLength; i++) {
|
||||
AppLogger.Debug($"Parameter #{i}: {nameParts[i + 1]}");
|
||||
if (nameParts.Length - 1 == paramLength)
|
||||
{
|
||||
for (int i = paramsProcessed; i < paramLength; i++)
|
||||
{
|
||||
//AppLogger.Debug($"Parameter #{i}: {nameParts[i + 1]}");
|
||||
|
||||
if (RomInfo.ScriptComparisonOperatorsReverseDict.TryGetValue(nameParts[i + 1].ToLower(), out cmdID)) { //Check succeeds when command is like "asdfg LESS" or "asdfg DIFFERENT"
|
||||
if (RomInfo.ScriptComparisonOperatorsReverseDict.TryGetValue(nameParts[i + 1].ToLower(), out cmdID))
|
||||
{ //Check succeeds when command is like "asdfg LESS" or "asdfg DIFFERENT"
|
||||
cmdParams.Add(new byte[] { (byte)cmdID });
|
||||
} else { //Not a comparison
|
||||
}
|
||||
else
|
||||
{ //Not a comparison
|
||||
/* Convert strings of parameters to the correct datatypes */
|
||||
NumberStyles numStyle = nameParts[i + 1].GetNumberStyle();
|
||||
if (!nameParts[i + 1].StartsWith("SEQ_")
|
||||
&& !nameParts[i + 1].StartsWith("SPECIES_")
|
||||
if (!nameParts[i + 1].StartsWith("SEQ_")
|
||||
&& !nameParts[i + 1].StartsWith("SPECIES_")
|
||||
&& !nameParts[i + 1].StartsWith("ITEM_")
|
||||
&& !nameParts[i + 1].StartsWith("MOVE_")
|
||||
&& !nameParts[i + 1].StartsWith("TRAINER_")) {
|
||||
&& !nameParts[i + 1].StartsWith("TRAINER_"))
|
||||
{
|
||||
nameParts[i + 1] = nameParts[i + 1].PurgeSpecial(ScriptFile.specialChars);
|
||||
}
|
||||
|
||||
@@ -251,12 +277,16 @@ namespace DSPRE.ROMFiles {
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
MessageBox.Show($"Argument {nameParts[i + 1]} at line {lineNumber} is not in the range [0, {Math.Pow(2, 8 * parametersSizeArr[i]) - 1}].", "Argument error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
string errorMsg = $"Argument {nameParts[i + 1]} at line {lineNumber} is not in the range [0, {Math.Pow(2, 8 * parametersSizeArr[i]) - 1}].";
|
||||
AppLogger.Error($"ScriptCommand parse error: {errorMsg} | Command: {nameParts[0]} (ID: 0x{id:X3}) | Full line: {wholeLine} | Expected param size: {parametersSizeArr[i]} bytes | This may indicate a database error for conditional commands.");
|
||||
MessageBox.Show(errorMsg + $"\n\nCommand: {nameParts[0]} (ID: 0x{id:X3})\nFull line: {wholeLine}\n\nNote: If this is a conditional command like UnionGroup, check your script command database for parameter size errors.", "Argument error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
id = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show($"Wrong number of parameters for command {nameParts[0]} at line {lineNumber}.\n" +
|
||||
$"Received: {nameParts.Length - 1}\n" +
|
||||
$"Expected: {paramLength}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
@@ -324,7 +354,8 @@ namespace DSPRE.ROMFiles {
|
||||
input = input.Trim('[', ']').Replace(" ", "").ToLower();
|
||||
|
||||
var closest = possibilities
|
||||
.Select(x => new {
|
||||
.Select(x => new
|
||||
{
|
||||
Name = x,
|
||||
Distance = LevenshteinDistance(
|
||||
input,
|
||||
@@ -401,6 +432,5 @@ namespace DSPRE.ROMFiles {
|
||||
|
||||
return parameter;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,18 @@ namespace DSPRE.ROMFiles
|
||||
/// </summary>
|
||||
public class ScriptFile : RomFile
|
||||
{
|
||||
// Cache for parsed plaintext scripts to avoid reparsing during search
|
||||
private static Dictionary<string, (DateTime timestamp, ScriptFile cached)> plaintextCache = new Dictionary<string, (DateTime, ScriptFile)>();
|
||||
|
||||
/// <summary>
|
||||
/// Clears the plaintext script cache. Useful when closing ROM or reloading.
|
||||
/// </summary>
|
||||
public static void ClearPlaintextCache()
|
||||
{
|
||||
plaintextCache.Clear();
|
||||
AppLogger.Info("Script: Plaintext cache cleared.");
|
||||
}
|
||||
|
||||
public enum ContainerTypes
|
||||
{
|
||||
Function,
|
||||
@@ -189,9 +201,24 @@ namespace DSPRE.ROMFiles
|
||||
}
|
||||
}
|
||||
|
||||
public ScriptFile(int fileID, bool readFunctions = true, bool readActions = true) : this(getFileStream(fileID), readFunctions, readActions)
|
||||
public ScriptFile(int fileID, bool readFunctions = true, bool readActions = true)
|
||||
{
|
||||
this.fileID = fileID;
|
||||
|
||||
if (TryReadPlaintextIfNewer())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (var fs = getFileStream(fileID))
|
||||
{
|
||||
// Copy the logic from the Stream constructor
|
||||
var tempScript = new ScriptFile(fs, readFunctions, readActions);
|
||||
this.allScripts = tempScript.allScripts;
|
||||
this.allFunctions = tempScript.allFunctions;
|
||||
this.allActions = tempScript.allActions;
|
||||
this.isLevelScript = tempScript.isLevelScript;
|
||||
}
|
||||
}
|
||||
|
||||
static FileStream getFileStream(int fileID)
|
||||
@@ -199,6 +226,379 @@ namespace DSPRE.ROMFiles
|
||||
string path = Filesystem.GetScriptPath(fileID);
|
||||
return new FileStream(path, FileMode.OpenOrCreate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file paths for both binary and plaintext versions of a script file
|
||||
/// </summary>
|
||||
public static (string binPath, string txtPath) GetFilePaths(int fileID)
|
||||
{
|
||||
string binPath = Filesystem.GetScriptPath(fileID);
|
||||
string expandedDir = Path.Combine(RomInfo.workDir, "expanded", "scripts");
|
||||
string txtPath = Path.Combine(expandedDir, $"{fileID:D4}.script");
|
||||
return (binPath, txtPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read the script file from plaintext ONLY if it's newer than the binary
|
||||
/// This is used during batch operations (like search) to respect external edits without slowdown
|
||||
/// Returns true if plaintext exists, is newer, and was successfully parsed
|
||||
/// Uses caching to avoid reparsing the same file multiple times
|
||||
/// </summary>
|
||||
private bool TryReadPlaintextIfNewer()
|
||||
{
|
||||
if (fileID < 0)
|
||||
return false;
|
||||
|
||||
string txtPath = GetFilePaths(fileID).txtPath;
|
||||
string binPath = GetFilePaths(fileID).binPath;
|
||||
|
||||
if (!File.Exists(txtPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DateTime txtTimestamp = File.GetLastWriteTimeUtc(txtPath);
|
||||
|
||||
if (File.Exists(binPath) && txtTimestamp <= File.GetLastWriteTimeUtc(binPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (plaintextCache.TryGetValue(txtPath, out var cached))
|
||||
{
|
||||
if (cached.timestamp == txtTimestamp && cached.cached != null)
|
||||
{
|
||||
// Cache hit! Copy the parsed data
|
||||
this.allScripts = cached.cached.allScripts;
|
||||
this.allFunctions = cached.cached.allFunctions;
|
||||
this.allActions = cached.cached.allActions;
|
||||
this.isLevelScript = cached.cached.isLevelScript;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool success = TryReadPlainTextFileCore();
|
||||
|
||||
if (success)
|
||||
{
|
||||
var cacheEntry = new ScriptFile(this.allScripts, this.allFunctions, this.allActions, fileID);
|
||||
cacheEntry.isLevelScript = this.isLevelScript;
|
||||
plaintextCache[txtPath] = (txtTimestamp, cacheEntry);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core parsing logic for reading plaintext script files
|
||||
/// </summary>
|
||||
private bool TryReadPlainTextFileCore()
|
||||
{
|
||||
string txtPath = GetFilePaths(fileID).txtPath;
|
||||
|
||||
try
|
||||
{
|
||||
string content = File.ReadAllText(txtPath);
|
||||
|
||||
// Split content into sections
|
||||
const string SCRIPTS_HEADER = "//===== SCRIPTS =====//";
|
||||
const string FUNCTIONS_HEADER = "//===== FUNCTIONS =====//";
|
||||
const string ACTIONS_HEADER = "//===== ACTIONS =====//";
|
||||
|
||||
int scriptsStart = content.IndexOf(SCRIPTS_HEADER);
|
||||
int functionsStart = content.IndexOf(FUNCTIONS_HEADER);
|
||||
int actionsStart = content.IndexOf(ACTIONS_HEADER);
|
||||
|
||||
if (scriptsStart == -1 || functionsStart == -1 || actionsStart == -1)
|
||||
{
|
||||
AppLogger.Error($"Script file {fileID:D4} ({txtPath}) has invalid format. Missing section headers. Binary will be re-extracted.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract each section
|
||||
string scriptsSection = content.Substring(
|
||||
scriptsStart + SCRIPTS_HEADER.Length,
|
||||
functionsStart - scriptsStart - SCRIPTS_HEADER.Length
|
||||
).Trim();
|
||||
|
||||
string functionsSection = content.Substring(
|
||||
functionsStart + FUNCTIONS_HEADER.Length,
|
||||
actionsStart - functionsStart - FUNCTIONS_HEADER.Length
|
||||
).Trim();
|
||||
|
||||
string actionsSection = content.Substring(
|
||||
actionsStart + ACTIONS_HEADER.Length
|
||||
).Trim();
|
||||
|
||||
// Parse each section using existing logic
|
||||
var scriptLines = scriptsSection.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
|
||||
var functionLines = functionsSection.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
|
||||
var actionLines = actionsSection.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
|
||||
|
||||
// Use the existing string-based constructor
|
||||
var tempScript = new ScriptFile(scriptLines, functionLines, actionLines, fileID);
|
||||
|
||||
// Check if parsing failed (constructor returns with null lists)
|
||||
if (tempScript.allScripts == null)
|
||||
{
|
||||
AppLogger.Error($"Script file {fileID:D4} ({txtPath}) failed to parse. Binary will be re-extracted.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Copy parsed data to this instance
|
||||
this.allScripts = tempScript.allScripts;
|
||||
this.allFunctions = tempScript.allFunctions;
|
||||
this.allActions = tempScript.allActions;
|
||||
this.isLevelScript = tempScript.isLevelScript;
|
||||
|
||||
AppLogger.Info($"Script file {fileID:D4} loaded from plaintext: {txtPath}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Error($"Script file {fileID:D4} ({txtPath}) - Exception: {ex.Message}. Binary will be re-extracted.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to format script/function containers to plaintext (matches ScriptEditor.displayScriptFile logic)
|
||||
/// </summary>
|
||||
private static void AppendContainerList(StringBuilder content, List<ScriptCommandContainer> commandList, ScriptFile.ContainerTypes containerType)
|
||||
{
|
||||
for (int i = 0; i < commandList.Count; i++)
|
||||
{
|
||||
ScriptCommandContainer scriptCommandContainer = commandList[i];
|
||||
|
||||
/* Write header */
|
||||
string header = containerType + " " + (i + 1);
|
||||
content.Append(header + ':' + Environment.NewLine);
|
||||
|
||||
/* 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++)
|
||||
{
|
||||
ScriptCommand command = scriptCommandContainer.commands[j];
|
||||
if (!ScriptDatabase.endCodes.Contains(command.id))
|
||||
{
|
||||
content.Append('\t');
|
||||
}
|
||||
|
||||
content.Append(command.name + Environment.NewLine);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
content.Append('\t' + "UseScript_#" + scriptCommandContainer.usedScriptID + Environment.NewLine);
|
||||
}
|
||||
|
||||
content.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to format action containers to plaintext (matches ScriptEditor.displayScriptFileActions logic)
|
||||
/// </summary>
|
||||
private static void AppendActionList(StringBuilder content, List<ScriptActionContainer> commandList, ScriptFile.ContainerTypes containerType)
|
||||
{
|
||||
for (int i = 0; i < commandList.Count; i++)
|
||||
{
|
||||
ScriptActionContainer currentCommand = commandList[i];
|
||||
|
||||
string header = containerType + " " + (i + 1);
|
||||
content.Append(header + ':' + Environment.NewLine);
|
||||
|
||||
for (int j = 0; j < currentCommand.commands.Count; j++)
|
||||
{
|
||||
ScriptAction command = currentCommand.commands[j];
|
||||
if (!ScriptDatabase.movementEndCodes.Contains(command.id))
|
||||
{
|
||||
content.Append('\t');
|
||||
}
|
||||
|
||||
content.Append(command.name + Environment.NewLine);
|
||||
}
|
||||
|
||||
content.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the script file to plaintext .script format in expanded/scripts/
|
||||
/// </summary>
|
||||
public void WritePlainTextFile()
|
||||
{
|
||||
if (fileID < 0)
|
||||
return;
|
||||
|
||||
string txtPath = GetFilePaths(fileID).txtPath;
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(txtPath));
|
||||
|
||||
try
|
||||
{
|
||||
StringBuilder content = new StringBuilder();
|
||||
|
||||
// Add file header
|
||||
content.AppendLine("/*");
|
||||
content.AppendLine(" * DSPRE Script File");
|
||||
|
||||
string romFileName = Path.GetFileNameWithoutExtension(RomInfo.projectName);
|
||||
string romFileNameClean = romFileName.EndsWith("_DSPRE_contents")
|
||||
? romFileName.Substring(0, romFileName.Length - "_DSPRE_contents".Length)
|
||||
: romFileName;
|
||||
content.AppendLine(" * Rom ID: " + romFileNameClean);
|
||||
content.AppendLine(" * Game: " + RomInfo.gameFamily);
|
||||
content.AppendLine($" * File: {fileID:D4}");
|
||||
content.AppendLine($" * Generated: {DateTime.Now}");
|
||||
content.AppendLine(" */");
|
||||
content.AppendLine();
|
||||
|
||||
// Add Scripts section
|
||||
content.AppendLine("//===== SCRIPTS =====//");
|
||||
AppendContainerList(content, allScripts, ScriptFile.ContainerTypes.Script);
|
||||
|
||||
// Add Functions section
|
||||
content.AppendLine("//===== FUNCTIONS =====//");
|
||||
AppendContainerList(content, allFunctions, ScriptFile.ContainerTypes.Function);
|
||||
|
||||
// Add Actions section
|
||||
content.AppendLine("//===== ACTIONS =====//");
|
||||
AppendActionList(content, allActions, ScriptFile.ContainerTypes.Action);
|
||||
|
||||
File.WriteAllText(txtPath, content.ToString());
|
||||
AppLogger.Info($"Script file {fileID:D4} written to plaintext: {txtPath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Error($"Failed to write plaintext script file {txtPath}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports all script files from the ROM to expanded/scripts/ on initial load
|
||||
/// This ensures the expanded directory is populated with plaintext versions
|
||||
/// </summary>
|
||||
public static void ExportAllScripts()
|
||||
{
|
||||
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)
|
||||
{
|
||||
AppLogger.Info($"Script: expanded/scripts already exists with {Directory.GetFiles(expandedDir, "*.script").Length} files, skipping initial export.");
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(expandedDir);
|
||||
|
||||
try
|
||||
{
|
||||
// Get count of script files
|
||||
int scriptCount = Filesystem.GetScriptCount();
|
||||
int exportedCount = 0;
|
||||
|
||||
AppLogger.Info($"Script: Beginning export of {scriptCount} script files to {expandedDir}...");
|
||||
|
||||
for (int i = 0; i < scriptCount; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Load from binary only (don't try to read plaintext since we're creating it)
|
||||
using (var fs = getFileStream(i))
|
||||
{
|
||||
var scriptFile = new ScriptFile(fs, true, true);
|
||||
scriptFile.fileID = i;
|
||||
scriptFile.WritePlainTextFile();
|
||||
exportedCount++;
|
||||
}
|
||||
|
||||
// Touch the binary file to make it "newer" than the plaintext
|
||||
// This ensures search performance isn't impacted (binary used unless plaintext is edited)
|
||||
string binPath = GetFilePaths(i).binPath;
|
||||
if (File.Exists(binPath))
|
||||
{
|
||||
File.SetLastWriteTimeUtc(binPath, DateTime.UtcNow.AddSeconds(1));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Error($"Failed to export script {i:D4}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
AppLogger.Info($"Script: Exported {exportedCount} of {scriptCount} script files to {expandedDir}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Error($"Failed to export scripts: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans expanded/scripts/ directory and rebuilds binary script files that are older than their plaintext versions
|
||||
/// Call this during ROM save, similar to TextArchive.BuildRequiredBins()
|
||||
/// </summary>
|
||||
public static bool BuildRequiredBins()
|
||||
{
|
||||
string expandedDir = Path.Combine(RomInfo.workDir, "expanded", "scripts");
|
||||
|
||||
if (!Directory.Exists(expandedDir))
|
||||
{
|
||||
AppLogger.Info("Script: No expanded scripts directory found, skipping .bin rebuild.");
|
||||
return true;
|
||||
}
|
||||
|
||||
var expandedScriptFiles = Directory.GetFiles(expandedDir, "*.script", SearchOption.AllDirectories);
|
||||
int newerBinCount = 0;
|
||||
int rebuiltCount = 0;
|
||||
|
||||
for (int i = 0; i < expandedScriptFiles.Length; i++)
|
||||
{
|
||||
string expandedScriptFile = expandedScriptFiles[i];
|
||||
string fileName = Path.GetFileNameWithoutExtension(expandedScriptFile);
|
||||
|
||||
int scriptID;
|
||||
|
||||
try
|
||||
{
|
||||
scriptID = int.Parse(fileName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
AppLogger.Error($"Skipping invalid script file name: {fileName}");
|
||||
continue;
|
||||
}
|
||||
|
||||
string binPath = ScriptFile.GetFilePaths(scriptID).binPath;
|
||||
|
||||
// Skip if .bin is newer than .script
|
||||
if (File.Exists(binPath) && File.GetLastWriteTimeUtc(binPath) > File.GetLastWriteTimeUtc(expandedScriptFile))
|
||||
{
|
||||
newerBinCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var scriptFile = new ScriptFile(scriptID);
|
||||
scriptFile.SaveToFileDefaultDir(scriptID, false);
|
||||
rebuiltCount++;
|
||||
|
||||
// Update .script last write time to prevent it being overwritten when reopening the ROM
|
||||
File.SetLastWriteTimeUtc(expandedScriptFile, DateTime.UtcNow);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Error($"Failed to rebuild script {scriptID:D4} from plaintext: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
AppLogger.Info($"Script: {rebuiltCount} .bin files built from .script, {newerBinCount} .bin files skipped because they were newer than the .script");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string prefix = isLevelScript ? "Level " : "";
|
||||
@@ -1121,7 +1521,14 @@ namespace DSPRE.ROMFiles
|
||||
|
||||
public bool SaveToFileDefaultDir(int IDtoReplace, bool showSuccessMessage = true)
|
||||
{
|
||||
return SaveToFileDefaultDir(RomInfo.DirNames.scripts, IDtoReplace, showSuccessMessage);
|
||||
bool success = SaveToFileDefaultDir(RomInfo.DirNames.scripts, IDtoReplace, showSuccessMessage);
|
||||
|
||||
if (success)
|
||||
{
|
||||
WritePlainTextFile();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
public void SaveToFileExplorePath(string suggestedFileName, bool blindmode)
|
||||
|
||||
Reference in New Issue
Block a user